private_broadcast: fix HavePendingTransactions() cascade that leaks tx origin #35530

pull JACKURUVI99 wants to merge 1 commits into bitcoin:master from JACKURUVI99:fix/private-broadcast-have-pending-transactions changing 4 files +53 −5
  1. JACKURUVI99 commented at 9:58 PM on June 13, 2026: none

    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 a sufficient_confirmations parameter (default SIZE_MAX preserves all existing callers' semantics — "any tx in system = pending").
    • The FinalizeNode call site passes NUM_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_threshold demonstrates that after 3 confirmations, HavePendingTransactions(3) returns false while the transaction is still present in m_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

  2. private_broadcast: fix HavePendingTransactions() cascade bug
    HavePendingTransactions() previously returned !m_transactions.empty(),
    which is true for any transaction that has not yet been garbage-collected
    — including those already confirmed by NUM_PRIVATE_BROADCAST_PER_TX peers.
    Transactions remain in m_transactions for up to ~1-3 minutes after full
    confirmation (until GetStale() + ReattemptPrivateBroadcast remove them).
    
    During that window, any failed private-broadcast connection triggers
    FinalizeNode → HavePendingTransactions() == true → NumToOpenAdd(1),
    opening a replacement connection even though all peers have already
    confirmed receipt. The replacement calls PickTxForSend, which picks the
    fully-confirmed transaction and sends it to an additional peer — a direct
    privacy violation: the transaction is relayed to more than
    NUM_PRIVATE_BROADCAST_PER_TX peers before public broadcast.
    
    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 extra connection
    the victim opens to an attacker-controlled node leaks that the victim's
    IP originated this transaction, deanonymizing the sender.
    
    Fix: accept a sufficient_confirmations threshold (default SIZE_MAX to
    preserve existing callers). FinalizeNode now passes
    NUM_PRIVATE_BROADCAST_PER_TX so a replacement is only opened when at
    least one transaction genuinely needs more confirmations.
    
    A new test have_pending_transactions_threshold demonstrates that after
    3 confirmations HavePendingTransactions(3) returns false while the
    transaction is still present in m_transactions.
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
    995431b9d4
  3. DrahtBot commented at 9:58 PM on June 13, 2026: contributor

    ♻️ Automatically closing for now based on heuristics. Please leave a comment, if this was erroneous. Generally, please focus on creating high-quality, original content that demonstrates a clear understanding of the project's requirements and goals.

    📝 Moderators: If this is spam, please replace the title with ., so that the thread does not appear in search results.

  4. DrahtBot closed this on Jun 13, 2026

  5. DrahtBot commented at 9:58 PM on June 13, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/35530.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline for information on the review process. A summary of reviews will appear here.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->


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