Merge bitcoin/bitcoin#34521: validation: fix UB in LoadChainTip

20ae9b98ea Extend functional test for setBlockIndexCandidates UB (marcofleon)
854a6d5a9a validation: fix UB in LoadChainTip (marcofleon)
9249e6089e validation: remove LoadChainTip call from ActivateSnapshot (marcofleon)

Pull request description:

  Addresses https://github.com/bitcoin/bitcoin/issues/34503. See this issue for more details as well.

  Fixes a bug where, under certain conditions, `setBlockIndexCandidates` had blocks in it that were worse than the tip. The block index candidate set uses `nSequenceId` as a sort key, so modifying this field while blocks are in the set results in undefined behavior. This PR populates `setBlockIndexCandidates` after the `nSequenceId` modifications, avoiding the UB.

ACKs for top commit:
  achow101:
    ACK 20ae9b98ea
  sedited:
    Re-ACK 20ae9b98ea
  sipa:
    Code review ACK 20ae9b98ea

Tree-SHA512: 121c170bb70fb6365089d578db63c811e7926e129d7206e569947f7a1f6c5ddc8d5f4937b80f1ba1b7d7daa42789b143ca5b56f154b7ab968a1cd55f925f378d
This commit is contained in:
Ava Chow
2026-03-06 08:22:42 -08:00
6 changed files with 76 additions and 43 deletions

View File

@@ -15,6 +15,9 @@ class ChainTiebreaksTest(BitcoinTestFramework):
self.num_nodes = 2
self.setup_clean_chain = True
def setup_network(self):
self.setup_nodes()
@staticmethod
def send_headers(node, blocks):
"""Submit headers for blocks to node."""
@@ -103,27 +106,29 @@ class ChainTiebreaksTest(BitcoinTestFramework):
node.invalidateblock(blocks[0].hash_hex)
def test_chain_split_from_disk(self):
node = self.nodes[0]
node = self.nodes[1]
peer = node.add_p2p_connection(P2PDataStore())
self.generate(node, 1, sync_fun=self.no_op)
self.log.info('Precomputing blocks')
#
# A1
# /
# G
# \
# A2
# /- A1
# /
# G -- B1 --- A2
# \
# \- A3
#
blocks = []
# Construct two blocks building from genesis.
# Construct three equal-work blocks building from the tip.
start_height = node.getblockcount()
genesis_block = node.getblock(node.getblockhash(start_height))
prev_time = genesis_block["time"]
tip_block = node.getblock(node.getbestblockhash())
prev_time = tip_block["time"]
for i in range(0, 2):
for i in range(0, 3):
blocks.append(create_block(
hashprev=int(genesis_block["hash"], 16),
hashprev=int(tip_block["hash"], 16),
tmpl={"height": start_height + 1,
# Make sure each block has a different hash.
"curtime": prev_time + i + 1,
@@ -131,16 +136,24 @@ class ChainTiebreaksTest(BitcoinTestFramework):
))
blocks[-1].solve()
# Send blocks and test the last one is not connected
self.log.info('Send A1 and A2. Make sure that only the former connects')
# Send blocks and test that only the first one connects
self.log.info('Send A1, A2, and A3. Make sure that only the former connects')
peer.send_blocks_and_test([blocks[0]], node, success=True)
peer.send_blocks_and_test([blocks[1]], node, success=False)
peer.send_blocks_and_test([blocks[2]], node, success=False)
self.log.info('Restart the node and check that the best tip before restarting matched the ones afterwards')
# Restart and check enough times for this to eventually fail if the logic is broken
for _ in range(10):
self.restart_node(0)
assert_equal(blocks[0].hash_hex, node.getbestblockhash())
# Restart and send a new block
self.restart_node(1)
assert_equal(blocks[0].hash_hex, node.getbestblockhash())
peer = node.add_p2p_connection(P2PDataStore())
next_block = create_block(
hashprev=blocks[0].hash_int,
tmpl={"height": start_height + 2,
"curtime": prev_time + 10,
}
)
next_block.solve()
peer.send_blocks_and_test([next_block], node, success=True)
def run_test(self):
self.test_chain_split_in_memory()