-
Notifications
You must be signed in to change notification settings - Fork 38.7k
p2p: Reduce bandwidth during initial headers sync when a block is found #25720
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -369,6 +369,9 @@ struct Peer { | |
| /** Set of txids to reconsider once their parent transactions have been accepted **/ | ||
| std::set<uint256> m_orphan_work_set GUARDED_BY(g_cs_orphans); | ||
|
|
||
| /** Whether we've sent this peer a getheaders in response to an inv prior to initial-headers-sync completing */ | ||
| bool m_inv_triggered_getheaders_before_sync{false}; | ||
|
|
||
| /** Protects m_getdata_requests **/ | ||
| Mutex m_getdata_requests_mutex; | ||
| /** Work queue of items requested by this peer **/ | ||
|
|
@@ -681,6 +684,9 @@ class PeerManagerImpl final : public PeerManager | |
| /** Number of nodes with fSyncStarted. */ | ||
| int nSyncStarted GUARDED_BY(cs_main) = 0; | ||
|
|
||
| /** Hash of the last block we received via INV */ | ||
| uint256 m_last_block_inv_triggering_headers_sync{}; | ||
|
|
||
| /** | ||
| * Sources of received blocks, saved to be able punish them when processing | ||
| * happens afterwards. | ||
|
|
@@ -3239,8 +3245,9 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type, | |
| UpdateBlockAvailability(pfrom.GetId(), inv.hash); | ||
| if (!fAlreadyHave && !fImporting && !fReindex && !IsBlockRequested(inv.hash)) { | ||
| // Headers-first is the primary method of announcement on | ||
| // the network. If a node fell back to sending blocks by inv, | ||
| // it's probably for a re-org. The final block hash | ||
| // the network. If a node fell back to sending blocks by | ||
| // inv, it may be for a re-org, or because we haven't | ||
| // completed initial headers sync. The final block hash | ||
| // provided should be the highest, so send a getheaders and | ||
| // then fetch the blocks we need to catch up. | ||
| best_block = &inv.hash; | ||
|
|
@@ -3265,10 +3272,30 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type, | |
| } | ||
|
|
||
| if (best_block != nullptr) { | ||
| if (MaybeSendGetHeaders(pfrom, m_chainman.ActiveChain().GetLocator(m_chainman.m_best_header), *peer)) { | ||
| LogPrint(BCLog::NET, "getheaders (%d) %s to peer=%d\n", | ||
| m_chainman.m_best_header->nHeight, best_block->ToString(), | ||
| pfrom.GetId()); | ||
| // If we haven't started initial headers-sync with this peer, then | ||
| // consider sending a getheaders now. On initial startup, there's a | ||
| // reliability vs bandwidth tradeoff, where we are only trying to do | ||
| // initial headers sync with one peer at a time, with a long | ||
| // timeout (at which point, if the sync hasn't completed, we will | ||
| // disconnect the peer and then choose another). In the meantime, | ||
| // as new blocks are found, we are willing to add one new peer per | ||
| // block to sync with as well, to sync quicker in the case where | ||
| // our initial peer is unresponsive (but less bandwidth than we'd | ||
| // use if we turned on sync with all peers). | ||
|
||
| CNodeState& state{*Assert(State(pfrom.GetId()))}; | ||
| if (state.fSyncStarted || (!peer->m_inv_triggered_getheaders_before_sync && *best_block != m_last_block_inv_triggering_headers_sync)) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If there are two blocks found at the same time/height, this could still trigger a getheaders to most peers, no? (Someone sends block A, then someone else sends block B, then someone else sends A, etc) Might it be better to set
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's true, but I think your suggestion is less robust overall than what I have proposed here: sending an INV to a node is free (there is no proof-of-work attached to a block hash), so an adversary could take advantage of your proposed strategy by continually connecting, sending an INV, and disconnecting, to prevent a node from trying to sync with any of its honest peers that are announcing main-chain blocks. On the other hand, I just checked one of my long-running, well-connected nodes, and it's seen about 10 stale blocks in the last 50000. So that seems like pretty good odds that a node starting up is unlikely to run into this? |
||
| if (MaybeSendGetHeaders(pfrom, m_chainman.ActiveChain().GetLocator(m_chainman.m_best_header), *peer)) { | ||
| LogPrint(BCLog::NET, "getheaders (%d) %s to peer=%d\n", | ||
| m_chainman.m_best_header->nHeight, best_block->ToString(), | ||
| pfrom.GetId()); | ||
| } | ||
| if (!state.fSyncStarted) { | ||
| peer->m_inv_triggered_getheaders_before_sync = true; | ||
| // Update the last block hash that triggered a new headers | ||
| // sync, so that we don't turn on headers sync with more | ||
| // than 1 new peer every new block. | ||
| m_last_block_inv_triggering_headers_sync = *best_block; | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,105 @@ | ||||||||||||
| #!/usr/bin/env python3 | ||||||||||||
| # Copyright (c) 2022 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 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. | ||||||||||||
| """ | ||||||||||||
|
|
||||||||||||
| from test_framework.test_framework import BitcoinTestFramework | ||||||||||||
| from test_framework.messages import ( | ||||||||||||
| CInv, | ||||||||||||
| MSG_BLOCK, | ||||||||||||
| msg_headers, | ||||||||||||
| msg_inv, | ||||||||||||
| ) | ||||||||||||
| from test_framework.p2p import ( | ||||||||||||
| p2p_lock, | ||||||||||||
| P2PInterface, | ||||||||||||
| ) | ||||||||||||
| from test_framework.util import ( | ||||||||||||
| assert_equal, | ||||||||||||
| ) | ||||||||||||
| import random | ||||||||||||
|
|
||||||||||||
| class HeadersSyncTest(BitcoinTestFramework): | ||||||||||||
| def set_test_params(self): | ||||||||||||
| self.setup_clean_chain = True | ||||||||||||
| self.num_nodes = 1 | ||||||||||||
|
|
||||||||||||
| def announce_random_block(self, peers): | ||||||||||||
| new_block_announcement = msg_inv(inv=[CInv(MSG_BLOCK, random.randrange(1<<256))]) | ||||||||||||
| for p in peers: | ||||||||||||
| p.send_and_ping(new_block_announcement) | ||||||||||||
|
|
||||||||||||
| def run_test(self): | ||||||||||||
|
||||||||||||
| def run_test(self): | |
| def run_test(self): | |
| node = self.nodes[0] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leaving this as-is; I think the self.num_nodes = 1 in set_test_params makes it clear that only one node is involved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't need to happen in this PR but in the future this test could also cover our preference for outbound peers for the initial headers sync.
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| for p in [peer2, peer3]: | |
| for p in [peer2, peer3]: | |
| p.sync_with_ping() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if I'm missing something; I believe this sync_with_ping() is unnecessary because we use send_and_ping() in announce_random_block(), does that sound right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, ignore my suggestion.
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| expected_peer.wait_for_getheaders() | |
| expected_peer.wait_for_getheaders() | |
| peer_receiving_headers.sync_with_ping() | |
| with p2p_lock: | |
| assert "getheaders" not in peer_receiving_headers.last_message |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I intentionally did not include this test (that a peer that we've started sync with doesn't receive a subsequent getheaders in response to an INV) because I felt like that was overspecifying what I think of as reasonable behavior. I think the most important property is that we add a new peer with each new block, but whether we continue trying to sync with existing peers is (in my view) up for debate.
For instance, if the test were rewritten a bit so that the peer receiving the initial getheaders in response to an INV had a big long headers chain to serve the node, then you'd expect there to be a getheaders in flight as headers sync proceeds. Granted that is not the situation in this test right now, but I feel like it's helpful for future test maintenance to not overspecify behavior if it doesn't really matter for what we're trying to achieve.
Uh oh!
There was an error while loading. Please reload this page.