p2p: Drop unsolicited CMPCTBLOCK from non-HB peer
What changed, and why it matters
This Bitcoin Core change tightens the rules for a fast block-relay feature called compact blocks (BIP 152). Previously, any peer could send a compact block announcement without being asked. Now, if a peer sends one without being marked as a 'high-bandwidth' peer and without the node having requested the block from them, the message is ignored. The commit says this partly closes a known mempool information leak and reduces the risk of bandwidth-wasting abuse, but it is only a partial fix because high-bandwidth peers can still send unsolicited compact blocks.
Review whether the remaining exposure through high-bandwidth peers is acceptable, and consider additional mempool privacy hardening for issue #28272. Operators should upgrade to include this partial mitigation.
Security signals we found
Unsolicited network message now dropped
Partial mitigation of reported mempool privacy leak (#28272)
DoS/bandwidth-waste mitigation mentioned by committer
BIP 152 behavior clarification/enforcement
Test changes add explicit solicitation (headers/getdata) before compact block sends
Evidence from the diff
In src/net_processing.cpp, ProcessMessage for CMPCTBLOCK now checks requested_block_from_this_peer || pfrom.m_bip152_highbandwidth_to before continuing to process the compact block. If neither is true, it logs and returns early. Tests are updated so that compact block announcements are preceded by a headers message that triggers a getdata request, satisfying the new ‘requested from this peer’ path, or peers are made high-bandwidth candidates. The change is described as a partial mitigation for issue #28272 (mempool leak) and potential DoS/bandwidth abuse discussed there.
Changed components
src/net_processing.cpp compact block processingBIP 152 high-bandwidth peer selection logictest/functional/p2p_compactblocks.pytest/functional/p2p_mutated_blocks.pyInspect captured patch +31 / −3
diff --git a/src/net_processing.cpp b/src/net_processing.cpp
index 50ea62ff..e7afa10f 100644
--- a/src/net_processing.cpp
+++ b/src/net_processing.cpp
@@ -2610,7 +2610,7 @@ void PeerManagerImpl::SendBlockTransactions(CNode& pfrom, Peer& peer, const CBlo
if (util::log::ShouldDebugLog(BCLog::CMPCTBLOCK)) {
uint32_t tx_requested_size{0};
for (const auto& tx : resp.txn) tx_requested_size += tx->ComputeTotalSize();
- LogDebug(BCLog::CMPCTBLOCK, "Peer %d sent us a GETBLOCKTXN for block %s, sending a BLOCKTXN with %u txns. (%u bytes)\n", pfrom.GetId(), block.GetHash().ToString(), resp.txn.size(), tx_requested_size);
+ LogDebug(BCLog::CMPCTBLOCK, "%s sent us a GETBLOCKTXN for block %s, sending a BLOCKTXN with %u txns. (%u bytes)", pfrom.LogPeer(), block.GetHash().ToString(), resp.txn.size(), tx_requested_size);
}
MakeAndPushMessage(pfrom, NetMsgType::BLOCKTXN, resp);
}
@@ -4556,6 +4556,11 @@ void PeerManagerImpl::ProcessMessage(Peer& peer, CNode& pfrom, const std::string
range_flight.first++;
}
+ if (!requested_block_from_this_peer && !pfrom.m_bip152_highbandwidth_to) {
+ LogDebug(BCLog::CMPCTBLOCK, "%s, not marked as high-bandwidth, sent us an unsolicited compact block!", pfrom.LogPeer());
+ return;
+ }
+
if (pindex->nChainWork <= m_chainman.ActiveChain().Tip()->nChainWork || // We know something better
pindex->nTx != 0) { // We had this block at some point, but pruned it
if (requested_block_from_this_peer) {
diff --git a/test/functional/p2p_compactblocks.py b/test/functional/p2p_compactblocks.py
index 52bbb294..243b010f 100755
--- a/test/functional/p2p_compactblocks.py
+++ b/test/functional/p2p_compactblocks.py
@@ -189,13 +189,16 @@ class CompactBlocksTest(BitcoinTestFramework):
assert_equal(self.nodes[0].getbestblockhash(), block2.hash_hex)
self.utxos.extend([[tx.txid_int, i, out_value] for i in range(10)])
- def announce_cmpct_block(self, node, peer, txn_count=5):
+ def announce_cmpct_block(self, node, peer, txn_count=5, solicit=False):
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())
+ if solicit:
+ peer.send_without_ping(msg_headers([block]))
+ peer.wait_for_getdata([block.hash_int], timeout=30)
peer.clear_getblocktxn()
peer.send_and_ping(msg)
@@ -292,6 +295,9 @@ class CompactBlocksTest(BitcoinTestFramework):
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.hash_int], timeout=30)
+
cmpct_block = P2PHeaderAndShortIDs()
cmpct_block.header = CBlockHeader(block)
cmpct_block.prefilled_txn_length = 1
@@ -581,6 +587,10 @@ class CompactBlocksTest(BitcoinTestFramework):
block = self.build_block_with_transactions(node, utxo, 2)
+ # The attacker sends the block header so that we request it.
+ test_node.send_without_ping(msg_headers([block]))
+ test_node.wait_for_getdata([block.hash_int], timeout=30)
+
# Send compact block
comp_block = HeaderAndShortIDs()
comp_block.initialize_from_block(block, prefill_list=[0], use_witness=True)
@@ -806,6 +816,12 @@ class CompactBlocksTest(BitcoinTestFramework):
msg = msg_cmpctblock(comp_block.to_p2p())
test_node.send_await_disconnect(msg)
+ # peer generates a block and sends it to node, which makes the peer a
+ # candidate for high-bandwidth 'to' (up to 3 peers according to BIP 152)
+ def make_peer_hb_to_candidate(self, node, peer):
+ block = self.build_block_on_tip(node)
+ peer.send_and_ping(msg_block(block))
+
# Helper for enabling cb announcements
# Send the sendcmpct request and sync headers
def request_cb_announcements(self, peer):
@@ -818,6 +834,7 @@ class CompactBlocksTest(BitcoinTestFramework):
node = self.nodes[0]
assert len(self.utxos)
+ self.make_peer_hb_to_candidate(node, delivery_peer)
block, cmpct_block = self.announce_cmpct_block(node, stalling_peer)
for tx in block.vtx[1:]:
@@ -890,6 +907,7 @@ class CompactBlocksTest(BitcoinTestFramework):
for name, peer in [("delivery", delivery_peer), ("inbound", inbound_peer), ("outbound", outbound_peer)]:
self.log.info(f"Setting {name} as high bandwidth peer")
+ self.make_peer_hb_to_candidate(node, peer)
block, cmpct_block = self.announce_cmpct_block(node, peer, 1)
msg = msg_blocktxn()
msg.block_transactions.blockhash = block.hash_int
@@ -907,7 +925,7 @@ class CompactBlocksTest(BitcoinTestFramework):
# Remaining low-bandwidth peer is stalling_peer, who announces first
assert_equal([peer['bip152_hb_to'] for peer in node.getpeerinfo()], [False, True, True, True])
- block, cmpct_block = self.announce_cmpct_block(node, stalling_peer, num_missing)
+ block, cmpct_block = self.announce_cmpct_block(node, stalling_peer, num_missing, solicit=True)
delivery_peer.send_and_ping(msg_cmpctblock(cmpct_block.to_p2p()))
# The second peer to announce should still get a getblocktxn
diff --git a/test/functional/p2p_mutated_blocks.py b/test/functional/p2p_mutated_blocks.py
index 609c4449..b9c266f8 100755
--- a/test/functional/p2p_mutated_blocks.py
+++ b/test/functional/p2p_mutated_blocks.py
@@ -14,6 +14,7 @@ from test_framework.messages import (
msg_cmpctblock,
msg_block,
msg_blocktxn,
+ msg_headers,
HeaderAndShortIDs,
)
from test_framework.test_framework import BitcoinTestFramework
@@ -57,6 +58,10 @@ class MutatedBlocksTest(BitcoinTestFramework):
mutated_block = copy.deepcopy(block)
mutated_block.vtx[1].version = 4
+ # Send block header through the honest relayer
+ honest_relayer.send_without_ping(msg_headers([block]))
+ honest_relayer.wait_for_getdata([block.hash_int], timeout=30)
+
# Announce the new block via a compact block through the honest relayer
cmpctblock = HeaderAndShortIDs()
cmpctblock.initialize_from_block(block, use_witness=True)
Why this scored 58/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.