From 45f4ffcf9ce254e7501646961c2da0912fa3739f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Wed, 18 Feb 2026 17:34:21 +0100 Subject: [PATCH 1/5] test: Send rejection blocks explicitly Replace `send_blocks_until_disconnected` with explicit block sends up to the invalid block, followed by `wait_for_disconnect()`. This removes a race where Python can outpace validation and advance the test before the disconnect happens. --- test/functional/feature_assumevalid.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/test/functional/feature_assumevalid.py b/test/functional/feature_assumevalid.py index 2ee23c6eb7e9..0027cc458c21 100755 --- a/test/functional/feature_assumevalid.py +++ b/test/functional/feature_assumevalid.py @@ -79,17 +79,6 @@ def setup_network(self): # signature so we can pass in the block hash as assumevalid. self.start_node(0) - def send_blocks_until_disconnected(self, p2p_conn): - """Keep sending blocks to the node until we're disconnected.""" - for i in range(len(self.blocks)): - if not p2p_conn.is_connected: - break - try: - p2p_conn.send_without_ping(msg_block(self.blocks[i])) - except IOError: - assert not p2p_conn.is_connected - break - def run_test(self): # Build the blockchain self.tip = int(self.nodes[0].getbestblockhash(), 16) @@ -161,8 +150,9 @@ def run_test(self): 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) + for i in range(0, 103): + p2p0.send_without_ping(msg_block(self.blocks[i])) + p2p0.wait_for_disconnect() assert_equal(self.nodes[0].getblockcount(), COINBASE_MATURITY + 1) self.wait_until(lambda: next(filter(lambda x: x["hash"] == self.blocks[-1].hash_hex, self.nodes[0].getchaintips()))["status"] == "invalid") @@ -193,9 +183,9 @@ def run_test(self): 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) + for i in range(0, 103): + p2p2.send_without_ping(msg_block(self.blocks[i])) + p2p2.wait_for_disconnect() assert_equal(self.nodes[2].getblockcount(), COINBASE_MATURITY + 1) From 3059aed661bc2d9a3dbb34dd45ca574c42b406bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Wed, 18 Feb 2026 17:38:55 +0100 Subject: [PATCH 2/5] test: Add progress logs to assumevalid cases Replace inline comments with `self.log.info()` messages for each scenario. --- test/functional/feature_assumevalid.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/test/functional/feature_assumevalid.py b/test/functional/feature_assumevalid.py index 0027cc458c21..5e4e1fefc2fe 100755 --- a/test/functional/feature_assumevalid.py +++ b/test/functional/feature_assumevalid.py @@ -138,9 +138,8 @@ def run_test(self): self.start_node(4, extra_args=[f"-assumevalid={block102.hash_hex}"]) self.start_node(5) - # nodes[0] - # Send blocks to node0. Block 102 will be rejected. + self.log.info("Send blocks to node0. Block 102 will be rejected.") with self.nodes[0].assert_debug_log(expected_msgs=[ f"Enabling script verification at block #1 ({block_1_hash}): assumevalid=0 (always verify).", "Block validation error: block-script-verify-flag-failed", @@ -156,8 +155,8 @@ def run_test(self): assert_equal(self.nodes[0].getblockcount(), COINBASE_MATURITY + 1) self.wait_until(lambda: next(filter(lambda x: x["hash"] == self.blocks[-1].hash_hex, self.nodes[0].getchaintips()))["status"] == "invalid") - # nodes[1] + self.log.info("Send all blocks to node1. All blocks will be accepted.") 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}): block height above assumevalid height.", @@ -166,16 +165,14 @@ def run_test(self): 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) - # nodes[2] - # Send blocks to node2. Block 102 will be rejected. + self.log.info("Send blocks to node2. Block 102 will be rejected.") with self.nodes[2].assert_debug_log(expected_msgs=[ f"Enabling script verification at block #1 ({block_1_hash}): block too recent relative to best header.", "Block validation error: block-script-verify-flag-failed", @@ -188,8 +185,8 @@ def run_test(self): p2p2.wait_for_disconnect() assert_equal(self.nodes[2].getblockcount(), COINBASE_MATURITY + 1) - # nodes[3] + self.log.info("Send two header chains, and a block not in the best header chain to node3.") with self.nodes[3].assert_debug_log(expected_msgs=[ f"Enabling script verification at block #1 ({block_1_hash}): block not in best header chain.", ]): @@ -211,8 +208,8 @@ def run_test(self): p2p3.send_without_ping(msg_block(self.blocks[0])) self.wait_until(lambda: self.nodes[3].getblockcount() == 1) - # nodes[4] + self.log.info("Send a block not in the assumevalid header chain to node4.") 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) @@ -226,9 +223,8 @@ def run_test(self): 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). + self.log.info("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])) @@ -244,6 +240,5 @@ def run_test(self): 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__': AssumeValidTest(__file__).main() From 245e077c726e9db4dbe900158f04f154d74f7d01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Wed, 18 Feb 2026 17:40:43 +0100 Subject: [PATCH 3/5] test: Narrow `assert_debug_log` scopes Move p2p setup and other non-log-producing preparation outside `assert_debug_log` blocks. This keeps each assertion focused on the log-producing actions. --- test/functional/feature_assumevalid.py | 52 +++++++++++--------------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/test/functional/feature_assumevalid.py b/test/functional/feature_assumevalid.py index 5e4e1fefc2fe..242f845a8702 100755 --- a/test/functional/feature_assumevalid.py +++ b/test/functional/feature_assumevalid.py @@ -140,15 +140,13 @@ def run_test(self): # nodes[0] self.log.info("Send blocks to node0. Block 102 will be rejected.") + 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:]) with self.nodes[0].assert_debug_log(expected_msgs=[ f"Enabling script verification at block #1 ({block_1_hash}): assumevalid=0 (always verify).", "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:]) - for i in range(0, 103): p2p0.send_without_ping(msg_block(self.blocks[i])) p2p0.wait_for_disconnect() @@ -157,14 +155,13 @@ def run_test(self): # nodes[1] self.log.info("Send all blocks to node1. All blocks will be accepted.") + 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=[ f"Disabling script verification at block #1 ({self.blocks[0].hash_hex}).", f"Enabling script verification at block #103 ({self.blocks[102].hash_hex}): block height above assumevalid height.", ]): - 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:]) 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. @@ -173,13 +170,12 @@ def run_test(self): # nodes[2] self.log.info("Send blocks to node2. Block 102 will be rejected.") + p2p2 = self.nodes[2].add_p2p_connection(BaseNode()) + p2p2.send_header_for_blocks(self.blocks[0:200]) with self.nodes[2].assert_debug_log(expected_msgs=[ f"Enabling script verification at block #1 ({block_1_hash}): block too recent relative to best header.", "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]) - for i in range(0, 103): p2p2.send_without_ping(msg_block(self.blocks[i])) p2p2.wait_for_disconnect() @@ -187,24 +183,21 @@ def run_test(self): # nodes[3] self.log.info("Send two header chains, and a block not in the best header chain to node3.") + 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]) with self.nodes[3].assert_debug_log(expected_msgs=[ f"Enabling script verification at block #1 ({block_1_hash}): block not in best header chain.", ]): - 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) @@ -214,12 +207,11 @@ def run_test(self): genesis_time = self.nodes[4].getblock(genesis_hash)['time'] alt1 = create_block(int(genesis_hash, 16), create_coinbase(1), genesis_time + 2) alt1.solve() + p2p4 = self.nodes[4].add_p2p_connection(BaseNode()) + p2p4.send_header_for_blocks(self.blocks[0:103]) with self.nodes[4].assert_debug_log(expected_msgs=[ f"Enabling script verification at block #1 ({alt1.hash_hex}): block not in assumevalid chain.", ]): - 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) From 847b8d30bbee434e790b8d8317f14a9574cfc2c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Wed, 18 Feb 2026 17:41:28 +0100 Subject: [PATCH 4/5] test: Split first block rejection log checks For node0, node1, and node2, check the block-1 assumevalid log in a dedicated scope, then check later logs in a second scope to make assertions more precise and failure output shorter. --- test/functional/feature_assumevalid.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/test/functional/feature_assumevalid.py b/test/functional/feature_assumevalid.py index 242f845a8702..6a6093f020e5 100755 --- a/test/functional/feature_assumevalid.py +++ b/test/functional/feature_assumevalid.py @@ -145,9 +145,12 @@ def run_test(self): p2p0.send_header_for_blocks(self.blocks[2000:]) with self.nodes[0].assert_debug_log(expected_msgs=[ f"Enabling script verification at block #1 ({block_1_hash}): assumevalid=0 (always verify).", + ]): + p2p0.send_and_ping(msg_block(self.blocks[0])) + with self.nodes[0].assert_debug_log(expected_msgs=[ "Block validation error: block-script-verify-flag-failed", ]): - for i in range(0, 103): + for i in range(1, 103): p2p0.send_without_ping(msg_block(self.blocks[i])) p2p0.wait_for_disconnect() assert_equal(self.nodes[0].getblockcount(), COINBASE_MATURITY + 1) @@ -160,9 +163,12 @@ def run_test(self): p2p1.send_header_for_blocks(self.blocks[2000:]) with self.nodes[1].assert_debug_log(expected_msgs=[ f"Disabling script verification at block #1 ({self.blocks[0].hash_hex}).", + ]): + p2p1.send_and_ping(msg_block(self.blocks[0])) + with self.nodes[1].assert_debug_log(expected_msgs=[ f"Enabling script verification at block #103 ({self.blocks[102].hash_hex}): block height above assumevalid height.", ]): - for i in range(2202): + for i in range(1, 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) @@ -174,9 +180,12 @@ def run_test(self): p2p2.send_header_for_blocks(self.blocks[0:200]) with self.nodes[2].assert_debug_log(expected_msgs=[ f"Enabling script verification at block #1 ({block_1_hash}): block too recent relative to best header.", + ]): + p2p2.send_and_ping(msg_block(self.blocks[0])) + with self.nodes[2].assert_debug_log(expected_msgs=[ "Block validation error: block-script-verify-flag-failed", ]): - for i in range(0, 103): + for i in range(1, 103): p2p2.send_without_ping(msg_block(self.blocks[i])) p2p2.wait_for_disconnect() assert_equal(self.nodes[2].getblockcount(), COINBASE_MATURITY + 1) From de739aaa8a3d4ae9218cf808efe9de73c47b6087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C5=91rinc?= Date: Wed, 18 Feb 2026 17:42:42 +0100 Subject: [PATCH 5/5] test: Use deterministic sync in remaining checks Replace polling-based waits with explicit synchronization and direct asserts. Use `send_and_ping` for non-disconnect paths, add the missing node2 invalid chaintip assertion, and simplify node1 height checking. --- test/functional/feature_assumevalid.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/test/functional/feature_assumevalid.py b/test/functional/feature_assumevalid.py index 6a6093f020e5..27039d99c74f 100755 --- a/test/functional/feature_assumevalid.py +++ b/test/functional/feature_assumevalid.py @@ -154,7 +154,7 @@ def run_test(self): p2p0.send_without_ping(msg_block(self.blocks[i])) p2p0.wait_for_disconnect() assert_equal(self.nodes[0].getblockcount(), COINBASE_MATURITY + 1) - self.wait_until(lambda: next(filter(lambda x: x["hash"] == self.blocks[-1].hash_hex, self.nodes[0].getchaintips()))["status"] == "invalid") + assert_equal(next(filter(lambda x: x["hash"] == self.blocks[-1].hash_hex, self.nodes[0].getchaintips()))["status"], "invalid") # nodes[1] self.log.info("Send all blocks to node1. All blocks will be accepted.") @@ -172,7 +172,7 @@ def run_test(self): 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].getblockcount(), 2202) # nodes[2] self.log.info("Send blocks to node2. Block 102 will be rejected.") @@ -189,6 +189,7 @@ def run_test(self): p2p2.send_without_ping(msg_block(self.blocks[i])) p2p2.wait_for_disconnect() assert_equal(self.nodes[2].getblockcount(), COINBASE_MATURITY + 1) + assert_equal(next(filter(lambda x: x["hash"] == self.blocks[199].hash_hex, self.nodes[2].getchaintips()))["status"], "invalid") # nodes[3] self.log.info("Send two header chains, and a block not in the best header chain to node3.") @@ -207,8 +208,8 @@ def run_test(self): with self.nodes[3].assert_debug_log(expected_msgs=[ f"Enabling script verification at block #1 ({block_1_hash}): block not in best header chain.", ]): - p2p3.send_without_ping(msg_block(self.blocks[0])) - self.wait_until(lambda: self.nodes[3].getblockcount() == 1) + p2p3.send_and_ping(msg_block(self.blocks[0])) + assert_equal(self.nodes[3].getblockcount(), 1) # nodes[4] self.log.info("Send a block not in the assumevalid header chain to node4.") @@ -221,8 +222,8 @@ def run_test(self): with self.nodes[4].assert_debug_log(expected_msgs=[ f"Enabling script verification at block #1 ({alt1.hash_hex}): block not in assumevalid chain.", ]): - p2p4.send_without_ping(msg_block(alt1)) - self.wait_until(lambda: self.nodes[4].getblockcount() == 1) + p2p4.send_and_ping(msg_block(alt1)) + assert_equal(self.nodes[4].getblockcount(), 1) # nodes[5] self.log.info("Reindex to hit specific assumevalid gates (no races with header downloads/chainwork during startup).")