From 61e800e75cffa256ccdbc2ffc7a1739c00880ce0 Mon Sep 17 00:00:00 2001 From: stringintech Date: Wed, 18 Jun 2025 22:56:32 +0330 Subject: [PATCH] test: headers sync timeout --- test/functional/p2p_initial_headers_sync.py | 116 +++++++++++++++++--- 1 file changed, 101 insertions(+), 15 deletions(-) diff --git a/test/functional/p2p_initial_headers_sync.py b/test/functional/p2p_initial_headers_sync.py index 595a5202f56..24824f5a6bd 100755 --- a/test/functional/p2p_initial_headers_sync.py +++ b/test/functional/p2p_initial_headers_sync.py @@ -1,12 +1,15 @@ #!/usr/bin/env python3 -# Copyright (c) 2022 The Bitcoin Core developers +# Copyright (c) 2022-present The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -"""Test initial headers download +"""Test initial headers download and timeout behavior Test that we only try to initially sync headers from one peer (until our chain is close to caught up), and that each block announcement results in only one additional peer receiving a getheaders message. + +Also test peer timeout during initial headers sync, including normal peer +disconnection vs noban peer behavior. """ from test_framework.test_framework import BitcoinTestFramework @@ -23,7 +26,24 @@ from test_framework.p2p import ( from test_framework.util import ( assert_equal, ) +import math import random +import time + +# Constants from net_processing +HEADERS_DOWNLOAD_TIMEOUT_BASE_SEC = 15 * 60 +HEADERS_DOWNLOAD_TIMEOUT_PER_HEADER_MS = 1 +POW_TARGET_SPACING_SEC = 10 * 60 + + +def calculate_headers_timeout(best_header_time, current_time): + seconds_since_best_header = current_time - best_header_time + # Using ceil ensures the calculated timeout is >= actual timeout and not lower + # because of precision loss from conversion to seconds + variable_timeout_sec = math.ceil(HEADERS_DOWNLOAD_TIMEOUT_PER_HEADER_MS / 1_000 * + seconds_since_best_header / POW_TARGET_SPACING_SEC) + return int(current_time + HEADERS_DOWNLOAD_TIMEOUT_BASE_SEC + variable_timeout_sec) + class HeadersSyncTest(BitcoinTestFramework): def set_test_params(self): @@ -35,7 +55,20 @@ class HeadersSyncTest(BitcoinTestFramework): for p in peers: p.send_and_ping(new_block_announcement) - def run_test(self): + def assert_single_getheaders_recipient(self, peers): + count = 0 + receiving_peer = None + for p in peers: + with p2p_lock: + if "getheaders" in p.last_message: + count += 1 + receiving_peer = p + assert_equal(count, 1) + return receiving_peer + + def test_initial_headers_sync(self): + self.log.info("Test initial headers sync") + self.log.info("Adding a peer to node0") peer1 = self.nodes[0].add_p2p_connection(P2PInterface()) best_block_hash = int(self.nodes[0].getbestblockhash(), 16) @@ -69,16 +102,8 @@ class HeadersSyncTest(BitcoinTestFramework): peer1.send_without_ping(msg_headers()) # Send empty response, see above self.log.info("Check that exactly 1 of {peer2, peer3} received a getheaders in response") - count = 0 - peer_receiving_getheaders = None - for p in [peer2, peer3]: - with p2p_lock: - if "getheaders" in p.last_message: - count += 1 - peer_receiving_getheaders = p - p.send_without_ping(msg_headers()) # Send empty response, see above - - assert_equal(count, 1) + peer_receiving_getheaders = self.assert_single_getheaders_recipient([peer2, peer3]) + peer_receiving_getheaders.send_without_ping(msg_headers()) # Send empty response, see above self.log.info("Announce another new block, from all peers") self.announce_random_block(all_peers) @@ -93,8 +118,69 @@ class HeadersSyncTest(BitcoinTestFramework): expected_peer.wait_for_getheaders(block_hash=best_block_hash) - self.log.info("Success!") + def setup_timeout_test_peers(self): + self.log.info("Add peer1 and check it receives an initial getheaders request") + node = self.nodes[0] + with node.assert_debug_log(expected_msgs=["initial getheaders (0) to peer=0"]): + peer1 = node.add_p2p_connection(P2PInterface()) + peer1.wait_for_getheaders(block_hash=int(node.getbestblockhash(), 16)) + + self.log.info("Add outbound peer2") + # This peer has to be outbound otherwise the stalling peer is + # protected from disconnection + peer2 = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=1, connection_type="outbound-full-relay") + + assert_equal(node.num_test_p2p_connections(), 2) + return peer1, peer2 + + def trigger_headers_timeout(self): + self.log.info("Trigger the headers download timeout by advancing mock time") + # The node has not received any headers from peers yet + # So the best header time is the genesis block time + best_header_time = self.nodes[0].getblockchaininfo()["time"] + current_time = self.nodes[0].mocktime + timeout = calculate_headers_timeout(best_header_time, current_time) + # The calculated timeout above is always >= actual timeout, but we still need + # +1 to trigger the timeout when both values are equal + self.nodes[0].setmocktime(timeout + 1) + + def test_normal_peer_timeout(self): + self.log.info("Test peer disconnection on header timeout") + self.restart_node(0) + self.nodes[0].setmocktime(int(time.time())) + peer1, peer2 = self.setup_timeout_test_peers() + + with self.nodes[0].assert_debug_log(["Timeout downloading headers, disconnecting peer=0"]): + self.trigger_headers_timeout() + + self.log.info("Check that stalling peer1 is disconnected") + peer1.wait_for_disconnect() + assert_equal(self.nodes[0].num_test_p2p_connections(), 1) + + self.log.info("Check that peer2 receives a getheaders request") + peer2.wait_for_getheaders(block_hash=int(self.nodes[0].getbestblockhash(), 16)) + + def test_noban_peer_timeout(self): + self.log.info("Test noban peer on header timeout") + self.restart_node(0, extra_args=['-whitelist=noban@127.0.0.1']) + self.nodes[0].setmocktime(int(time.time())) + peer1, peer2 = self.setup_timeout_test_peers() + + with self.nodes[0].assert_debug_log(["Timeout downloading headers from noban peer, not disconnecting peer=0"]): + self.trigger_headers_timeout() + + self.log.info("Check that noban peer1 is not disconnected") + peer1.sync_with_ping() + assert_equal(self.nodes[0].num_test_p2p_connections(), 2) + + self.log.info("Check that exactly 1 of {peer1, peer2} receives a getheaders") + self.assert_single_getheaders_recipient([peer1, peer2]) + + def run_test(self): + self.test_initial_headers_sync() + self.test_normal_peer_timeout() + self.test_noban_peer_timeout() + if __name__ == '__main__': HeadersSyncTest(__file__).main() -