-
Notifications
You must be signed in to change notification settings - Fork 37.7k
p2p: Respond to getheaders if we have sufficient chainwork #24178
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
p2p: Respond to getheaders if we have sufficient chainwork #24178
Conversation
For additional background on where this logic came from originally, please see: #5927 (comment), #6172, #21106 (comment) (as well as additional discussion in #21106 generally) |
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.
Concept/Approach ACK
A few suggestions for the test, feel free to pick/choose
diff --git a/test/functional/feature_minchainwork.py b/test/functional/feature_minchainwork.py
index d596464b27..637623706a 100755
--- a/test/functional/feature_minchainwork.py
+++ b/test/functional/feature_minchainwork.py
@@ -17,9 +17,9 @@ only succeeds past a given node once its nMinimumChainWork has been exceeded.
import time
+from test_framework.p2p import P2PInterface, msg_getheaders
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import assert_equal
-from test_framework.p2p import P2PInterface, msg_getheaders
# 2 hashes required per regtest block (with no difficulty adjustment)
REGTEST_WORK_PER_BLOCK = 2
@@ -43,7 +43,7 @@ class MinimumChainWorkTest(BitcoinTestFramework):
self.connect_nodes(i+1, i)
# Set clock of node2 2 days ahead, to keep it in IBD during this test.
- self.nodes[2].setmocktime(int(time.time())+48*60*60)
+ self.nodes[2].setmocktime(int(time.time()) + 48 * 60 * 60)
def run_test(self):
# Start building a chain on node0. node2 shouldn't be able to sync until node1's
@@ -75,13 +75,12 @@ class MinimumChainWorkTest(BitcoinTestFramework):
assert self.nodes[1].getbestblockhash() != self.nodes[0].getbestblockhash()
assert_equal(self.nodes[2].getblockcount(), starting_blockcount)
- # Check that getheaders requests to node2 are ignored.
+ self.log.info("Test that getheaders requests to node2 are ignored")
peer = self.nodes[2].add_p2p_connection(P2PInterface())
msg = msg_getheaders()
msg.locator.vHave = [int(self.nodes[2].getbestblockhash(), 16)]
msg.hashstop = 0
- peer.send_message(msg)
- peer.sync_with_ping()
+ peer.send_and_ping(msg)
time.sleep(5)
assert "headers" not in peer.last_message
@@ -100,13 +99,13 @@ class MinimumChainWorkTest(BitcoinTestFramework):
self.log.info(f"Blockcounts: {[n.getblockcount() for n in self.nodes]}")
- # Now getheaders requests should not be ignored
- peer.send_message(msg)
- peer.sync_with_ping()
+ self.log.info("Test that getheaders requests are not ignored")
+ peer.send_and_ping(msg)
assert "headers" in peer.last_message
# Verify that node2 is in fact still in IBD (otherwise this test may
# not be exercising the logic we want!)
- assert self.nodes[2].getblockchaininfo()['initialblockdownload']
+ assert_equal(self.nodes[2].getblockchaininfo()['initialblockdownload'], True)
+
if __name__ == '__main__':
MinimumChainWorkTest().main()
ea335c1
to
7ee8f57
Compare
Thanks @jonatack -- I've made the test cleanups you suggested. |
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. ConflictsReviewers, this pull request conflicts with the following ones:
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first. |
@sdaftuar So when two nodes differ in their checkpoint value, the attack could in theory still apply (being fed a bogus chain during IBD, which would then cause you to be banned by other nodes), but that's just too expensive if it requires the attacker to either branch off long before the current nMinimumChainWork value (effectively requiring the attacker to rewrite history), or shortly before the current nMinimumChainWork (but require recent-ish difficulty blocks on top)? |
I'm interpreting "differ in their checkpoint value" as meaning "have chains that disagree about blocks before the checkpoint value", instead of "have software that disagrees about which blockhashes are checkpointed", because in that latter case I think it should be clear that all bets are off for such peers remaining in consensus with each other. In the former case, which is what this logic is trying to protect against, then yes the point of the Maybe I should have reframed what this PR does a bit when I opened it. Right now, one of the criteria to leave IBD (so that
So what this PR is doing is not requiring the tip-age check to pass, and it's also not requiring that we're done reindexing or importing blocks from disk. Relaxing the tip-age check is intentional and I think a better behavior in the event that block creation stalled on the network for some reason. Now that I think about it, maybe I should re-add the guard about |
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.
Concept ACK
Now that I think about it, maybe I should re-add the guard about
fImporting || fReindex
so that we don't have peers asking to download blocks from us while we're still busy processing what we have on disk, but not sure how important that is.
When either of those is true
, Tip()
is always going to be at or behind what we've already imported / reindexed. So the nMinimumChainWork
check should be enough. There are two ways we decide with what headers to respond:
- If
locator
is null, look up thehashStop
block. That won't make it throughBlockRequestAllowed
if the block is outside the main chain an we haven't revalidated it (which we wouldn't with a reindex). - If
locator
is present, and has something in the main chain (FindForkInGlobalIndex
); already covered by thenMinimumChainWork
check
That said, it doesn't hurt to just guard that condition.
P.S. we should probably document the p2p messages in the source code so I don't have to google them, but here's a nice overview of how getheaders
is normally used: https://developer.bitcoin.org/devguide/p2p_network.html?highlight=getheaders#headers-first
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.
Concept ACK
If I understand this correctly, this means that also under normal circumstances, if we are in IBD (and beyond the point of nMinimumChainWork
), we could get inbound connections from peers that are also in IBD, and we would serve them headers and blocks, which would take away some bandwidth and could slow down our own IBD progress.
This doesn't seem like a probable scenario for new nodes that don't self-advertise their address until out of IBD, but a node that is well-known to the network and needs to do IBD again for some reason might want to disable incoming connections, especially on an older bitcoin core version with lots of blocks between nMinimumChainWork
and the current network's height.
Previously, we would check to see if we were in IBD and ignore getheaders requests accordingly. However, the IBD criteria -- an optimization mostly targeted at behavior when we have peers serving us many blocks we need to download -- is difficult to reason about in edge-case scenarios, such as if the network were to go a long time without any blocks found and nodes getting restarted during that time. To make things simpler to reason about, just use nMinimumChainWork as our anti-DoS threshold; as long as our chain has that much work, it should be fine to respond to a peer asking for our headers (and this should allow such a peer to request blocks from us if needed).
Expect responses to a getheaders iff the node has a chain with more than nMinimumChainWork
7ee8f57
to
a35f963
Compare
Agreed. I think this is not such a big concern, as other peers would only be downloading from us if they also were new nodes, and the likelihood of a new node coming online, learning our address from someone else and then doing IBD from us in the minutes/hours(?) that we may be finishing our IBD seems small. I imagine it's more likely the inbounds we get in that window are other nodes that have been online already anyway, and won't be completing IBD from us (but instead relaying us transactions). I updated this PR so that we won't respond to the getheaders if we're importing or reindexing, and I addressed @Sjors's comment about checking for nullptr as well. |
crACK a35f963 Verified that:
|
ACK a35f963 It is indeed easier to reason about IBD edge cases if we drop tip-age check (and also it just works better without the check). |
review ACK a35f963 🗯 Show signatureSignature:
|
Previously, we would check to see if we were in IBD and ignore getheaders requests accordingly. However, the IBD criteria -- an optimization mostly targeted at behavior when we have peers serving us many blocks we need to download -- is difficult to reason about in edge-case scenarios, such as if the network were to go a long time without any blocks found and nodes are getting restarted during that time.
To make things simpler to reason about, just use
nMinimumChainWork
as our anti-DoS threshold for responding to a getheaders request; as long as our chain has that much work, it should be fine to respond to a peer asking for our headers (and this should allow such a peer to request blocks from us if needed).