From 4fad4e992c49a532e3a8928965f242cb311eeb29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Sun, 14 Sep 2025 14:01:27 -0700 Subject: [PATCH] test: add assumevalid scenarios scaffold Increase the test to 6 nodes and add flows for baseline, deep anchor, and too-recent cases, plus scaffolding for off-best-header, not-in-assumevalid, and reindex gates. Assertions are minimal here; follow-ups add reason checks. Co-authored-by: Hodlinator <172445034+hodlinator@users.noreply.github.com> --- test/functional/feature_assumevalid.py | 125 ++++++++++++++++++++----- 1 file changed, 100 insertions(+), 25 deletions(-) diff --git a/test/functional/feature_assumevalid.py b/test/functional/feature_assumevalid.py index 2cc4c754056..54c62e87528 100755 --- a/test/functional/feature_assumevalid.py +++ b/test/functional/feature_assumevalid.py @@ -18,7 +18,7 @@ We build a chain that includes an invalid signature for one of the transactions: 103-2202: bury the bad block with just over two weeks' worth of blocks (2100 blocks) -Start three nodes: +Start a few nodes: - node0 has no -assumevalid parameter. Try to sync to block 2202. It will reject block 102 and only sync as far as block 101 @@ -27,6 +27,12 @@ Start three nodes: - node2 has -assumevalid set to the hash of block 102. Try to sync to block 200. node2 will reject block 102 since it's assumed valid, but it isn't buried by at least two weeks' work. + - node3 has -assumevalid set to the hash of block 102. Feed a longer + competing headers-only branch so block #1 is not on the best header chain. + - node4 has -assumevalid set to the hash of block 102. Submit an alternative + block #1 that is not part of the assumevalid chain. + - node5 starts with no -assumevalid parameter. Reindex to hit + "assumevalid hash not in headers" and "below minimum chainwork". """ from test_framework.blocktools import ( @@ -63,11 +69,11 @@ class BaseNode(P2PInterface): class AssumeValidTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True - self.num_nodes = 3 + self.num_nodes = 6 self.rpc_timeout = 120 def setup_network(self): - self.add_nodes(3) + self.add_nodes(self.num_nodes) # Start node0. We don't start the other nodes yet since # we need to pre-mine a block with an invalid transaction # signature so we can pass in the block hash as assumevalid. @@ -136,40 +142,109 @@ class AssumeValidTest(BitcoinTestFramework): self.block_time += 1 height += 1 - # Start node1 and node2 with assumevalid so they accept a block with a bad signature. - self.start_node(1, extra_args=["-assumevalid=" + block102.hash_hex]) - self.start_node(2, extra_args=["-assumevalid=" + block102.hash_hex]) + self.start_node(1, extra_args=[f"-assumevalid={block102.hash_hex}"]) + self.start_node(2, extra_args=[f"-assumevalid={block102.hash_hex}"]) + self.start_node(3, extra_args=[f"-assumevalid={block102.hash_hex}"]) + self.start_node(4, extra_args=[f"-assumevalid={block102.hash_hex}"]) + self.start_node(5) - p2p0 = self.nodes[0].add_p2p_connection(BaseNode()) - p2p0.send_header_for_blocks(self.blocks[0:2000]) - p2p0.send_header_for_blocks(self.blocks[2000:]) + # nodes[0] # Send blocks to node0. Block 102 will be rejected. - self.send_blocks_until_disconnected(p2p0) - self.wait_until(lambda: self.nodes[0].getblockcount() >= COINBASE_MATURITY + 1) - assert_equal(self.nodes[0].getblockcount(), COINBASE_MATURITY + 1) - - p2p1 = self.nodes[1].add_p2p_connection(BaseNode()) - p2p1.send_header_for_blocks(self.blocks[0:2000]) - p2p1.send_header_for_blocks(self.blocks[2000:]) - with self.nodes[1].assert_debug_log(expected_msgs=[ - "Disabling script verification at block #1", - "Enabling script verification at block #103", + with self.nodes[0].assert_debug_log(expected_msgs=[ + "Block validation error: block-script-verify-flag-failed", ]): + p2p0 = self.nodes[0].add_p2p_connection(BaseNode()) + + p2p0.send_header_for_blocks(self.blocks[0:2000]) + p2p0.send_header_for_blocks(self.blocks[2000:]) + + self.send_blocks_until_disconnected(p2p0) + self.wait_until(lambda: self.nodes[0].getblockcount() >= COINBASE_MATURITY + 1) + assert_equal(self.nodes[0].getblockcount(), COINBASE_MATURITY + 1) + + + # nodes[1] + with self.nodes[1].assert_debug_log(expected_msgs=[ + f"Disabling script verification at block #1 ({self.blocks[0].hash_hex}).", + f"Enabling script verification at block #103 ({self.blocks[102].hash_hex}).", + ]): + p2p1 = self.nodes[1].add_p2p_connection(BaseNode()) + + p2p1.send_header_for_blocks(self.blocks[0:2000]) + p2p1.send_header_for_blocks(self.blocks[2000:]) # Send all blocks to node1. All blocks will be accepted. for i in range(2202): p2p1.send_without_ping(msg_block(self.blocks[i])) # Syncing 2200 blocks can take a while on slow systems. Give it plenty of time to sync. p2p1.sync_with_ping(timeout=960) - assert_equal(self.nodes[1].getblock(self.nodes[1].getbestblockhash())['height'], 2202) + assert_equal(self.nodes[1].getblock(self.nodes[1].getbestblockhash())['height'], 2202) - p2p2 = self.nodes[2].add_p2p_connection(BaseNode()) - p2p2.send_header_for_blocks(self.blocks[0:200]) + # nodes[2] # Send blocks to node2. Block 102 will be rejected. - self.send_blocks_until_disconnected(p2p2) - self.wait_until(lambda: self.nodes[2].getblockcount() >= COINBASE_MATURITY + 1) - assert_equal(self.nodes[2].getblockcount(), COINBASE_MATURITY + 1) + with self.nodes[2].assert_debug_log(expected_msgs=[ + "Block validation error: block-script-verify-flag-failed", + ]): + p2p2 = self.nodes[2].add_p2p_connection(BaseNode()) + p2p2.send_header_for_blocks(self.blocks[0:200]) + + self.send_blocks_until_disconnected(p2p2) + + self.wait_until(lambda: self.nodes[2].getblockcount() >= COINBASE_MATURITY + 1) + assert_equal(self.nodes[2].getblockcount(), COINBASE_MATURITY + 1) + + + # nodes[3] + with self.nodes[3].assert_debug_log(expected_msgs=[ + ]): + best_hash = self.nodes[3].getbestblockhash() + tip_block = self.nodes[3].getblock(best_hash) + second_chain_tip, second_chain_time, second_chain_height = int(best_hash, 16), tip_block["time"] + 1, tip_block["height"] + 1 + second_chain = [] + for _ in range(150): + block = create_block(second_chain_tip, create_coinbase(second_chain_height), second_chain_time) + block.solve() + second_chain.append(block) + second_chain_tip, second_chain_time, second_chain_height = block.hash_int, second_chain_time + 1, second_chain_height + 1 + + p2p3 = self.nodes[3].add_p2p_connection(BaseNode()) + + p2p3.send_header_for_blocks(second_chain) + p2p3.send_header_for_blocks(self.blocks[0:103]) + + p2p3.send_without_ping(msg_block(self.blocks[0])) + self.wait_until(lambda: self.nodes[3].getblockcount() == 1) + + + # nodes[4] + genesis_hash = self.nodes[4].getbestblockhash() + genesis_time = self.nodes[4].getblock(genesis_hash)['time'] + alt1 = create_block(int(genesis_hash, 16), create_coinbase(1), genesis_time + 2) + alt1.solve() + with self.nodes[4].assert_debug_log(expected_msgs=[ + ]): + p2p4 = self.nodes[4].add_p2p_connection(BaseNode()) + p2p4.send_header_for_blocks(self.blocks[0:103]) + + p2p4.send_without_ping(msg_block(alt1)) + self.wait_until(lambda: self.nodes[4].getblockcount() == 1) + + + # nodes[5] + # Reindex to hit specific assumevalid gates (no races with header downloads/chainwork during startup). + p2p5 = self.nodes[5].add_p2p_connection(BaseNode()) + p2p5.send_header_for_blocks(self.blocks[0:200]) + p2p5.send_without_ping(msg_block(self.blocks[0])) + self.wait_until(lambda: self.nodes[5].getblockcount() == 1) + with self.nodes[5].assert_debug_log(expected_msgs=[ + ]): + self.restart_node(5, extra_args=["-reindex-chainstate", "-assumevalid=1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"]) + assert_equal(self.nodes[5].getblockcount(), 1) + with self.nodes[5].assert_debug_log(expected_msgs=[ + ]): + self.restart_node(5, extra_args=["-reindex-chainstate", f"-assumevalid={block102.hash_hex}", "-minimumchainwork=0xffff"]) + assert_equal(self.nodes[5].getblockcount(), 1) if __name__ == '__main__':