Skip to content

Commit 85bcfee

Browse files
committed
Merge bitcoin/bitcoin#30666: validation: fix m_best_header tracking and BLOCK_FAILED_CHILD assignment
0bd53d9 test: add test for getchaintips behavior with invalid chains (Martin Zumsande) ccd98ea test: cleanup rpc_getchaintips.py (Martin Zumsande) f5149dd validation: mark blocks building on an invalid block as BLOCK_FAILED_CHILD (Martin Zumsande) 783cb73 validation: call RecalculateBestHeader in InvalidChainFound (Martin Zumsande) 9275e96 rpc: call RecalculateBestHeader as part of reconsiderblock (Martin Zumsande) a51e917 validation: add RecalculateBestHeader() function (Martin Zumsande) Pull request description: `m_best_header` (the most-work header not known to be on an invalid chain) can be wrong in the context of invalidation / reconsideration of blocks. This can happen naturally (a valid header is received and stored in our block tree db; when the full block arrives, it is found to be invalid) or triggered by the user with the `invalidateblock` / `reconsiderblock` rpc. We don't currently use `m_best_header` for any critical things (see OP of #16974 for a list that still seems up-to-date), so it being wrong affects mostly rpcs. This PR proposes to recalculate it if necessary by looping over the block index and finding the best header. It also suggest to mark headers between an invalidatetd block and the previous `m_best_header` as invalid, so they won't be considered in the recalculation. It adds tests to `rpc_invalidateblock.py` and `rpc_getchaintips.py` that fail on master. One alternative to this suggested in the past would be to introduce a continuous tracking of header tips (#12138). While this might be more performant, it is also more complicated, and situations where we need this data are only be remotely triggerable by paying the cost of creating a valid PoW header for an invalid block. Therefore I think it isn't necessary to optimise for performance here, plus the solution in this PR doesn't perform any extra steps in the normal node operation where no invalidated blocks are encountered. Fixes #26245 ACKs for top commit: fjahr: reACK 0bd53d9 achow101: ACK 0bd53d9 TheCharlatan: Re-ACK 0bd53d9 Tree-SHA512: 23c2fc42d7c7bb4f9b4ba4949646b3d0031dd29ed15484e436afd66cd821ed48e0f16a1d02f45477b5d0d73a006f6e81a56b82d9721e0dee2e924219f528b445
2 parents 2257c6d + 0bd53d9 commit 85bcfee

File tree

5 files changed

+113
-21
lines changed

5 files changed

+113
-21
lines changed

src/rpc/blockchain.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1647,6 +1647,7 @@ void ReconsiderBlock(ChainstateManager& chainman, uint256 block_hash) {
16471647
}
16481648

16491649
chainman.ActiveChainstate().ResetBlockFailureFlags(pblockindex);
1650+
chainman.RecalculateBestHeader();
16501651
}
16511652

16521653
BlockValidationState state;

src/validation.cpp

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2056,8 +2056,9 @@ void Chainstate::InvalidChainFound(CBlockIndex* pindexNew)
20562056
if (!m_chainman.m_best_invalid || pindexNew->nChainWork > m_chainman.m_best_invalid->nChainWork) {
20572057
m_chainman.m_best_invalid = pindexNew;
20582058
}
2059+
SetBlockFailureFlags(pindexNew);
20592060
if (m_chainman.m_best_header != nullptr && m_chainman.m_best_header->GetAncestor(pindexNew->nHeight) == pindexNew) {
2060-
m_chainman.m_best_header = m_chain.Tip();
2061+
m_chainman.RecalculateBestHeader();
20612062
}
20622063

