p2p: make blocksonly nodes ignore CMPCTBLOCK messages
What changed, and why it matters
This change fixes a privacy leak in Bitcoin Core's 'blocksonly' mode. Blocksonly nodes intentionally avoid keeping a mempool of unconfirmed transactions. Previously, they would still try to process compact block (CMPCTBLOCK) messages from peers. Because they lack a mempool, they would be missing almost every transaction referenced in a compact block—except for transactions they themselves created. When responding to ask for the missing transactions, the node would reveal exactly which transactions were its own, linking those transactions to that node's IP address. The fix makes blocksonly nodes simply ignore incoming compact block messages, so they never send those revealing responses. The commit message explicitly calls this out as a real privacy issue, not just a defensive cleanup.
Reviewers should confirm that ignoring CMPCTBLOCK does not interfere with blocksonly nodes' ability to stay in sync or serve compact blocks to peers (the test asserts serving still works). Operators running -blocksonly should upgrade to obtain the privacy fix. No immediate incident response is indicated beyond normal patch uptake.
Security signals we found
Privacy / deanonymization fix: blocksonly node's GETBLOCKTXN response could reveal own transactions
Network-layer behavior change: ignore CMPCTBLOCK when -blocksonly is enabled
New functional test covering both solicited and unsolicited compact blocks on blocksonly nodes
Commit message explicitly frames the change as security-relevant, not merely hardening
Evidence from the diff
In src/net_processing.cpp, the CMPCTBLOCK handler now returns early when m_opts.ignore_incoming_txs is true (the blocksonly configuration), in addition to the existing early-return during block import. The commit message states that without this, a blocksonly node receiving a compact block would be unable to reconstruct it from its mempool and would send a GETBLOCKTXN response listing every missing transaction except its own, deanonymizing which transactions originated from that node. A new functional test, p2p_compactblocks_blocksonly.py, verifies that blocksonly nodes ignore CMPCTBLOCK even when the block is solicited, while normal and low-bandwidth nodes still process them appropriately. The patch is partial in the sense that it is narrowly scoped to the receive path and does not alter compact block relay behavior for non-blocksonly peers.
Changed components
src/net_processing.cpp (CMPCTBLOCK message handler)test/functional/p2p_compactblocks_blocksonly.pyBitcoin Core P2P compact block protocol (BIP152) interaction with -blocksonly nodesInspect captured patch +51 / −1
diff --git a/src/net_processing.cpp b/src/net_processing.cpp
index e7afa10f..41d39966 100644
--- a/src/net_processing.cpp
+++ b/src/net_processing.cpp
@@ -4469,7 +4469,10 @@ void PeerManagerImpl::ProcessMessage(Peer& peer, CNode& pfrom, const std::string
{
// Ignore cmpctblock received while importing
if (m_chainman.m_blockman.LoadingBlocks()) {
- LogDebug(BCLog::NET, "Unexpected cmpctblock message received from peer %d\n", pfrom.GetId());
+ LogDebug(BCLog::CMPCTBLOCK, "%s sent us a compact block even though we are still loading blocks!", pfrom.LogPeer());
+ return;
+ } else if (m_opts.ignore_incoming_txs) {
+ LogDebug(BCLog::CMPCTBLOCK, "%s sent us a compact block even though we are blocksonly!", pfrom.LogPeer());
return;
}
diff --git a/test/functional/p2p_compactblocks_blocksonly.py b/test/functional/p2p_compactblocks_blocksonly.py
index befc283f..3fed634a 100755
--- a/test/functional/p2p_compactblocks_blocksonly.py
+++ b/test/functional/p2p_compactblocks_blocksonly.py
@@ -12,7 +12,9 @@ from test_framework.messages import (
CBlockHeader,
CInv,
from_hex,
+ HeaderAndShortIDs,
msg_block,
+ msg_cmpctblock,
msg_getdata,
msg_headers,
msg_sendcmpct,
@@ -38,6 +40,30 @@ class P2PCompactBlocksBlocksOnly(BitcoinTestFramework):
block = from_hex(CBlock(), block_hex)
return block
+ def ignores_cmpctblock(self, node_id, conn, solicited=False):
+ # Briefly connect to mining node, sync, and disconnect, just to make
+ # sure the node is on the same tip as the miner so that compact block
+ # reconstruction works.
+ self.connect_nodes(2, node_id)
+ self.sync_blocks([self.nodes[2], self.nodes[node_id]], timeout=10)
+ self.disconnect_nodes(2, node_id)
+
+ block = self.build_block_on_tip()
+ if solicited:
+ conn.send_without_ping(msg_headers([block]))
+ conn.wait_for_getdata([block.hash_int], timeout=10)
+ cmpct_block = HeaderAndShortIDs()
+ cmpct_block.initialize_from_block(block, use_witness=True)
+ msg = msg_cmpctblock(cmpct_block.to_p2p())
+ conn.send_and_ping(msg)
+
+ cmpct_block_received = self.nodes[node_id].getbestblockhash() == cmpct_block.header.hash_hex
+ if not cmpct_block_received:
+ # Compact block was ignored, send the full block to keep in sync.
+ conn.send_and_ping(msg_block(block))
+
+ return not cmpct_block_received
+
def run_test(self):
# Nodes will only request hb compact blocks mode when they're out of IBD
for node in self.nodes:
@@ -98,11 +124,19 @@ class P2PCompactBlocksBlocksOnly(BitcoinTestFramework):
p2p_conn_high_bw.send_and_ping(msg_headers(headers=[CBlockHeader(block1)]))
assert_equal(p2p_conn_high_bw.last_message['getdata'].inv, [CInv(MSG_CMPCT_BLOCK, block1.hash_int)])
+ # Send the block to avoid stalling the peer later in the test.
+ comp_block = HeaderAndShortIDs()
+ comp_block.initialize_from_block(block1, use_witness=True)
+ block1_cb_msg = msg_cmpctblock(comp_block.to_p2p())
+ p2p_conn_high_bw.send_and_ping(block1_cb_msg)
+
self.log.info("Test that getdata(CMPCT) is still sent on BIP152 low bandwidth connections"
" when no -blocksonly nodes are involved")
p2p_conn_low_bw.send_and_ping(msg_headers(headers=[CBlockHeader(block1)]))
assert_equal(p2p_conn_low_bw.last_message['getdata'].inv, [CInv(MSG_CMPCT_BLOCK, block1.hash_int)])
+ # Send the block to avoid stalling the peer later in the test.
+ p2p_conn_low_bw.send_and_ping(block1_cb_msg)
self.log.info("Test that -blocksonly nodes still serve compact blocks")
@@ -122,5 +156,18 @@ class P2PCompactBlocksBlocksOnly(BitcoinTestFramework):
self.nodes[0].submitblock(block2.serialize().hex())
p2p_conn_blocksonly.wait_until(lambda: test_for_cmpctblock(block2))
+ # This is redundant with other tests, and is here as a test-of-the-test
+ self.log.info("Test that normal nodes don't ignore CMPCTBLOCK messages from HB peers")
+ assert not self.ignores_cmpctblock(1, p2p_conn_high_bw, solicited=False)
+
+ self.log.info("Test that -blocksonly nodes ignore CMPCTBLOCK messages")
+ assert self.ignores_cmpctblock(0, p2p_conn_blocksonly, solicited=False)
+
+ self.log.info("Test that low bandwidth nodes listen to CMPCTBLOCK messages when the block is requested")
+ assert not self.ignores_cmpctblock(3, p2p_conn_low_bw, solicited=True)
+
+ self.log.info("Test that -blocksonly nodes ignore CMPCTBLOCK messages even when the block is requested")
+ assert self.ignores_cmpctblock(0, p2p_conn_blocksonly, solicited=True)
+
if __name__ == '__main__':
P2PCompactBlocksBlocksOnly(__file__).main()
Why this scored 60/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.