Fixes #26245. From furszy:
Update best_header inside Connect/DisconnectTip This solves a problem where best_header isn’t updated in case of forced reorg like a block invalidation and reconsideration.
I added a test.
Fixes #26245. From furszy:
Update best_header inside Connect/DisconnectTip This solves a problem where best_header isn’t updated in case of forced reorg like a block invalidation and reconsideration.
I added a test.
1568@@ -1569,6 +1569,7 @@ static RPCHelpMan reconsiderblock()
1569 throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found");
1570 }
1571
1572+ chainman.m_best_header = pblockindex;
What happens if the ActivateBestChain()
below fails? You set m_best_header
but the reconsiderblock
call failed.
pblockindex
is also not guaranteed to be the index for the tip of the reconsidered chain, so you end up with the same inconsistent output as described in #26245. You can test that invalidating and reconsidering an older block, e.g.:
0bitcoin-cli getblockchaininfo
1bitcoin-cli invalidateblock <blockhash at height=tip-5>
2bitcoin-cli getblockchaininfo
3bitcoin-cli reconsiderblock <blockhash at height=tip-5>
4bitcoin-cli getblockchaininfo
I think this is a validation bug not a rpc bug, so setting m_best_header
(validation specific state) on an rpc call does not seem right to me.
Currently, we only ever set m_best_header
in three places:
When we add a new header to our index:
When an invalid block is found:
Or upon startup while loading the block index into memory:
afaict anytime we are reorging to an already known chain (i.e. we already have the headers in our index) we will not set m_best_headers
until a new header is added to our index on that chain.
I’ll try this patch on ForkMonitor, at least for the v0.21.2 node (using g_best_block
). We run two nodes that constantly roll back the chain in order to call gettxoutsetinfo
on them (no longer needed for new nodes thanks to #24539) as well as to fetch stale blocks that we only have headers for. (and then it would call reconsiderblock
).
I’ve occasionally gotten error emails where the resulting tip was not what was expected (and I think these happen just after a rollback).
That said, I agree with @dergoegge that we probably shouldn’t touch m_best_header
directly from RPC code.
ActivateBestChain
in validation.cpp
. Is it better?
I also added your test vector @dergoegge.
3017@@ -3018,6 +3018,7 @@ bool Chainstate::ActivateBestChain(BlockValidationState& state, std::shared_ptr<
3018 pindexMostWork = nullptr;
3019 }
3020 pindexNewTip = m_chain.Tip();
3021+ m_chainman.m_best_header = pindexNewTip;
0 if(pindexNewTip->nChainWork > m_chainman.m_best_header->nChainWork) {
1 m_chainman.m_best_header = pindexNewTip;
2 }
Quick thing: in case of regular headers-sync, we only reorg if the chain has a higher diff, so best_header
field is fine there.
In case of forced reorgs, like a block invalidation + reconsideration, we aren’t reorganizing the chain due PoW, thus why the best_header
isn’t updated (we currently only update the best_header
field after adding it to the block index, inside AddToBlockIndex
).
So, for those cases, could either update best_header
inside ConnectTip
and DisconnectTip
like https://github.com/furszy/bitcoin-core/commit/52f5d62957ad0d9e2b2c8d00333a369fb4a369c7 or.. update it inside the reconsider block process only (the invalidate block process is fine because we are updating the best_header inside InvalidChainFound
).
2631@@ -2632,6 +2632,12 @@ bool Chainstate::DisconnectTip(BlockValidationState& state, DisconnectedBlockTra
2632
2633 m_chain.SetTip(*pindexDelete->pprev);
2634
2635+ // if we are roll-backing the chain, rollback the best header as well
2636+ if (m_chainman.m_best_header && m_chainman.m_best_header->GetBlockHash() == pindexDelete->GetBlockHash()
2637+ && pindexDelete->pprev) {
2750@@ -2745,6 +2751,12 @@ bool Chainstate::ConnectTip(BlockValidationState& state, CBlockIndex* pindexNew,
2751 }
2752 // Update m_chain & related variables.
2753 m_chain.SetTip(*pindexNew);
2754+
2755+ // If the best header is the prev block, update the best header
2756+ if (pindexNew->pprev && m_chainman.m_best_header && m_chainman.m_best_header->GetBlockHash() == pindexNew->pprev->GetBlockHash()) {
0 if (pindexNew->pprev == m_chainman.m_best_header) {
?
This feels like a special-cased hack. Is there a reason we can’t adjust the current code that changes m_best_header when an invalid block is encountered?
(Also, PR title needs updating.)
This solves a problem where best_header isn't updated in case of forced reorg
like a block invalidation and reconsideration.
This feels like a special-cased hack. Is there a reason we can’t adjust the current code that changes m_best_header when an invalid block is encountered?
I actually wrote two ways to fix this above #26260 (comment).
The problem is on the block reconsideration flow, not inside the invalidation one.
Inside the invalidateblock
process, we update the best_header by
manually calling InvalidChainFound
after invalidating blocks. Which is fine.
We need to do the same for reconsiderblock
and update the
best_header field after finishing the process.
Note: the only difference between this two commands is that ‘reconsiderblock’ does not have its own separate function, it’s a plain RPC command that resets the block index flag and calls ActivateBestChain.
So, @aureleoules, could drop the other commit and cherry-pick this change to fix the problem too: https://github.com/furszy/bitcoin-core/commit/4f3318c684af631d7f79b872cd7b8d4feae4b828
Both works. This last one is specific to the reconsiderblock
flow (which is where we have the issue).
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
See the guideline for information on the review process.
Type | Reviewers |
---|---|
Concept ACK | pinheadmz |
If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.
invalidateblock
and reconsiderblock
. It’s cherry-picked on top of the v25.0rc1 and v24.1rc2 tags. Typically we get about one error message per week where the block it rewinds to is not the one we expected. We’ll see if that stops.
2631@@ -2632,6 +2632,12 @@ bool Chainstate::DisconnectTip(BlockValidationState& state, DisconnectedBlockTra
2632
2633 m_chain.SetTip(*pindexDelete->pprev);
2634
2635+ // if we are roll-backing the chain, rollback the best header as well
2636+ if (m_chainman.m_best_header && m_chainman.m_best_header->GetBlockHash() == pindexDelete->GetBlockHash()) {
2637+ assert(pindexDelete->pprev);
2638+ m_chainman.m_best_header = pindexDelete->pprev;
2639+ }
headers == blocks
isn’t enough? Might need to use getchaintips
to cover.
concept ACK
one question below.
19@@ -20,6 +20,10 @@ def set_test_params(self):
20 def setup_network(self):
21 self.setup_nodes()
22
23+ def check_blocks_headers(self, node):
24+ blockchaininfos = node.getblockchaininfo()
There hasn’t been much activity lately. What is the status here?
Finding reviewers may take time. However, if the patch is no longer relevant, please close this pull request. If the author lost interest or time to work on this, please close it and mark it ‘Up for grabs’ with the label, so that it can be picked up in the future.