11#!/usr/bin/env python3
2- # Copyright (c) 2022 The Bitcoin Core developers
2+ # Copyright (c) 2022-present The Bitcoin Core developers
33# Distributed under the MIT software license, see the accompanying
44# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5- """Test initial headers download
5+ """Test initial headers download and timeout behavior
66
77Test that we only try to initially sync headers from one peer (until our chain
88is close to caught up), and that each block announcement results in only one
99additional peer receiving a getheaders message.
10+
11+ Also test peer timeout during initial headers sync, including normal peer
12+ disconnection vs noban peer behavior.
1013"""
1114
1215from test_framework .test_framework import BitcoinTestFramework
2326from test_framework .util import (
2427 assert_equal ,
2528)
29+ import math
2630import random
31+ import time
32+
33+ # Constants from net_processing
34+ HEADERS_DOWNLOAD_TIMEOUT_BASE_SEC = 15 * 60
35+ HEADERS_DOWNLOAD_TIMEOUT_PER_HEADER_MS = 1
36+ POW_TARGET_SPACING_SEC = 10 * 60
37+
38+
39+ def calculate_headers_timeout (best_header_time , current_time ):
40+ seconds_since_best_header = current_time - best_header_time
41+ # Using ceil ensures the calculated timeout is >= actual timeout and not lower
42+ # because of precision loss from conversion to seconds
43+ variable_timeout_sec = math .ceil (HEADERS_DOWNLOAD_TIMEOUT_PER_HEADER_MS / 1_000 *
44+ seconds_since_best_header / POW_TARGET_SPACING_SEC )
45+ return int (current_time + HEADERS_DOWNLOAD_TIMEOUT_BASE_SEC + variable_timeout_sec )
46+
2747
2848class HeadersSyncTest (BitcoinTestFramework ):
2949 def set_test_params (self ):
@@ -35,7 +55,20 @@ def announce_random_block(self, peers):
3555 for p in peers :
3656 p .send_and_ping (new_block_announcement )
3757
38- def run_test (self ):
58+ def assert_single_getheaders_recipient (self , peers ):
59+ count = 0
60+ receiving_peer = None
61+ for p in peers :
62+ with p2p_lock :
63+ if "getheaders" in p .last_message :
64+ count += 1
65+ receiving_peer = p
66+ assert_equal (count , 1 )
67+ return receiving_peer
68+
69+ def test_initial_headers_sync (self ):
70+ self .log .info ("Test initial headers sync" )
71+
3972 self .log .info ("Adding a peer to node0" )
4073 peer1 = self .nodes [0 ].add_p2p_connection (P2PInterface ())
4174 best_block_hash = int (self .nodes [0 ].getbestblockhash (), 16 )
@@ -69,16 +102,8 @@ def run_test(self):
69102 peer1 .send_without_ping (msg_headers ()) # Send empty response, see above
70103
71104 self .log .info ("Check that exactly 1 of {peer2, peer3} received a getheaders in response" )
72- count = 0
73- peer_receiving_getheaders = None
74- for p in [peer2 , peer3 ]:
75- with p2p_lock :
76- if "getheaders" in p .last_message :
77- count += 1
78- peer_receiving_getheaders = p
79- p .send_without_ping (msg_headers ()) # Send empty response, see above
80-
81- assert_equal (count , 1 )
105+ peer_receiving_getheaders = self .assert_single_getheaders_recipient ([peer2 , peer3 ])
106+ peer_receiving_getheaders .send_without_ping (msg_headers ()) # Send empty response, see above
82107
83108 self .log .info ("Announce another new block, from all peers" )
84109 self .announce_random_block (all_peers )
@@ -93,8 +118,69 @@ def run_test(self):
93118
94119 expected_peer .wait_for_getheaders (block_hash = best_block_hash )
95120
96- self .log .info ("Success!" )
121+ def setup_timeout_test_peers (self ):
122+ self .log .info ("Add peer1 and check it receives an initial getheaders request" )
123+ node = self .nodes [0 ]
124+ with node .assert_debug_log (expected_msgs = ["initial getheaders (0) to peer=0" ]):
125+ peer1 = node .add_p2p_connection (P2PInterface ())
126+ peer1 .wait_for_getheaders (block_hash = int (node .getbestblockhash (), 16 ))
127+
128+ self .log .info ("Add outbound peer2" )
129+ # This peer has to be outbound otherwise the stalling peer is
130+ # protected from disconnection
131+ peer2 = node .add_outbound_p2p_connection (P2PInterface (), p2p_idx = 1 , connection_type = "outbound-full-relay" )
132+
133+ assert_equal (node .num_test_p2p_connections (), 2 )
134+ return peer1 , peer2
135+
136+ def trigger_headers_timeout (self ):
137+ self .log .info ("Trigger the headers download timeout by advancing mock time" )
138+ # The node has not received any headers from peers yet
139+ # So the best header time is the genesis block time
140+ best_header_time = self .nodes [0 ].getblockchaininfo ()["time" ]
141+ current_time = self .nodes [0 ].mocktime
142+ timeout = calculate_headers_timeout (best_header_time , current_time )
143+ # The calculated timeout above is always >= actual timeout, but we still need
144+ # +1 to trigger the timeout when both values are equal
145+ self .nodes [0 ].setmocktime (timeout + 1 )
146+
147+ def test_normal_peer_timeout (self ):
148+ self .log .info ("Test peer disconnection on header timeout" )
149+ self .restart_node (0 )
150+ self .nodes [0 ].setmocktime (int (time .time ()))
151+ peer1 , peer2 = self .setup_timeout_test_peers ()
152+
153+ with self .nodes [0 ].assert_debug_log (["Timeout downloading headers, disconnecting peer=0" ]):
154+ self .trigger_headers_timeout ()
155+
156+ self .log .info ("Check that stalling peer1 is disconnected" )
157+ peer1 .wait_for_disconnect ()
158+ assert_equal (self .nodes [0 ].num_test_p2p_connections (), 1 )
159+
160+ self .log .info ("Check that peer2 receives a getheaders request" )
161+ peer2 .wait_for_getheaders (block_hash = int (self .nodes [0 ].getbestblockhash (), 16 ))
162+
163+ def test_noban_peer_timeout (self ):
164+ self .log .info ("Test noban peer on header timeout" )
165+ self .
restart_node (
0 ,
extra_args = [
'[email protected] ' ])
166+ self .nodes [0 ].setmocktime (int (time .time ()))
167+ peer1 , peer2 = self .setup_timeout_test_peers ()
168+
169+ with self .nodes [0 ].assert_debug_log (["Timeout downloading headers from noban peer, not disconnecting peer=0" ]):
170+ self .trigger_headers_timeout ()
171+
172+ self .log .info ("Check that noban peer1 is not disconnected" )
173+ peer1 .sync_with_ping ()
174+ assert_equal (self .nodes [0 ].num_test_p2p_connections (), 2 )
175+
176+ self .log .info ("Check that exactly 1 of {peer1, peer2} receives a getheaders" )
177+ self .assert_single_getheaders_recipient ([peer1 , peer2 ])
178+
179+ def run_test (self ):
180+ self .test_initial_headers_sync ()
181+ self .test_normal_peer_timeout ()
182+ self .test_noban_peer_timeout ()
183+
97184
98185if __name__ == '__main__' :
99186 HeadersSyncTest (__file__ ).main ()
100-
0 commit comments