-
Notifications
You must be signed in to change notification settings - Fork 38.7k
p2p: Drop unsolicited CMPCTBLOCK from non-HB peer #32606
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
base: master
Are you sure you want to change the base?
p2p: Drop unsolicited CMPCTBLOCK from non-HB peer #32606
Conversation
|
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. Code Coverage & BenchmarksFor details see: https://corecheck.dev/bitcoin/bitcoin/pulls/32606. ReviewsSee the guideline for information on the review process.
If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update. 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. |
|
Concept ACK. BIP152 is marked as Final but perhaps could be clarified on this point. |
|
Concept ACK. LLM linter's punctuation advice seems right to me. :) |
mzumsande
left a comment
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
In the commit message:
"making it only possible for peers selected as high bandwidth to discover a node's
mempool" is a bit too strong, since mempools are discoverable, just not transactions that were added to them after the last inv to the peer.
28086e7 to
1fc95e0
Compare
|
Rebased to address @ajtowns and @mzumsande feedback. |
w0xlt
left a comment
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.
ACK 1fc95e0
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.
My main concern here is if this disables a FIBRE future; FIBRE apparently used unsolicited compact blocks (see: #28272 (comment)). If we are not concerned about that, then Concept ACK.
| @@ -4435,6 +4435,11 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type, | |||
| range_flight.first++; | |||
| } | |||
|
|
|||
| if (!requested_block_from_this_peer && !pfrom.m_bip152_highbandwidth_to) { | |||
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.
I think this check is still permissive enough such that a NetMsgType::CMPCTBLOCK message can be sent to a node in -blocksonly mode and still get processed. This would require the -blocksonly node to request the block from the peer.
Maybe something like this instead?
+ if ((!requested_block_from_this_peer && !pfrom.m_bip152_highbandwidth_to) || m_opts.ignore_incoming_txs) {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.
Agreed - even if we are in blocks-only mode we might have requested_block_from_this_peer, but we won't be able to reconstruct a block from our barren mempool unless the block is empty (or only contains transactions we originated).
One could special-case it to account for allowing empty block reconstruction on blocks-only nodes but it feels like dead weight in the code.
In case C of the diagram at https://github.com/bitcoin/bips/blob/master/bip-0152.mediawiki#intended-protocol-flow, we might have explicitly asked for a compact block from a low bandwidth peer. But we appear to not be doing so in blocks-only mode, which makes sense:
https://github.com/bitcoin/bitcoin/blob/6e1a49b66e77c2e6d158c51f51a19ec43cd74707/src/net_processing.cpp#L2780-L2788
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.
One could special-case it to account for allowing empty block reconstruction on blocks-only nodes but it feels like dead weight in the code.
Yeah, I feel like the complexity isn't worth it.
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.
I think this check is still permissive enough such that a
NetMsgType::CMPCTBLOCKmessage can be sent to a node in -blocksonly mode and still get processed. T
Should we only send SENDCMPCT hb=false after VERACK if we're not blocksonly? Making all the compact block stuff conditional on not being -blocksonly=1 would make sense to me, but probably isn't needed for this PR.
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.
Should we only send SENDCMPCT hb=false after VERACK if we're not blocksonly?
I think this breaks -blocksonly nodes being able to send compact blocks since m_provides_cmpctblocks is never set (a peer won't set them as HB)? I can PR the -blocksonly change I suggested above separately if it's tangential.
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.
Future improvements to fully "lock down" this code path could be to:
- check
m_opts.ignore_incoming_txs - check
m_provides_cmpctblocksis set - check that if the block was requested, we used
MSG_CMPCT_BLOCK(requires state)
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.
What if in the case of being in -blocksonly mode and receive a CMCTBLOCK message we instantly respond with a GETBLOCKTXN asking for all transactions (skipping all InitData logic)?
diff --git a/src/net_processing.cpp b/src/net_processing.cpp
index dad2c4ce6d..d46c9b003b 100644
--- a/src/net_processing.cpp
+++ b/src/net_processing.cpp
@@ -4413,6 +4413,15 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
}
if (!requested_block_from_this_peer && !pfrom.m_bip152_highbandwidth_to) {
+ if (m_opts.ignore_incoming_txs) {
+ BlockTransactionsRequest req;
+ for (size_t i = 0; i < cmpctblock.BlockTxCount(); i++) {
+ req.indexes.push_back(i);
+ }
+ req.blockhash = pindex->GetBlockHash();
+ MakeAndPushMessage(pfrom, NetMsgType::GETBLOCKTXN, vInv);
+ return;
+ }
LogDebug(BCLog::CMPCTBLOCK, "Peer %d%s, not marked as high-bandwidth, sent us an unsolicited compact block!", pfrom.GetId(), pfrom.LogIP(fLogIPs));
return;
}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.
It doesn't really ever make sense to process a CMPCTBLOCK message as blocks-only, the peer should just send the block, if we want to do anything with the message as a blocks-only, we should just process the header of the CMPCTBLOCK:
fRevertToHeaderProcessing = true;
This will call ProcessHeadersMessage() and that can request the block.
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.
Agree that it doesn't make sense to process, we should drop it asap. Haven't tested either of the below things:
ProcessHeadersMessage will trigger a GETDATA and if the block turns out to be invalid, our peer who sent us the CMPCTBLOCK won't respond and will get disconnected for stalling. If our peer is modified to relay these in the first place to all peers, they could isolate themselves from -blocksonly nodes (assuming they aren't a mining node and don't validate what they send out). This is #13370.
A tangent, but I think a miner could trigger this statement in honest peer's connections if they mined and withheld enough blocks (~4-5) and if the last CMPCTBLOCK is for an invalid block. I'm leaving out a few steps and can write out what I think on the linked issue if it's worth addressing?
|
Yup I think this means the defense is ineffective.
This is interesting, but in this case I would still consider the compact block as "requested" as the peer has updated its internal state to request the compact block. The fibre patchset is being revived and while I haven't tested it so I don't know if it breaks fibre, I would rather be cautious and only include the defense for -blocksonly nodes. |
The fact that you're enabling FIBRE on a connection can just be taken as implicitly soliciting compact blocks on that connection, so I don't think this is a problem. |
hodlinator
left a comment
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 1fc95e080040a9d038a8291eeecf645e0413a5c8
| @@ -4435,6 +4435,11 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type, | |||
| range_flight.first++; | |||
| } | |||
|
|
|||
| if (!requested_block_from_this_peer && !pfrom.m_bip152_highbandwidth_to) { | |||
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.
Agreed - even if we are in blocks-only mode we might have requested_block_from_this_peer, but we won't be able to reconstruct a block from our barren mempool unless the block is empty (or only contains transactions we originated).
One could special-case it to account for allowing empty block reconstruction on blocks-only nodes but it feels like dead weight in the code.
In case C of the diagram at https://github.com/bitcoin/bips/blob/master/bip-0152.mediawiki#intended-protocol-flow, we might have explicitly asked for a compact block from a low bandwidth peer. But we appear to not be doing so in blocks-only mode, which makes sense:
https://github.com/bitcoin/bitcoin/blob/6e1a49b66e77c2e6d158c51f51a19ec43cd74707/src/net_processing.cpp#L2780-L2788
Right, my earlier comment was wrong. I think that dropping unsolicited cmpctblock in the non -blocksonly case is kind of ineffective as pointed out by others. But I also don't have a strong opinion about it given that it doesn't affect FIBRE. |
test/functional/p2p_compactblocks.py
Outdated
| self.generate(self.nodes[0], COINBASE_MATURITY + 1) | ||
| block = self.build_block_on_tip(self.nodes[0]) | ||
|
|
||
| self.segwit_node.send_header_for_blocks([block]) | ||
| self.segwit_node.wait_for_getdata([block.sha256], timeout=30) |
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.
AttributeError: 'CBlock' object has no attribute 'sha256'
1fc95e0 to
5caaefd
Compare
|
Force-pushed to address merge conflict and @hodlinator's feedback to log IP's. |
|
concept ACK |
|
Concept ACK here too; haven't looked at the code yet |
|
one race condition we might hit is a compact block being sent to our node before that peer receives the message un-selecting them as a hb peer. |
In this case we probably are OK to drop, even though it wastes a little bandwidth, and for the time that our We'll only unselect a peer as high bandwidth when selecting another which only happens when connecting a new block. It takes ~half a round-trip for our peer to find out they've gotten the boot, so this condition only occurs when:
If 1/2 RTT = 50ms, on average 0.008% ( |
hodlinator
left a comment
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.
Reviewed 5caaefd8d4174cfc27d5538e7f2d3a1c2bdb77d8
| @@ -4412,6 +4412,11 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type, | |||
| range_flight.first++; | |||
| } | |||
|
|
|||
| if (!requested_block_from_this_peer && !pfrom.m_bip152_highbandwidth_to) { | |||
| LogDebug(BCLog::NET, "Peer %d, not marked as high-bandwidth, sent us an unsolicited compact block!\n", pfrom.GetId()); | |||
| return; | |||
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.
remark: Wondered whether it would be fair to mark as Misbehaving() or disconnect them, but there's a tiny chance they may have recently been an HB peer (as remarked in #32606 (comment)).
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.
+1 to the way @instagibbs pointed out for honest peers sending unsolicited CMPCTBLOCK's making this a bad idea. And if we did this, any way now or in the future that you could trick a node into sending an unsolicited CMPCTBLOCK becomes an attack vector. Given that there is little an attacker could accomplish by sending us an unsolicited compact block if we drop unsolicited compact blocks on the ground, I think we don't gain much by marking them as misbehaving or disconnecting them.
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.
Haven't tested. I think this will cause extra log messages if:
- receive
HEADERSfrom LB peer=0 - send
GETDATAto peer=0 - receive
CMPCTBLOCKfrom HB peer=1, it reconstructs, clearing download state for all peers for this block - receive
CMPCTBLOCKfrom LB peer=0
Might confuse a user, but nothing is actually wrong here?
|
|
||
| block, msg = build_compact_block() | ||
| unsolicited_peer.send_without_ping(msg_headers([block])) | ||
| unsolicited_peer.wait_for_getdata([block.hash_int], timeout=30) |
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.
Maybe use a comment to point out that since we only send the header and not the whole block, we are not marked as an HB peer?
And/or use assert_highbandwidth_states(node, idx=-1, hb_to=False) another time at the end of this block to verify/clarify.
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.
unsolicited_peer also hasn't sent sendcmpct so its m_provides_cmpctblocks is false, and the node still processes the compact block! I am ok with this as-is since imo it is useful to have coverage for this case.
Crypt-iQ
left a comment
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.
I agree with @dergoegge that it's easy to become a HB peer if you're already the first to send unsolicited compact blocks to try and fingerprint a node's mempool. I think the benefit with this change is being more strict with what we'll accept and following the protocol closer (and I think there's even more improvements we can make). Left some test-related nits.
|
|
||
| block, msg = build_compact_block() | ||
| unsolicited_peer.send_without_ping(msg_headers([block])) | ||
| unsolicited_peer.wait_for_getdata([block.hash_int], timeout=30) |
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.
unsolicited_peer also hasn't sent sendcmpct so its m_provides_cmpctblocks is false, and the node still processes the compact block! I am ok with this as-is since imo it is useful to have coverage for this case.
| @@ -4435,6 +4435,11 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type, | |||
| range_flight.first++; | |||
| } | |||
|
|
|||
| if (!requested_block_from_this_peer && !pfrom.m_bip152_highbandwidth_to) { | |||
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.
Future improvements to fully "lock down" this code path could be to:
- check
m_opts.ignore_incoming_txs - check
m_provides_cmpctblocksis set - check that if the block was requested, we used
MSG_CMPCT_BLOCK(requires state)
|
I'm not sure this makes sense, per #28272 (comment) . Perhaps it would be better to:
That would perhaps be slightly wasteful of bandwidth if a block arrives that includes a tx that was broadcast just before the block was found, but would avoid this being an additional fingerprinting vector independently of how high-bandwidth a potential attacker was, as far as I can see. |
Processing unsolicited CMPCTBLOCK's from a peer that has not been marked high bandwidth is not well-specified behavior in BIP-0152, in fact the BIP seems to imply that it is not permitted: "[...] method is not useful for compact blocks because `cmpctblock` blocks can be sent unsolicitedly in high-bandwidth mode" See https://github.com/bitcoin/bips/blob/master/bip-0152.mediawiki#separate-version-for-segregated-witness This partially mitigates a mempool leak described in [bitcoin#28272](bitcoin#28272), but that particular issue will persist for peers that have been selected as high bandwidth. This also mitigates potential DoS / bandwidth-wasting / abusive behavior that is discussed in the comments of bitcoin#28272.
This is move-only, and allows assert_highbandwidth_states to be used by other tests.
Modifies the invalid_cmpctblock_message to check that both HB peers sending unsolicited and non-HB peers sending unsolicited invalid cmpctblock's get the boot from us. Also refactors the test to make it less stateful.
Clear the getblocktxn message so we're not checking existing messages, and check that the hash in the getblocktxn match the cmpctblock being announced. Co-authored-by: Hodlinator <172445034+hodlinator@users.noreply.github.com>
5caaefd to
32bfd61
Compare
|
Rebased to address reviewer feedback. @ajtowns I'm thinking of this now less as a mitigation for the fingerprinting vector, and more just defense-in-depth / closing off attack surface for compact blocks. E.g. CVE-2024-35202 and CVE-2024-52921 are more trivial to exploit because you don't need an HB slot. An HB slot might be a low bar for an attacker to overcome as @dergoegge points out above/ But, as I see it, there is very rarely a legitimate/useful reason today to process an unsolicited CMPCTBLOCK message, especially if it's the case that getting an HB slot is trivial, and it does raise the cost of an attack, and it mitigates any DoS that require sending malformed CMPCTBLOCK's persistently, any attacker is likely to get cycled out of their HB slot. For example, once the Edit: Actually that DoS might be more effective with a single fake shortid. |
Kind of a tangent, but do you have any idea how fast/slow this is if the mempool has ~100k txns? |
|
@Crypt-iQ |
If we're concerned about that, seems better to fix it directly? --- a/src/net_processing.cpp
+++ b/src/net_processing.cpp
@@ -4412,6 +4412,11 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
while (range_flight.first != range_flight.second) {
if (range_flight.first->second.first == pfrom.GetId()) {
requested_block_from_this_peer = true;
+ const auto& qb_list_it = range_flight.first->second.second;
+ if (qb_list_it->partialBlock != nullptr) {
+ LogDebug(BCLog::CMPCTBLOCK, "Ignoring duplicate CMPCTBLOCK message for block %s received from peer=%d%s", pindex->GetBlockHash().ToString(), pfrom.GetId(), pfrom.LogIP(fLogIPs));
+ return;
+ }
break;
}
range_flight.first++;
I could imagine someone restarting the FIBRE network, and having it interface to the p2p network with a custom node implementation that implements logic like "only relay txs with an internal node. drop connections to anyone with a ping time exceeding 50ms. for each block, did I get this block via FIBRE? if so, send a CMPCTBLOCK message to all peers because I'm probably the quickest source in this region. if not, behave normally." |
The suggested diff doesn't work because we remove the block request from mapBlocksInFlight if the txn's are missing, the bitcoin/src/net_processing.cpp Lines 4494 to 4497 in 24434c1
To do something like what you're suggesting I think we would need another map that tracks whether or not a peer has already had the block in flight at one point and we gave up on it.
If I'm not mistaken, at least the only effort I know of at restarting the FIBRE network does not want to reveal the identity of the FIBRE nodes to prevent targeted DoS, but maybe I am mistaken, pinging @w0xlt. But, in general, I hear the broader stroke of your objection as: The "hole" in our implementation of compact block relay might actually be an opportunity we can leverage to improve block propagation, and we shouldn't close it so hastily. And that's fair enough, I kind of disagree about the FIBRE use case, but it is worth thinking about if, in an implementation like you suggested, where each node only processes one unsolicited block per peer per valid header, the price of
|
Does that logic make sense? If we receive: sequenceDiagram
participant A
Us ->> A: SENDCMPCT 0
Us ->> B: SENDCMPCT 1 (high-bandwidth)
A ->> Us: t=0 HEADERS
Us ->> A: GETDATA(CMPCT)
B ->> Us: t=25ms CMPCTBLOCK
Us ->> B: GETBLOCKTXN
A ->> Us: t=50ms CMPCTBLOCK
does it really make sense to not request the missing txns from the peer who first announced the header to us? If the RTT latency between us and B is 80ms (versus us and A at 50ms), then we'd get a reply from A at t=100ms and the reply from B at t=105ms, eg. Not requesting GETBLOCKTXN in response to a CMPCTBLOCK from a non-hb peer when we didn't request the cb and we already have a request in flight still sounds sensible, though, so I don't think this resolves the objection. Finding a way to refactor this code so that the logic for how we do block relay is mostly in one place rather than scattered amongst message various different message parsing logic might be valuable... I guess if you treated the logic as:
then if you received an unsolicited CMPCTBLOCK message that you already had things in flight for that block, you'd stop at (2) and just treat the message as an overly verbose headers update, and not even try to process it against the mempool (after all, if you have requests in flight, that was probably from an honest peer and you probably are actually missing transactions, so the reconstruction would probably fail anyway). OTOH, if you didn't have things in flight for it (or weren't being as parallel as you'd like to be), you would continue, and would request (and eventually process) the missing transactions. |
|
We add peers to The flow is bitcoin/src/net_processing.cpp Line 1241 in 6f35969
I think the first step of simplifying the logic would be to separate out "in-flight" as in I've The refactor you suggest sounds interesting, I don't know exactly how it would look, but if I'm understanding you right a centralized block processing function where some additional data (e.g. a CMPCTBLOCK) might or might not be available seems a reasonable approach, but the complexity would be in all the possible ways to "re-enter" the function, e.g. process the first message, it's a header we care about but no block or cmpctblock for us to process so send a GETDATA, get a cmpctblock message we're missing tx'es so send a getblocktxn, get a blocktxn, would this all be in the same function? |
polespinasa
left a comment
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.
code review ACK 32bfd61
I think it's worth resolving the odd case for -blocksonly in this pull request. But I would be happy too with it being done in a follow-up PR.
| @@ -4435,6 +4435,11 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type, | |||
| range_flight.first++; | |||
| } | |||
|
|
|||
| if (!requested_block_from_this_peer && !pfrom.m_bip152_highbandwidth_to) { | |||
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.
What if in the case of being in -blocksonly mode and receive a CMCTBLOCK message we instantly respond with a GETBLOCKTXN asking for all transactions (skipping all InitData logic)?
diff --git a/src/net_processing.cpp b/src/net_processing.cpp
index dad2c4ce6d..d46c9b003b 100644
--- a/src/net_processing.cpp
+++ b/src/net_processing.cpp
@@ -4413,6 +4413,15 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
}
if (!requested_block_from_this_peer && !pfrom.m_bip152_highbandwidth_to) {
+ if (m_opts.ignore_incoming_txs) {
+ BlockTransactionsRequest req;
+ for (size_t i = 0; i < cmpctblock.BlockTxCount(); i++) {
+ req.indexes.push_back(i);
+ }
+ req.blockhash = pindex->GetBlockHash();
+ MakeAndPushMessage(pfrom, NetMsgType::GETBLOCKTXN, vInv);
+ return;
+ }
LogDebug(BCLog::CMPCTBLOCK, "Peer %d%s, not marked as high-bandwidth, sent us an unsolicited compact block!", pfrom.GetId(), pfrom.LogIP(fLogIPs));
return;
}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.
treat a CMPCTBLOCK announcement as being fine at any time (eg, so that miners who get a block via RPC can just send CMPCTBLOCK messages directly), and the "high-bandwidth" as just a way for nodes to avoid wasting their bandwidth sending those messages unnecessarily
I think yes, except for -blocksonly nodes, where on receipt we wouldn't punish or anything, but we would ideally drop the message?
change our behaviour when sending GETBLOCKTXN to also request any transactions that were in our mempool but had a sequence number higher than when we first saw the new block's header (If you completely reconstructed the block, but used newer txns, you would still not send a GETBLOCKTXN)
I think it would be possible for an attacker to send HEADERS, receive GETDATA, wait for some time to allow new txns to accumulate in the node's mempool, then send a CMPCTBLOCK with suspected new txns that wouldn't have been announced yet. But, if the targeted node receives the valid block from any other peer, the download state gets wiped. So this is an edge-of-an-edge case. Since an attacker wants "tight timing information" as you pointed out on the linked issue and this is only able to be done ~10mins (and may not even work all the time if the attacker is not HB), it doesn't seem urgent.
without something concrete I would prefer to close down our implementation of the protocol to be as conservative as possible about what CMPCTBLOCK messages we process
Any restrictive change here introduces a kind of implicit version for compact block relay even if it's later lifted. I guess the unknown for me is whether this is something that miners (or others) want to do, are planning to do, or if this is a usecase we should support?
+1 to the refactor suggestion. I don't have any suggestions off-hand. Maybe a new file could be introduced specifically for compact block relay (similar to what was done for blockencodings.cpp)? It would make writing tests a bit easier. Also, checking whether partialBlock is nullptr (and having to reset it in some cases), is awkward.
| @@ -4435,6 +4435,11 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type, | |||
| range_flight.first++; | |||
| } | |||
|
|
|||
| if (!requested_block_from_this_peer && !pfrom.m_bip152_highbandwidth_to) { | |||
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.
Agree that it doesn't make sense to process, we should drop it asap. Haven't tested either of the below things:
ProcessHeadersMessage will trigger a GETDATA and if the block turns out to be invalid, our peer who sent us the CMPCTBLOCK won't respond and will get disconnected for stalling. If our peer is modified to relay these in the first place to all peers, they could isolate themselves from -blocksonly nodes (assuming they aren't a mining node and don't validate what they send out). This is #13370.
A tangent, but I think a miner could trigger this statement in honest peer's connections if they mined and withheld enough blocks (~4-5) and if the last CMPCTBLOCK is for an invalid block. I'm leaving out a few steps and can write out what I think on the linked issue if it's worth addressing?
| @@ -4412,6 +4412,11 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type, | |||
| range_flight.first++; | |||
| } | |||
|
|
|||
| if (!requested_block_from_this_peer && !pfrom.m_bip152_highbandwidth_to) { | |||
| LogDebug(BCLog::NET, "Peer %d, not marked as high-bandwidth, sent us an unsolicited compact block!\n", pfrom.GetId()); | |||
| return; | |||
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.
Haven't tested. I think this will cause extra log messages if:
- receive
HEADERSfrom LB peer=0 - send
GETDATAto peer=0 - receive
CMPCTBLOCKfrom HB peer=1, it reconstructs, clearing download state for all peers for this block - receive
CMPCTBLOCKfrom LB peer=0
Might confuse a user, but nothing is actually wrong here?
hodlinator
left a comment
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.
Reviewed 32bfd61
| unsolicited_peer.send_and_ping(msg) | ||
| with p2p_lock: | ||
| assert "getblocktxn" in unsolicited_peer.last_message | ||
| unsolicited_peer.clear_getblocktxn() |
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.
This clearing should arguably happen before the assert, along the lines of #32606 (comment) / 32bfd61
Full diff, including some previously missing clear_getblocktxn(), and deferring clearing to the last possible place.
--- a/test/functional/p2p_compactblocks.py
+++ b/test/functional/p2p_compactblocks.py
@@ -414,6 +414,7 @@ class CompactBlocksTest(BitcoinTestFramework):
[k0, k1] = comp_block.get_siphash_keys()
coinbase_hash = block.vtx[0].wtxid_int
comp_block.shortids = [calculate_shortid(k0, k1, coinbase_hash)]
+ test_node.clear_getblocktxn()
test_node.send_and_ping(msg_cmpctblock(comp_block.to_p2p()))
assert_equal(int(node.getbestblockhash(), 16), block.hashPrevBlock)
# Expect a getblocktxn message.
@@ -451,6 +452,7 @@ class CompactBlocksTest(BitcoinTestFramework):
node = self.nodes[0]
def test_getblocktxn_response(compact_block, peer, expected_result):
+ peer.clear_getblocktxn()
msg = msg_cmpctblock(compact_block.to_p2p())
peer.send_and_ping(msg)
with p2p_lock:
@@ -517,8 +519,7 @@ class CompactBlocksTest(BitcoinTestFramework):
assert tx.txid_hex in mempool
# Clear out last request.
- with p2p_lock:
- test_node.last_message.pop("getblocktxn", None)
+ test_node.clear_getblocktxn()
# Send compact block
comp_block.initialize_from_block(block, prefill_list=[0], use_witness=True)
@@ -547,6 +548,7 @@ class CompactBlocksTest(BitcoinTestFramework):
# Send compact block
comp_block = HeaderAndShortIDs()
comp_block.initialize_from_block(block, prefill_list=[0], use_witness=True)
+ test_node.clear_getblocktxn()
test_node.send_and_ping(msg_cmpctblock(comp_block.to_p2p()))
absolute_indexes = []
with p2p_lock:
@@ -592,6 +594,7 @@ class CompactBlocksTest(BitcoinTestFramework):
# Send compact block
comp_block = HeaderAndShortIDs()
comp_block.initialize_from_block(block, prefill_list=[0], use_witness=True)
+ test_node.clear_getblocktxn()
test_node.send_and_ping(msg_cmpctblock(comp_block.to_p2p()))
absolute_indexes = []
with p2p_lock:
@@ -844,6 +847,7 @@ class CompactBlocksTest(BitcoinTestFramework):
cmpct_block = HeaderAndShortIDs()
cmpct_block.initialize_from_block(block)
+ peer.clear_getblocktxn()
msg = msg_cmpctblock(cmpct_block.to_p2p())
peer.send_and_ping(msg)
with p2p_lock:
@@ -924,14 +928,13 @@ class CompactBlocksTest(BitcoinTestFramework):
assert len(self.utxos)
def announce_cmpct_block(node, peer, txn_count, expect_getblocktxn):
- peer.clear_getblocktxn()
-
utxo = self.utxos.pop(0)
block = self.build_block_with_transactions(node, utxo, txn_count)
cmpct_block = HeaderAndShortIDs()
cmpct_block.initialize_from_block(block)
msg = msg_cmpctblock(cmpct_block.to_p2p())
+ peer.clear_getblocktxn()
peer.send_and_ping(msg)
with p2p_lock:
if expect_getblocktxn:
@@ -1027,10 +1030,10 @@ class CompactBlocksTest(BitcoinTestFramework):
block, msg = build_compact_block()
unsolicited_peer.send_without_ping(msg_headers([block]))
unsolicited_peer.wait_for_getdata([block.hash_int], timeout=30)
+ unsolicited_peer.clear_getblocktxn()
unsolicited_peer.send_and_ping(msg)
with p2p_lock:
assert "getblocktxn" in unsolicited_peer.last_message
- unsolicited_peer.clear_getblocktxn()
# The node will ask for transactions from an unsolicited compact block
# it receives from a high bandwidth peer, we need to use one set up earlier,| # This index will be too high | ||
| prefilled_txn = PrefilledTransaction(1, block.vtx[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.
nit: Named variable feels clearer. (No risk of confusing with the index used for block.vtx[...]).
| # This index will be too high | |
| prefilled_txn = PrefilledTransaction(1, block.vtx[0]) | |
| too_high_index = 1 | |
| prefilled_txn = PrefilledTransaction(too_high_index, block.vtx[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.
Reviewed 32bfd61 and left some comments. I think if the use-case brought up by @ajtowns is not a concern, then I can ACK.
Separately, I've been thinking about how a refactor would look. I'm still at a loss because unifying the headers and the compact blocks processing logic seems difficult because sometimes the compact block can be processed as a headers message (i.e. calls ProcessHeadersMessage) and there are compact blocks-specific checks that a headers message wouldn't go through.
| hb_peer.send_await_disconnect(msg_cmpctblock(cmpct_block)) | ||
| assert_equal(int(self.nodes[0].getbestblockhash(), 16), block.hashPrevBlock) | ||
|
|
||
| # Test a solicited invalid cmpctblock from a non-HB peer. |
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.
Commit message of 42e281d can change to say "non-HB peers sending unsolicited invalid"
| assert_equal([peer['bip152_hb_to'] for peer in node.getpeerinfo()], [False, True, True, True]) | ||
|
|
||
| block, cmpct_block = announce_cmpct_block(node, stalling_peer, num_missing) | ||
| block, cmpct_block = announce_cmpct_block(node, stalling_peer, num_missing, expect_getblocktxn=False) |
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.
Since stalling_peer is low-bandwidth, the compact block is dropped and the full block is requested (taking up the first slot) in SendMessages since self.nodes[0] has processed the header. The test still passes, but I think what this is testing has changed slightly from the original. The original behavior can be maintained if stalling_peer sends the header first and then sends over the compact block.
w0xlt
left a comment
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.
reACK 32bfd61
| range_flight.first++; | ||
| } | ||
|
|
||
| if (!requested_block_from_this_peer && !pfrom.m_bip152_highbandwidth_to) { |
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.
nit: Maybe add a comment for clarification?
| if (!requested_block_from_this_peer && !pfrom.m_bip152_highbandwidth_to) { | |
| // Accept unsolicited cmpctblock *only* from peers we selected as high-bandwidth | |
| // "to" (hb_to). Peers that merely requested HB *from* us (hb_from) don't qualify. | |
| // This matches BIP152's "unsolicited in high-bandwidth mode" intent. | |
| if (!requested_block_from_this_peer && !pfrom.m_bip152_highbandwidth_to) { |
Processing unsolicited
CMPCTBLOCK's from a peer that has not been marked high bandwidth is not well-specified behavior in BIP-0152, in fact the BIP seems to imply that it is not permitted:See https://github.com/bitcoin/bips/blob/master/bip-0152.mediawiki#separate-version-for-segregated-witness
This partially mitigates a mempool leak described in #28272, making it only possible for peers selected as high bandwidth to discover a node's mempool.