private_broadcast: HavePendingTransactions() returns true after sufficient confirmations, causing privacy-violating cascade #35529

issue JACKURUVI99 opened this issue on June 13, 2026
  1. JACKURUVI99 commented at 9:58 PM on June 13, 2026: none

    Summary

    PrivateBroadcast::HavePendingTransactions() returns !m_transactions.empty(), which is true even when all transactions have already been confirmed by NUM_PRIVATE_BROADCAST_PER_TX (3) peers.

    Transactions are not immediately removed from m_transactions after full confirmation — they remain until GetStale() + ReattemptPrivateBroadcast cleans them up, which can take 1–3 minutes (governed by STALE_DURATION = 1min).

    Impact

    In FinalizeNode, when a private-broadcast connection closes without sending PONG:

    if (node.IsPrivateBroadcastConn() &&
        !m_tx_for_private_broadcast.DidNodeConfirmReception(nodeid) &&
        m_tx_for_private_broadcast.HavePendingTransactions()) {  // ← always true during stale window
        m_connman.m_private_broadcast.NumToOpenAdd(1);
    }
    

    This opens a replacement connection even when all 3 target peers have confirmed. The replacement calls PickTxForSend, which picks the already-fully-confirmed transaction and sends it to a 4th (or 5th, 6th…) peer — directly defeating the privacy guarantee of private broadcast.

    Exploit Scenario

    An adversary running several Bitcoin nodes can:

    1. Accept a private-broadcast connection from a victim
    2. Receive the INV (transaction)
    3. Disconnect without sending PONG (no confirmation)
    4. Observe the victim immediately open another connection to another adversary-controlled node
    5. Repeat, causing unbounded cascade

    Each extra connection the victim opens to an adversary-controlled node confirms that this specific IP originated the transaction, deanonymizing the sender. This requires no special exploit — just not responding to PING.

    Root Cause

    // src/private_broadcast.cpp (before fix)
    bool PrivateBroadcast::HavePendingTransactions()
    {
        LOCK(m_mutex);
        return !m_transactions.empty();  // true even for fully-confirmed txs
    }
    

    Fix

    Accept a sufficient_confirmations threshold and only return true if at least one transaction has fewer confirmations than the threshold. The FinalizeNode call site passes NUM_PRIVATE_BROADCAST_PER_TX:

    // src/private_broadcast.cpp (after fix)
    bool PrivateBroadcast::HavePendingTransactions(size_t sufficient_confirmations)
    {
        LOCK(m_mutex);
        for (const auto& [tx, state] : m_transactions) {
            const Priority p{DerivePriority(state.send_statuses)};
            if (p.num_confirmed < sufficient_confirmations) return true;
        }
        return false;
    }
    

    A PR with the fix and a regression test is attached.

  2. pinheadmz commented at 10:14 PM on June 13, 2026: member

    Each additional connection the victim opens to an adversary-controlled node confirms that this IP originated the transaction

    Are you saying these connections are not made over Tor?

  3. Crypt-iQ commented at 1:39 PM on June 15, 2026: contributor

    This doesn't make sense? If the node hasn't responded with a PONG, the private broadcast sender will, by design, broadcast to other peers and will do so through the proxy.

  4. maflcko commented at 1:43 PM on June 15, 2026: member

    looks like ai slop

  5. maflcko closed this on Jun 15, 2026


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-06-20 23:51 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me