Skip to content

Commit 22d96d7

Browse files
committed
Merge bitcoin#25720: p2p: Reduce bandwidth during initial headers sync when a block is found
f6a9166 Add functional test for block announcements during initial headers sync (Suhas Daftuar) 05f7f31 Reduce bandwidth during initial headers sync when a block is found (Suhas Daftuar) Pull request description: On startup, if our headers chain is more than a day behind current time, we'll pick one peer to sync headers with until our best headers chain is caught up (at that point, we'll try to sync headers with all peers). However, if an INV for a block is received before our headers chain is caught up, we'll then start to sync headers from each peer announcing the block. This can result in doing a big headers sync with many (if not all) of our peers simultaneously, which wastes bandwidth. This PR would reduce that overhead by picking (at most) one new peer to try syncing headers with whenever a new block is announced, prior to our headers chain being caught up. ACKs for top commit: LarryRuane: ACK f6a9166 ajtowns: ACK f6a9166 mzumsande: ACK f6a9166 dergoegge: Code review ACK f6a9166 achow101: ACK f6a9166 Tree-SHA512: 0662000bd68db146f55981de4adc2e2b07cbfda222b1176569d61c22055e5556752ffd648426f69687ed1cc203105515e7304c12b915d6270df8e41a4a0e1eaa
2 parents 6d4889a + f6a9166 commit 22d96d7

File tree

3 files changed

+139
-6
lines changed

3 files changed

+139
-6
lines changed

src/net_processing.cpp

+33-6
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 **/
@@ -682,6 +685,9 @@ class PeerManagerImpl final : public PeerManager
682685
/** Number of nodes with fSyncStarted. */
683686
int nSyncStarted GUARDED_BY(cs_main) = 0;
684687

