Skip to content

Commit c92b0f5

Browse files
kwvgPastaPastaPasta
authored andcommitted
merge bitcoin#25720: Reduce bandwidth during initial headers sync when a block is found
1 parent 0f9ece0 commit c92b0f5

File tree

3 files changed

+154
-7
lines changed

3 files changed

+154
-7
lines changed

src/net_processing.cpp

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,9 @@ struct Peer {
370370
/** Set of txids to reconsider once their parent transactions have been accepted **/
371371
std::set<uint256> m_orphan_work_set GUARDED_BY(g_cs_orphans);
372372

373+
/** Whether we've sent this peer a getheaders in response to an inv prior to initial-headers-sync completing */
374+
bool m_inv_triggered_getheaders_before_sync{false};
375+
373376
/** Protects m_getdata_requests **/
374377
Mutex m_getdata_requests_mutex;
375378
/** Work queue of items requested by this peer **/
@@ -664,6 +667,9 @@ class PeerManagerImpl final : public PeerManager
664667
/** Number of nodes with fSyncStarted. */
665668
int nSyncStarted GUARDED_BY(cs_main) = 0;
666669

670+
/** Hash of the last block we received via INV */
671+
uint256 m_last_block_inv_triggering_headers_sync{};
672+
667673
/**
668674
* Sources of received blocks, saved to be able punish them when processing
669675
* happens afterwards.
@@ -3939,8 +3945,9 @@ void PeerManagerImpl::ProcessMessage(
39393945
UpdateBlockAvailability(pfrom.GetId(), inv.hash);
39403946
if (!fAlreadyHave && !fImporting && !fReindex && !mapBlocksInFlight.count(inv.hash)) {
39413947
// Headers-first is the primary method of announcement on
3942-
// the network. If a node fell back to sending blocks by inv,
3943-
// it's probably for a re-org. The final block hash
3948+
// the network. If a node fell back to sending blocks by
3949+
// inv, it may be for a re-org, or because we haven't
3950+
// completed initial headers sync. The final block hash
39443951
// provided should be the highest, so send a getheaders and
39453952
// then fetch the blocks we need to catch up.
39463953
best_block = &inv.hash;
@@ -3979,11 +3986,31 @@ void PeerManagerImpl::ProcessMessage(
39793986
}
39803987
}
39813988
if (best_block != nullptr) {
3982-
std::string msg_type = UsesCompressedHeaders(*peer) ? NetMsgType::GETHEADERS2 : NetMsgType::GETHEADERS;
3983-
if (MaybeSendGetHeaders(pfrom, msg_type, m_chainman.ActiveChain().GetLocator(m_chainman.m_best_header), *peer)) {
3984-
LogPrint(BCLog::NET, "%s (%d) %s to peer=%d\n",
3985-
msg_type, m_chainman.m_best_header->nHeight, best_block->ToString(),
3986-
pfrom.GetId());
3989+
// If we haven't started initial headers-sync with this peer, then
3990+
// consider sending a getheaders now. On initial startup, there's a
3991+
// reliability vs bandwidth tradeoff, where we are only trying to do
3992+
// initial headers sync with one peer at a time, with a long
3993+
// timeout (at which point, if the sync hasn't completed, we will
3994+
// disconnect the peer and then choose another). In the meantime,
3995+
// as new blocks are found, we are willing to add one new peer per
3996+
// block to sync with as well, to sync quicker in the case where
3997+
// our initial peer is unresponsive (but less bandwidth than we'd
3998+
// use if we turned on sync with all peers).
3999+
CNodeState& state{*Assert(State(pfrom.GetId()))};
4000+
if (state.fSyncStarted || (!peer->m_inv_triggered_getheaders_before_sync && *best_block != m_last_block_inv_triggering_headers_sync)) {
4001+
std::string msg_type = UsesCompressedHeaders(*peer) ? NetMsgType::GETHEADERS2 : NetMsgType::GETHEADERS;
4002+
if (MaybeSendGetHeaders(pfrom, msg_type, m_chainman.ActiveChain().GetLocator(m_chainman.m_best_header), *peer)) {
4003+
LogPrint(BCLog::NET, "%s (%d) %s to peer=%d\n",
4004+
msg_type, m_chainman.m_best_header->nHeight, best_block->ToString(),
4005+
pfrom.GetId());
4006+
}
4007+
if (!state.fSyncStarted) {
4008+
peer->m_inv_triggered_getheaders_before_sync = true;
4009+
// Update the last block hash that triggered a new headers
4010+
// sync, so that we don't turn on headers sync with more
4011+
// than 1 new peer every new block.
4012+
m_last_block_inv_triggering_headers_sync = *best_block;
4013+
}
39874014
}
39884015
}
39894016

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2022 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
"""Test initial headers download
6+
7+
Test that we only try to initially sync headers from one peer (until our chain
8+
is close to caught up), and that each block announcement results in only one
9+
additional peer receiving a getheaders message.
10+
"""
11+
12+
from test_framework.test_framework import BitcoinTestFramework
13+
from test_framework.messages import (
14+
CInv,
15+
MSG_BLOCK,
16+
msg_headers2,
17+
msg_inv,
18+
)
19+
from test_framework.p2p import (
20+
p2p_lock,
21+
P2PInterface,
22+
)
23+
from test_framework.util import (
24+
assert_equal,
25+
)
26+
import random
27+
28+
class HeadersSyncTest(BitcoinTestFramework):
29+
def set_test_params(self):
30+
self.setup_clean_chain = True
31+
self.num_nodes = 1
32+
33+
def setup_chain(self):
34+
# This test operates under the assumption that the adjusted time is well ahead of block
35+
# time.
36+
#
37+
# By default when we setup a new chain, we also adjust the mocktime (this is not done in
38+
# Bitcoin's test suite), which violates this test's assumption and causes it to fail. We
39+
# remedy this by ensuring the test's assumptions are met (i.e. we don't adjust mocktime)
40+
#
41+
self.log.info("Initializing test directory " + self.options.tmpdir)
42+
if self.setup_clean_chain:
43+
self._initialize_chain_clean()
44+
else:
45+
self._initialize_chain()
46+
47+
def announce_random_block(self, peers):
48+
new_block_announcement = msg_inv(inv=[CInv(MSG_BLOCK, random.randrange(1<<256))])
49+
for p in peers:
50+
p.send_and_ping(new_block_announcement)
51+
52+
def run_test(self):
53+
self.log.info("Adding a peer to node0")
54+
peer1 = self.nodes[0].add_p2p_connection(P2PInterface())
55+
56+
# Wait for peer1 to receive a getheaders
57+
peer1.wait_for_getheaders()
58+
# An empty reply will clear the outstanding getheaders request,
59+
# allowing additional getheaders requests to be sent to this peer in
60+
# the future.
61+
peer1.send_message(msg_headers2())
62+
63+
self.log.info("Connecting two more peers to node0")
64+
# Connect 2 more peers; they should not receive a getheaders yet
65+
peer2 = self.nodes[0].add_p2p_connection(P2PInterface())
66+
peer3 = self.nodes[0].add_p2p_connection(P2PInterface())
67+
68+
all_peers = [peer1, peer2, peer3]
69+
70+
self.log.info("Verify that peer2 and peer3 don't receive a getheaders after connecting")
71+
for p in all_peers:
72+
p.sync_with_ping()
73+
with p2p_lock:
74+
assert "getheaders2" not in peer2.last_message
75+
assert "getheaders2" not in peer3.last_message
76+
77+
with p2p_lock:
78+
peer1.last_message.pop("getheaders2", None)
79+
80+
self.log.info("Have all peers announce a new block")
81+
self.announce_random_block(all_peers)
82+
83+
self.log.info("Check that peer1 receives a getheaders in response")
84+
peer1.wait_for_getheaders()
85+
peer1.send_message(msg_headers2()) # Send empty response, see above
86+
with p2p_lock:
87+
peer1.last_message.pop("getheaders2", None)
88+
89+
self.log.info("Check that exactly 1 of {peer2, peer3} received a getheaders in response")
90+
count = 0
91+
peer_receiving_getheaders = None
92+
for p in [peer2, peer3]:
93+
with p2p_lock:
94+
if "getheaders2" in p.last_message:
95+
count += 1
96+
peer_receiving_getheaders = p
97+
p.last_message.pop("getheaders2", None)
98+
p.send_message(msg_headers2()) # Send empty response, see above
99+
100+
assert_equal(count, 1)
101+
102+
self.log.info("Announce another new block, from all peers")
103+
self.announce_random_block(all_peers)
104+
105+
self.log.info("Check that peer1 receives a getheaders in response")
106+
peer1.wait_for_getheaders()
107+
108+
self.log.info("Check that the remaining peer received a getheaders as well")
109+
expected_peer = peer2
110+
if peer2 == peer_receiving_getheaders:
111+
expected_peer = peer3
112+
113+
expected_peer.wait_for_getheaders()
114+
115+
self.log.info("Success!")
116+
117+
if __name__ == '__main__':
118+
HeadersSyncTest().main()
119+

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@
255255
'rpc_generate.py',
256256
'wallet_balance.py --legacy-wallet',
257257
'wallet_balance.py --descriptors',
258+
'p2p_initial_headers_sync.py',
258259
'feature_nulldummy.py --legacy-wallet',
259260
'feature_nulldummy.py --descriptors',
260261
'mempool_accept.py',

0 commit comments

Comments
 (0)