-
Notifications
You must be signed in to change notification settings - Fork 37.8k
index: Commit MuHash and best block together for coinstatsindex #24138
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
index: Commit MuHash and best block together for coinstatsindex #24138
Conversation
FYI @fjahr |
2c630fe
to
010156c
Compare
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. ConflictsReviewers, this pull request conflicts with the following ones:
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first. |
CDBBatch batch(*m_db); | ||
batch.Write(DBHeightKey(pindex->nHeight), value); | ||
batch.Write(DB_MUHASH, m_muhash); | ||
return m_db->WriteBatch(batch); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see why this should change. The CommitInternal
override feels like a hacky refactor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is spot is problematic, and the reason for the behavior reported in #24076:
If we persist DB_MUHASH
here in each BlockConnected()
callback, but not DB_BEST_BLOCK
(which is persisted only in ChainStateFlushed()
callbacks and will therefore at this point in time have a lower height), the db is temporarily in an inconsistent state.
So in case of an unclean shutdown (with no time to flush the chainstate to disk to get in sync again), on next startup the index will initialize with the older DB_BEST_BLOCK
but a DB_MUHASH
which contains data from newer blocks. The index then reprocesses the blocks after DB_BEST_BLOCK
's height a second time.
But MuHash is a rolling hash, so it will incorporate data from these blocks twice, and the hash will be incorrect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it was not considered hacky by the author of the current index architecture because it's done the same way in blockfilterindex.cpp
and the documentation in index/base.h
also states that this method can be overridden.
Obviously, I did it differently here but I can't remember if that was a deliberate choice or rather just lack of understanding (probably the latter).
And that doesn't mean there isn't mayor room for refactoring here. But it looks like #24230 should be considered first when thinking about it first. I have not started reviewing it yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In commit "index: Commit DB_MUHASH and DB_BEST_BLOCK to disk together" (010156c)
The
CommitInternal
override feels like a hacky refactor.
I think I agree with luke if the point is that the override-and-call-base class code in CommitInternal
is a little hacky, but fjahr's right this just the way the base index class was designed, so the override seems like appropriate way to handle this now. #24230 does clean up all the overrides and will simplify this a bit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to make this clear: This is not a refactor, it changes behavior (the points in time when DB_MUHASH
is written to disk).
My understanding is that the indexes work in general such that that all persisted data describing the current progress of indexing (such as the best block) must be in sync with the persisted chainstate information (which is sometimes not flushed for several blocks received) - that is why ChainStateFlushed()
callbacks are used. In case of an unclean shutdown, the chainstate will be constructed from this point on, so the indexes must as well.
In contrast to this is the actual indexed data for each block - I think we could in principle cache it and also write it to disk with each ChainStateFlushed()
callback and not use BlockConnected()
at all.
But it is also possible to write it to disk in advance with each BlockConnected()
callback, as it is done:
This is dependent on the assumption that we will not read (and possibly overwrite) the indexed data in case it is ahead of the flushed chainstate data (and the best block), and also that before flushing the chainstate, we have called BlockConnected()
for each block.
With this in mind, I don't see a better way of doing this other than overriding CommitInternal
.
@@ -481,5 +486,5 @@ bool CoinStatsIndex::ReverseBlock(const CBlock& block, const CBlockIndex* pindex | |||
Assert(m_total_unspendables_scripts == read_out.second.total_unspendables_scripts); | |||
Assert(m_total_unspendables_unclaimed_rewards == read_out.second.total_unspendables_unclaimed_rewards); | |||
|
|||
return m_db->Write(DB_MUHASH, m_muhash); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like the only problem is this lone line?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I replace only that line with true
(or false
for that matter) the reorg test in feature_coinstatsindex.py
still passes. But not sure if that helps with the original issue (and it indicates an obvious test coverage omission, cc @fjahr).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only replacing this line means the rolling muhash
state is not persisted during reorgs. That would be a problem if the node gets shut down during the reorg, then we would risk an inconsistent state or probably the index wouldn't even start up anymore because the guard check in Init()
would fail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if the node gets shut down during the reorg,
I guess this is very non trivial to test, but maybe add a comment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a comment at the end of CoinStatsIndex::WriteBlock
(as suggested by ryanofsky below). I think that duplicating this comment in CoinStatsIndex::ReverseBlock
is not required, since this function doesn't write anything else to disk there wouldn't really be the expectation that for it to write DB_MUHASH
here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Concept ACK
It might be worth rebasing this on #24133 and if at all possible, add a test that triggers the error added there.
I think overriding CommitInternal
makes sense given that we require DB_MUHASH
and DB_BEST_BLOCK
to be in sync (which we explicitly check as of #24133).
The alternative would be to drop DB_MUHASH
altogether, but since MuHash can't (?) be initialized from a finalized hash, we would have to index both the denominator and numerator.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code review ACK 010156c
While I can not reproduce the issue I agree that this is likely to fix #24076.
I think there could still be inconsistencies between the running muhash and the coinstats data since those are not batched anymore now but at least that would then lead to a failure in Init()
at restart. This could be avoided by squashing all of that into CommitInternal()
as well (drafted here) but I don't think this is necessary right now. As I have reviewed this I am now really not sure about the boundaries between Commit()
and WriteBlock()
anymore and will think about refactoring that while reviewing #24230 (maybe that's already solved there).
@@ -481,5 +486,5 @@ bool CoinStatsIndex::ReverseBlock(const CBlock& block, const CBlockIndex* pindex | |||
Assert(m_total_unspendables_scripts == read_out.second.total_unspendables_scripts); | |||
Assert(m_total_unspendables_unclaimed_rewards == read_out.second.total_unspendables_unclaimed_rewards); | |||
|
|||
return m_db->Write(DB_MUHASH, m_muhash); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only replacing this line means the rolling muhash
state is not persisted during reorgs. That would be a problem if the node gets shut down during the reorg, then we would risk an inconsistent state or probably the index wouldn't even start up anymore because the guard check in Init()
would fail.
CDBBatch batch(*m_db); | ||
batch.Write(DBHeightKey(pindex->nHeight), value); | ||
batch.Write(DB_MUHASH, m_muhash); | ||
return m_db->WriteBatch(batch); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it was not considered hacky by the author of the current index architecture because it's done the same way in blockfilterindex.cpp
and the documentation in index/base.h
also states that this method can be overridden.
Obviously, I did it differently here but I can't remember if that was a deliberate choice or rather just lack of understanding (probably the latter).
And that doesn't mean there isn't mayor room for refactoring here. But it looks like #24230 should be considered first when thinking about it first. I have not started reviewing it yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code review ACK 010156c
It would be good to add a unit test to coinstatsindex_tests that triggers the issue this is supposed to fix. I think the test would just need to make the index object, send a BlockConnected notification, and then destroy the index object without sending ChainStateFlushed notification. This would save a corrupted index that should result in errors next time it is loaded.
re: #24138 (review)
I think there could still be inconsistencies between the running muhash and the coinstats data since those are not batched anymore now but at least that would then lead to a failure in
Init()
at restart.
I could be wrong but this seems fine to me since coinstats data is accessed by height and written before the muhash & best block. So if the coinstats data is ahead of the muhash. it is just ignored. In your suggested followup a90394e, I think the if (!m_db->Read(DBHeightKey(pindex->nHeight))
check would be good to add, but I think it should it should just trigger a fatal error if the key isn't found. I don't see how the recovery code added in that commit would work.
#24230 (maybe that's already solved there).
My guess is #24230 doesn't affect this, since it's trying to leave index's internal behavior the same, and just change the way it receives block connected and disconnected notifications, without changing the way it handles them.
CDBBatch batch(*m_db); | ||
batch.Write(DBHeightKey(pindex->nHeight), value); | ||
batch.Write(DB_MUHASH, m_muhash); | ||
return m_db->WriteBatch(batch); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In commit "index: Commit DB_MUHASH and DB_BEST_BLOCK to disk together" (010156c)
The
CommitInternal
override feels like a hacky refactor.
I think I agree with luke if the point is that the override-and-call-base class code in CommitInternal
is a little hacky, but fjahr's right this just the way the base index class was designed, so the override seems like appropriate way to handle this now. #24230 does clean up all the overrides and will simplify this a bit.
If these are written to disk at different times, unclean shutdowns can lead to index corruption.
010156c
to
eb6cc05
Compare
I pushed an update, rebasing (with #24133 merged) and addressing feedback (adding a comment).
This would be great. I haven't done this with this push, because |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code review ACK eb6cc05. Only changes since previous review are rebasing and adding comments.
@mzumsande I wrote a little test which passes with this bugfix, and fails with the fix reverted. Feel free to add it to this PR or use the code in any way: ded9fda d3c2ebb
Thank you! The test looks great, I added your commit to this PR (and confirmed that it fails on master). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code review ACK 691d45f. Only change since last review is adding test
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{ | ||
CoinStatsIndex index{1 << 20}; | ||
// Make sure the index can be loaded. | ||
BOOST_REQUIRE(index.Start(chainstate)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in 691d45f:
Not sure why we need this whole block with the new index
instead of reusing the old index
above. It doesn't seem to make a difference in my testing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a huge cost to spinning up the second index? If not, I think both versions are fine.
Fixes #24076
Coinstatsindex currently writes the MuHash (
DB_MUHASH
) to disk inCoinStatsIndex::WriteBlock()
andCoinStatsIndex::ReverseBlock()
, but the best synced block is written inBaseIndex::Commit()
. These are called at different points in time, both during the ThreadSync phase, and also after the initial sync is finished and validation callbacks (BlockConnected()
vsChainStateFlushed()
) perform the syncing.As a result, the index DB is temporarily in an inconsistent state, and if bitcoind is terminated uncleanly (so that there is no time to call
Commit()
by receiving an interrupt or by flushing the chainstate) this leads to problems:On the next startup,
Init()
will read the best block and a MuHash that corresponds to a different (higher) block. Indexing will be picked up at the the best block processing some blocks again, but since MuHash is a rolling hash, it will process some utxos twice and the muhashes for all future blocks will be wrong, as was observed in #24076.Fix this by always committing
DB_MUHASH
together withDB_BEST_BLOCK
.Note that the block data for the index is still written at different times, but this does not corrupt the index - at worst, these entries will be processed another time and overwritten after an unclean shutdown and restart.