688+
/** Hash of the last block we received via INV */
689+
uint256 m_last_block_inv_triggering_headers_sync{};
690+
685691
/**
686692
* Sources of received blocks, saved to be able punish them when processing
687693
* happens afterwards.
@@ -3240,8 +3246,9 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
32403246
UpdateBlockAvailability(pfrom.GetId(), inv.hash);
32413247
if (!fAlreadyHave && !fImporting && !fReindex && !IsBlockRequested(inv.hash)) {
32423248
// Headers-first is the primary method of announcement on
3243-
// the network. If a node fell back to sending blocks by inv,
3244-
// it's probably for a re-org. The final block hash
3249+
// the network. If a node fell back to sending blocks by
3250+
// inv, it may be for a re-org, or because we haven't
3251+
// completed initial headers sync. The final block hash
32453252
// provided should be the highest, so send a getheaders and
32463253
// then fetch the blocks we need to catch up.
32473254
best_block = &inv.hash;
@@ -3266,10 +3273,30 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
32663273
}
32673274

32683275
if (best_block != nullptr) {
3269-
if (MaybeSendGetHeaders(pfrom, m_chainman.ActiveChain().GetLocator(m_chainman.m_best_header), *peer)) {
3270-
LogPrint(BCLog::NET, "getheaders (%d) %s to peer=%d\n",
3271-
m_chainman.m_best_header->nHeight, best_block->ToString(),
3272-
pfrom.GetId());
3276+
// If we haven't started initial headers-sync with this peer, then
3277+
// consider sending a getheaders now. On initial startup, there's a
3278+
// reliability vs bandwidth tradeoff, where we are only trying to do
3279+
// initial headers sync with one peer at a time, with a long
3280+
// timeout (at which point, if the sync hasn't completed, we will
3281+
// disconnect the peer and then choose another). In the meantime,
3282+
// as new blocks are found, we are willing to add one new peer per
3283+
// block to sync with as well, to sync quicker in the case where
3284+
// our initial peer is unresponsive (but less bandwidth than we'd
3285+
// use if we turned on sync with all peers).
3286+
CNodeState& state{*Assert(State(pfrom.GetId()))};
3287+
if (state.fSyncStarted || (!peer->m_inv_triggered_getheaders_before_sync && *best_block != m_last_block_inv_triggering_headers_sync)) {
3288+
if (MaybeSendGetHeaders(pfrom, m_chainman.ActiveChain().GetLocator(m_chainman.m_best_header), *peer)) {
3289+
LogPrint(BCLog::NET, "getheaders (%d) %s to peer=%d\n",
3290+
m_chainman.m_best_header->nHeight, best_block->ToString(),
3291+
pfrom.GetId());
3292+
}
3293+
if (!state.fSyncStarted) {
3294+
peer->m_inv_triggered_getheaders_before_sync = true;
3295+
// Update the last block hash that triggered a new headers
3296+
// sync, so that we don't turn on headers sync with more
3297+
// than 1 new peer every new block.
3298+
m_last_block_inv_triggering_headers_sync = *best_block;
3299+
}
32733300
}
32743301
}
32753302

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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_headers,
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 announce_random_block(self, peers):
34+
new_block_announcement = msg_inv(inv=[CInv(MSG_BLOCK, random.randrange(1<<256))])
35+
for p in peers:
36+
p.send_and_ping(new_block_announcement)
37+
38+
def run_test(self):
39+
self.log.info("Adding a peer to node0")
40+
peer1 = self.nodes[0].add_p2p_connection(P2PInterface())
41+
42+
# Wait for peer1 to receive a getheaders
43+
peer1.wait_for_getheaders()
44+
# An empty reply will clear the outstanding getheaders request,
45+
# allowing additional getheaders requests to be sent to this peer in
46+
# the future.
47+
peer1.send_message(msg_headers())
48+
49+
self.log.info("Connecting two more peers to node0")
50+
# Connect 2 more peers; they should not receive a getheaders yet
51+
peer2 = self.nodes[0].add_p2p_connection(P2PInterface())
52+
peer3 = self.nodes[0].add_p2p_connection(P2PInterface())
53+
54+
all_peers = [peer1, peer2, peer3]
55+
56+
self.log.info("Verify that peer2 and peer3 don't receive a getheaders after connecting")
57+
for p in all_peers:
58+
p.sync_with_ping()
59+
with p2p_lock:
60+
assert "getheaders" not in peer2.last_message
61+
assert "getheaders" not in peer3.last_message
62+
63+
with p2p_lock:
64+
peer1.last_message.pop("getheaders", None)
65+
66+
self.log.info("Have all peers announce a new block")
67+
self.announce_random_block(all_peers)
68+
69+
self.log.info("Check that peer1 receives a getheaders in response")
70+
peer1.wait_for_getheaders()
71+
peer1.send_message(msg_headers()) # Send empty response, see above
72+
with p2p_lock:
73+
peer1.last_message.pop("getheaders", None)
74+
75+
self.log.info("Check that exactly 1 of {peer2, peer3} received a getheaders in response")
76+
count = 0
77+
peer_receiving_getheaders = None
78+
for p in [peer2, peer3]:
79+
with p2p_lock:
80+
if "getheaders" in p.last_message:
81+
count += 1
82+
peer_receiving_getheaders = p
83+
p.last_message.pop("getheaders", None)
84+
p.send_message(msg_headers()) # Send empty response, see above
85+
86+
assert_equal(count, 1)
87+
88+
self.log.info("Announce another new block, from all peers")
89+
self.announce_random_block(all_peers)
90+
91+
self.log.info("Check that peer1 receives a getheaders in response")
92+
peer1.wait_for_getheaders()
93+
94+
self.log.info("Check that the remaining peer received a getheaders as well")
95+
expected_peer = peer2
96+
if peer2 == peer_receiving_getheaders:
97+
expected_peer = peer3
98+
99+
expected_peer.wait_for_getheaders()
100+
101+
self.log.info("Success!")
102+
103+
if __name__ == '__main__':
104+
HeadersSyncTest().main()
105+

test/functional/test_runner.py

+1
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@
247247
'rpc_generate.py',
248248
'wallet_balance.py --legacy-wallet',
249249
'wallet_balance.py --descriptors',
250+
'p2p_initial_headers_sync.py',
250251
'feature_nulldummy.py',
251252
'mempool_accept.py',
252253
'mempool_expiry.py',

0 commit comments

Comments
 (0)