20632064
LogPrintf("%s: invalid block=%s height=%d log2_work=%f date=%s\n", __func__,
@@ -3793,6 +3794,17 @@ bool Chainstate::InvalidateBlock(BlockValidationState& state, CBlockIndex* pinde
37933794
return true;
37943795
}
37953796

3797+
void Chainstate::SetBlockFailureFlags(CBlockIndex* invalid_block)
3798+
{
3799+
AssertLockHeld(cs_main);
3800+
3801+
for (auto& [_, block_index] : m_blockman.m_block_index) {
3802+
if (block_index.GetAncestor(invalid_block->nHeight) == invalid_block && !(block_index.nStatus & BLOCK_FAILED_MASK)) {
3803+
block_index.nStatus |= BLOCK_FAILED_CHILD;
3804+
}
3805+
}
3806+
}
3807+
37963808
void Chainstate::ResetBlockFailureFlags(CBlockIndex *pindex) {
37973809
AssertLockHeld(cs_main);
37983810

@@ -6404,6 +6416,17 @@ std::optional<int> ChainstateManager::GetSnapshotBaseHeight() const
64046416
return base ? std::make_optional(base->nHeight) : std::nullopt;
64056417
}
64066418

6419+
void ChainstateManager::RecalculateBestHeader()
6420+
{
6421+
AssertLockHeld(cs_main);
6422+
m_best_header = ActiveChain().Tip();
6423+
for (auto& entry : m_blockman.m_block_index) {
6424+
if (!(entry.second.nStatus & BLOCK_FAILED_MASK) && m_best_header->nChainWork < entry.second.nChainWork) {
6425+
m_best_header = &entry.second;
6426+
}
6427+
}
6428+
}
6429+
64076430
bool ChainstateManager::ValidatedSnapshotCleanup()
64086431
{
64096432
AssertLockHeld(::cs_main);

src/validation.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,9 @@ class Chainstate
729729
EXCLUSIVE_LOCKS_REQUIRED(!m_chainstate_mutex)
730730
LOCKS_EXCLUDED(::cs_main);
731731

732+
/** Set invalidity status to all descendants of a block */
733+
void SetBlockFailureFlags(CBlockIndex* pindex) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
734+
732735
/** Remove invalidity status from a block and its descendants. */
733736
void ResetBlockFailureFlags(CBlockIndex* pindex) EXCLUSIVE_LOCKS_REQUIRED(cs_main);
734737

@@ -1314,6 +1317,11 @@ class ChainstateManager
13141317
//! nullopt.
13151318
std::optional<int> GetSnapshotBaseHeight() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
13161319

1320+
//! If, due to invalidation / reconsideration of blocks, the previous
1321+
//! best header is no longer valid / guaranteed to be the most-work
1322+
//! header in our block-index not known to be invalid, recalculate it.
1323+
void RecalculateBestHeader() EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
1324+
13171325
CCheckQueue<CScriptCheck>& GetCheckQueue() { return m_script_check_queue; }
13181326

13191327
~ChainstateManager();

test/functional/rpc_getchaintips.py

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
- verify that getchaintips now returns two chain tips.
1111
"""
1212

13+
from test_framework.blocktools import (
14+
create_block,
15+
create_coinbase,
16+
)
1317
from test_framework.test_framework import BitcoinTestFramework
1418
from test_framework.util import assert_equal
1519

@@ -18,44 +22,73 @@ def set_test_params(self):
1822
self.num_nodes = 4
1923

2024
def run_test(self):
25+
self.log.info("Test getchaintips behavior with two chains of different length")
2126
tips = self.nodes[0].getchaintips()
2227
assert_equal(len(tips), 1)
2328
assert_equal(tips[0]['branchlen'], 0)
2429
assert_equal(tips[0]['height'], 200)
2530
assert_equal(tips[0]['status'], 'active')
2631

27-
# Split the network and build two chains of different lengths.
32+
self.log.info("Split the network and build two chains of different lengths.")
2833
self.split_network()
2934
self.generate(self.nodes[0], 10, sync_fun=lambda: self.sync_all(self.nodes[:2]))
3035
self.generate(self.nodes[2], 20, sync_fun=lambda: self.sync_all(self.nodes[2:]))
3136

32-
tips = self.nodes[1].getchaintips ()
33-
assert_equal (len (tips), 1)
37+
tips = self.nodes[1].getchaintips()
38+
assert_equal(len(tips), 1)
3439
shortTip = tips[0]
35-
assert_equal (shortTip['branchlen'], 0)
36-
assert_equal (shortTip['height'], 210)
37-
assert_equal (tips[0]['status'], 'active')
40+
assert_equal(shortTip['branchlen'], 0)
41+
assert_equal(shortTip['height'], 210)
42+
assert_equal(tips[0]['status'], 'active')
3843

39-
tips = self.nodes[3].getchaintips ()
40-
assert_equal (len (tips), 1)
44+
tips = self.nodes[3].getchaintips()
45+
assert_equal(len(tips), 1)
4146
longTip = tips[0]
42-
assert_equal (longTip['branchlen'], 0)
43-
assert_equal (longTip['height'], 220)
44-
assert_equal (tips[0]['status'], 'active')
47+
assert_equal(longTip['branchlen'], 0)
48+
assert_equal(longTip['height'], 220)
49+
assert_equal(tips[0]['status'], 'active')
4550

46-
# Join the network halves and check that we now have two tips
51+
self.log.info("Join the network halves and check that we now have two tips")
4752
# (at least at the nodes that previously had the short chain).
48-
self.join_network ()
53+
self.join_network()
4954

50-
tips = self.nodes[0].getchaintips ()
51-
assert_equal (len (tips), 2)
52-
assert_equal (tips[0], longTip)
55+
tips = self.nodes[0].getchaintips()
56+
assert_equal(len(tips), 2)
57+
assert_equal(tips[0], longTip)
5358

54-
assert_equal (tips[1]['branchlen'], 10)
55-
assert_equal (tips[1]['status'], 'valid-fork')
59+
assert_equal(tips[1]['branchlen'], 10)
60+
assert_equal(tips[1]['status'], 'valid-fork')
5661
tips[1]['branchlen'] = 0
5762
tips[1]['status'] = 'active'
58-
assert_equal (tips[1], shortTip)
63+
assert_equal(tips[1], shortTip)
64+
65+
self.log.info("Test getchaintips behavior with invalid blocks")
66+
self.disconnect_nodes(0, 1)
67+
n0 = self.nodes[0]
68+
tip = int(n0.getbestblockhash(), 16)
69+
start_height = self.nodes[0].getblockcount()
70+
# Create invalid block (too high coinbase)
71+
block_time = n0.getblock(n0.getbestblockhash())['time'] + 1
72+
invalid_block = create_block(tip, create_coinbase(start_height+1, nValue=100), block_time)
73+
invalid_block.solve()
74+
75+
block_time += 1
76+
block2 = create_block(invalid_block.sha256, create_coinbase(2), block_time, version=4)
77+
block2.solve()
78+
79+
self.log.info("Submit headers-only chain")
80+
n0.submitheader(invalid_block.serialize().hex())
81+
n0.submitheader(block2.serialize().hex())
82+
tips = n0.getchaintips()
83+
assert_equal(len(tips), 3)
84+
assert_equal(tips[0]['status'], 'headers-only')
85+
86+
self.log.info("Submit invalid block that invalidates the headers-only chain")
87+
n0.submitblock(invalid_block.serialize().hex())
88+
tips = n0.getchaintips()
89+
assert_equal(len(tips), 3)
90+
assert_equal(tips[0]['status'], 'invalid')
91+
5992

6093
if __name__ == '__main__':
6194
GetChainTipsTest(__file__).main()

test/functional/rpc_invalidateblock.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

77
from test_framework.test_framework import BitcoinTestFramework
88
from test_framework.address import ADDRESS_BCRT1_UNSPENDABLE_DESCRIPTOR
9+
from test_framework.blocktools import (
10+
create_block,
11+
create_coinbase,
12+
)
913
from test_framework.util import (
1014
assert_equal,
1115
assert_raises_rpc_error,
@@ -35,12 +39,33 @@ def run_test(self):
3539
self.connect_nodes(0, 1)
3640
self.sync_blocks(self.nodes[0:2])
3741
assert_equal(self.nodes[0].getblockcount(), 6)
38-
badhash = self.nodes[1].getblockhash(2)
42+
43+
# Add a header to the tip of node 0 without submitting the block. This shouldn't
44+
# affect results since this chain will be invalidated next.
45+
tip = self.nodes[0].getbestblockhash()
46+
block_time = self.nodes[0].getblock(self.nodes[0].getbestblockhash())['time'] + 1
47+
block = create_block(int(tip, 16), create_coinbase(self.nodes[0].getblockcount()), block_time, version=4)
48+
block.solve()
49+
self.nodes[0].submitheader(block.serialize().hex())
50+
assert_equal(self.nodes[0].getblockchaininfo()["headers"], self.nodes[0].getblockchaininfo()["blocks"] + 1)
3951

4052
self.log.info("Invalidate block 2 on node 0 and verify we reorg to node 0's original chain")
53+
badhash = self.nodes[1].getblockhash(2)
54+
self.nodes[0].invalidateblock(badhash)
55+
assert_equal(self.nodes[0].getblockcount(), 4)
56+
assert_equal(self.nodes[0].getbestblockhash(), besthash_n0)
57+
# Should report consistent blockchain info
58+
assert_equal(self.nodes[0].getblockchaininfo()["headers"], self.nodes[0].getblockchaininfo()["blocks"])
59+
60+
self.log.info("Reconsider block 6 on node 0 again and verify that the best header is set correctly")
61+
self.nodes[0].reconsiderblock(tip)
62+
assert_equal(self.nodes[0].getblockchaininfo()["headers"], self.nodes[0].getblockchaininfo()["blocks"] + 1)
63+
64+
self.log.info("Invalidate block 2 on node 0 and verify we reorg to node 0's original chain again")
4165
self.nodes[0].invalidateblock(badhash)
4266
assert_equal(self.nodes[0].getblockcount(), 4)
4367
assert_equal(self.nodes[0].getbestblockhash(), besthash_n0)
68+
assert_equal(self.nodes[0].getblockchaininfo()["headers"], self.nodes[0].getblockchaininfo()["blocks"])
4469

4570
self.log.info("Make sure we won't reorg to a lower work chain:")
4671
self.connect_nodes(1, 2)
@@ -83,6 +108,8 @@ def run_test(self):
83108
self.nodes[1].reconsiderblock(blocks[-4])
84109
# Should be back at the tip by now
85110
assert_equal(self.nodes[1].getbestblockhash(), blocks[-1])
111+
# Should report consistent blockchain info
112+
assert_equal(self.nodes[1].getblockchaininfo()["headers"], self.nodes[1].getblockchaininfo()["blocks"])
86113

87114
self.log.info("Verify that invalidating an unknown block throws an error")
88115
assert_raises_rpc_error(-5, "Block not found", self.nodes[1].invalidateblock, "00" * 32)

0 commit comments

Comments
 (0)