Fixes #35529
Problem
HavePendingTransactions() returned !m_transactions.empty(), which is true for any transaction still in the map — including those already confirmed by all NUM_PRIVATE_BROADCAST_PER_TX (3) peers. Transactions remain in m_transactions for up to ~1-3 minutes after full confirmation (until GetStale() + ReattemptPrivateBroadcast remove them via STALE_DURATION).
During that window, FinalizeNode incorrectly opens a replacement connection whenever any private-broadcast connection closes without confirming — even though the transaction is fully confirmed. The replacement calls PickTxForSend, which picks the fully-confirmed transaction and sends it to a 4th peer, violating the privacy guarantee of private broadcast.
Adversary scenario: A network-level adversary running several Bitcoin nodes can deliberately disconnect private-broadcast connections without responding to PING, triggering an unbounded cascade of replacements. Each additional connection the victim opens to an adversary-controlled node confirms that this IP originated the transaction — deanonymizing the sender with no special exploit code required.
Fix
HavePendingTransactions()now accepts asufficient_confirmationsparameter (defaultSIZE_MAXpreserves all existing callers' semantics — "any tx in system = pending").- The
FinalizeNodecall site passesNUM_PRIVATE_BROADCAST_PER_TX, so a replacement is only opened when at least one transaction genuinely has fewer than 3 confirmations. - A new unit test
have_pending_transactions_thresholddemonstrates that after 3 confirmations,HavePendingTransactions(3)returnsfalsewhile the transaction is still present inm_transactions— directly covering the previously-buggy path.
Changes
| File | Change |
|---|---|
src/private_broadcast.h |
Add sufficient_confirmations parameter with docstring |
src/private_broadcast.cpp |
Replace !m_transactions.empty() with per-tx confirmation count loop |
src/net_processing.cpp |
Pass NUM_PRIVATE_BROADCAST_PER_TX at the FinalizeNode call site |
src/test/private_broadcast_tests.cpp |
Add have_pending_transactions_threshold test case |
Test plan
./build/src/test/test_bitcoin --run_test=private_broadcast_tests/have_pending_transactions_threshold./build/src/test/test_bitcoin --run_test=private_broadcast_tests(all existing tests pass)- CI green
🤖 Generated with Claude Code