Skip to content

Commit 5d17e64

Browse files
committed
Merge bitcoin/bitcoin#32677: test: headers sync timeout
61e800e test: headers sync timeout (stringintech) Pull request description: When reviewing PR #32051 and considering which functional tests might need to be adapted/extended accordingly, I noticed there appears to be limited functional test coverage for header sync timeouts and thought it might be helpful to add one. This test attempts to cover two scenarios: 1. **Normal peer timeout behavior:** When a peer fails to respond to initial getheaders requests within the timeout period, it should be disconnected and the node should attempt to sync headers from the next available peer. 2. **Noban peer behavior:** When a peer with noban privileges times out, it should remain connected while the node still attempts to sync headers from other peers. ACKs for top commit: maflcko: re-ACK 61e800e 🗝 stratospher: reACK 61e800e. Tree-SHA512: b8a867e7986b6f0aa00d81a84b205f2bf8fb2e6047a2e37272e0244229d1f43020e9031467827dabbfe7849a91429f2685e00a25356e2ed477fa1a035fa0b1fd
2 parents 0087ba4 + 61e800e commit 5d17e64

File tree

1 file changed

+101
-15
lines changed

1 file changed

+101
-15
lines changed

test/functional/p2p_initial_headers_sync.py

Lines changed: 101 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
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
77
Test that we only try to initially sync headers from one peer (until our chain
88
is close to caught up), and that each block announcement results in only one
99
additional 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

1215
from test_framework.test_framework import BitcoinTestFramework
@@ -23,7 +26,24 @@
2326
from test_framework.util import (
2427
assert_equal,
2528
)
29+
import math
2630
import 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

2848
class 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=['-whitelist=noban@127.0.0.1'])
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

98185
if __name__ == '__main__':
99186
HeadersSyncTest(__file__).main()
100-

0 commit comments

Comments
 (0)