test: add assumevalid scenarios scaffold
What changed, and why it matters
This commit only adds new test cases to a Bitcoin Core functional test file. It expands the existing assumevalid test from 3 to 6 nodes and adds scaffolding for additional scenarios, but it does not change any production code. The assertions are intentionally minimal and described as follow-up work. There is no security fix or vulnerability being introduced here.
No security action required. This is a test-only change. Reviewers may want to verify the follow-up commits that add the actual reason checks referenced in the commit message.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff modifies test/functional/feature_assumevalid.py to increase num_nodes from 3 to 6 and adds test flows for node3 (longer competing headers-only branch), node4 (alternative block not in assumevalid chain), and node5 (reindex with assumevalid hash not in headers and below minimum chainwork). It wraps existing node0-2 assertions in assert_debug_log context managers and uses f-strings for extra_args. No consensus, networking, or validation logic in src/ is changed. The commit message explicitly calls this a scaffold with minimal assertions and follow-ups for reason checks.
Changed components
test/functional/feature_assumevalid.pyInspect captured patch +98 / −23
diff --git a/test/functional/feature_assumevalid.py b/test/functional/feature_assumevalid.py
index 2cc4c754..54c62e87 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)
+ 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)
- 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:])
+
+ # nodes[1]
with self.nodes[1].assert_debug_log(expected_msgs=[
- "Disabling script verification at block #1",
- "Enabling script verification at block #103",
+ 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__':
Why this scored 14/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.