rpc,net: Add private broadcast RPCs #34329

pull andrewtoth wants to merge 7 commits into bitcoin:master from andrewtoth:private_broadcast_rpcs changing 9 files +315 −9
  1. andrewtoth commented at 1:46 am on January 18, 2026: contributor

    Follow up from #29415

    Sending a transaction via private broadcast does not have any way for a user to track the status of the transaction before it gets returned by another peer. The default logs have been removed as well in #34267. Nor is there any way to abort a transaction once it has been added to the private broadcast queue.

    This adds two new RPCs:

    • getprivatebroadastinfo returns information about what transactions are in the private broadcast queue, including all the peers’ addresses we have chosen and timestamps.
    • abortprivatebroadcast stops broadcasting a transaction in the private broadcast queue.
  2. DrahtBot commented at 1:46 am on January 18, 2026: contributor

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

    Code Coverage & Benchmarks

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

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept ACK nervana21, l0rinc
    Stale ACK danielabrozzoni, vasild

    If your review is incorrectly listed, please copy-paste <!–meta-tag:bot-skip–> into the comment that the bot should ignore.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #34322 (node: Persist private broadcast transactions over node restarts by andrewtoth)
    • #34271 (net_processing: make m_tx_for_private_broadcast optional by vasild)
    • #34049 (rpc: Disallow captures in RPCMethodImpl by ajtowns)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    LLM Linter (✨ experimental)

    Possible places where comparison-specific test macros should replace generic comparisons:

    • test/functional/p2p_private_broadcast.py: “assert len(peers) >= NUM_PRIVATE_BROADCAST_PER_TX” -> recommendation: use assert_greater_than_or_equal(len(peers), NUM_PRIVATE_BROADCAST_PER_TX)
    • test/functional/p2p_private_broadcast.py: “assert sum(1 for p in peers if "received" in p) >= NUM_PRIVATE_BROADCAST_PER_TX” -> recommendation: store the sum in a variable and use assert_greater_than_or_equal(that_sum, NUM_PRIVATE_BROADCAST_PER_TX)

    No comparison macro suggestions were found in C++ code.

    2026-02-10 15:02:59

  3. DrahtBot added the label CI failed on Jan 18, 2026
  4. DrahtBot commented at 3:04 am on January 18, 2026: contributor

    🚧 At least one of the CI tasks failed. Task Windows native, fuzz, VS 2022: https://github.com/bitcoin/bitcoin/actions/runs/21104117103/job/60692665532 LLM reason (✨ experimental): Fuzzing failed because the RPC command “abortprivatebroadcast” is not registered as safe or not-safe for fuzzing in rpc.cpp.

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  5. andrewtoth force-pushed on Jan 18, 2026
  6. andrewtoth force-pushed on Jan 18, 2026
  7. andrewtoth force-pushed on Jan 18, 2026
  8. in src/test/private_broadcast_tests.cpp:133 in ac6c2bd064
    128+        BOOST_REQUIRE(it_confirmed != infos.end());
    129+        BOOST_REQUIRE(it_unconfirmed != infos.end());
    130+        const auto& confirmed_tx{*it_confirmed};
    131+        const auto& unconfirmed_tx{*it_unconfirmed};
    132+
    133+        BOOST_CHECK_EQUAL(confirmed_tx.num_peer_reception_acks, 1);
    


    vasild commented at 9:15 am on January 21, 2026:

    I picked “confirmed” for PrivateBroadcast::SendStatus::confirmed, but looking at it now…

    “Confirmed” overlaps with an already established term, meaning “included in a block”. So, maybe better to use any other word, but not “confirmed”.

    “received”? “reception confirmed”? “broadcast”?

    Or at least keep this in mind ;-) Feel free to ignore.


    andrewtoth commented at 6:30 pm on January 25, 2026:
    Changed to “received”.
  9. in src/private_broadcast.cpp:128 in ac6c2bd064
    123+    }
    124+
    125+    // Ensure stable output order (m_transactions is an unordered_map).
    126+    std::sort(entries.begin(), entries.end(), [](const TxBroadcastInfo& a, const TxBroadcastInfo& b) {
    127+        return std::tie(a.tx->GetHash(), a.tx->GetWitnessHash()) < std::tie(b.tx->GetHash(), b.tx->GetWitnessHash());
    128+    });
    


    vasild commented at 9:22 am on January 21, 2026:
    Sorting seems unnecessary. It might be difficult to drop it later if users get used to it and start depending on it.
  10. in src/private_broadcast.h:42 in ac6c2bd064 outdated
    34+        CTransactionRef tx;
    35+        size_t num_sent{0};
    36+        std::optional<NodeClock::time_point> last_sent{};
    37+        size_t num_peer_reception_acks{0};
    38+        std::optional<NodeClock::time_point> last_peer_reception_ack{};
    39+    };
    


    vasild commented at 9:41 am on January 21, 2026:

    It would be useful to show also the addresses the transaction was sent to and some additional information. Like so:

     0[
     1    {
     2        "txid": "tx1",
     3        "sent": [
     4            {
     5                "address": "aaa.onion:8333",
     6                "begin": "2026.01...", // time when picked for sending
     7                "end": "2026.01..." // time when reception was confirmed by the peer
     8            },
     9            {
    10                "address": "bbb.onion:8333",
    11                "begin": "2026.01...",
    12                "end": "2026.01..."
    13            }
    14        ],
    15        "received": {
    16            "address": "1.2.3.4:8333", // received back from the network from this peer
    17            "when": "2026.01..." // at this time
    18        }
    19    }   
    20]
    

    This would mean to not immediately drop the statistics when we receive the transaction back from the network, but keep the stats for some time, in order to be able to show them to the user. Maybe keep this info for e.g. 24 hours or better, keep the last N entries.

    PS as a side effect, this would make it possible to see the round-trip time through the network.


    andrewtoth commented at 5:02 pm on January 24, 2026:

    Yes, this information would be interesting to provide. I will add the extra sent information.

    Maybe keep this info for e.g. 24 hours or better, keep the last N entries.

    This raises an interesting point about how we should store this information. I’m curious about your thoughts on #34322 as well @vasild.

    Perhaps we introduce a new option privatebroadcast_persist or something, that when set keeps track of all private broadcasts indefinitely. This would let us show the received information above, but if turned off it would alleviate the concerns raised in #34322.


    andrewtoth commented at 3:07 am on January 28, 2026:

    Re: your suggestions here #34329#pullrequestreview-3706202946.

    I’ve implemented them. This is a bit tricky, so I tried to break up the changes to do that in multiple commits. I think the way it works now will be compatible with your suggestions in #34322 as well, since we prune oldest finished txs now when adding a new one. We could just prune txs that are expired whenever we call GetBroadcastInfo there.

  11. in test/functional/p2p_private_broadcast.py:423 in ac6c2bd064
    420@@ -395,8 +421,8 @@ def run_test(self):
    421         with tx_originator.busy_wait_for_debug_log(expected_msgs=[rebroadcast_msg.encode()]):
    422             tx_originator.setmocktime(int(time.time()) + delta)
    423             tx_originator.mockscheduler(delta)
    424-        self.check_broadcasts("Rebroadcast", txs[1], 1, skip_destinations)
    425         tx_originator.setmocktime(0) # Let the clock tick again (it will go backwards due to this).
    426+        self.check_broadcasts("Rebroadcast", txs[1], 1, skip_destinations)
    


    vasild commented at 9:55 am on January 21, 2026:
    Why reorder this?

    andrewtoth commented at 6:49 pm on January 24, 2026:

    This was causing a consistent CI failure on macOS native in this PR. I’m not sure why it isn’t reproducing in other PRs or other CI jobs. The mocktime being advanced causes the node to timeout a peer that was opened since it has been advanced more than 180 seconds in check_broadcasts. This assertion fails since tx and ping are not received.

    0            assert_equal(peer.message_count, {
    1                "version": 1,
    2                "verack": 1,
    3                "inv": 1,
    4                "tx": 1,
    5                "ping": 1
    6            })
    

    I don’t know exactly why a connection is open before we reach there though. Could possibly be that the job is slow enough to cause a real 2-3 minute rebroadcast before we set mocktime. I’m still investigating, but if you have any insight please let me know.


    andrewtoth commented at 6:30 pm on January 25, 2026:
    I removed the reorder. Let’s see what CI says.
  12. in src/net_processing.h:127 in ac6c2bd064
    119@@ -118,6 +120,12 @@ class PeerManager : public CValidationInterface, public NetEventsInterface
    120     /** Get peer manager info. */
    121     virtual PeerManagerInfo GetInfo() const = 0;
    122 
    123+    /** Get info about transactions currently being privately broadcast. */
    124+    virtual std::vector<PrivateBroadcast::TxBroadcastInfo> GetPrivateBroadcastInfo() const = 0;
    125+
    126+    /** Abort private broadcast attempts for a transaction (identified by txid or wtxid). */
    127+    virtual std::vector<CTransactionRef> AbortPrivateBroadcast(const uint256& id) = 0;
    


    vasild commented at 10:01 am on January 21, 2026:
    Would be good to document the return value. It may not be immediately obvious why it is a vector.
  13. in src/rpc/mempool.cpp:234 in ac6c2bd064
    229+            const NodeContext& node{EnsureAnyNodeContext(request.context)};
    230+            PeerManager& peerman{EnsurePeerman(node)};
    231+
    232+            const auto removed_txs{peerman.AbortPrivateBroadcast(id)};
    233+            if (removed_txs.empty()) {
    234+                throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Transaction not in private broadcast queue");
    


    vasild commented at 10:03 am on January 21, 2026:
    Could point the user here to go look at the getprivatebroadcastinfo RPC to see what is in the queue.
  14. vasild approved
  15. vasild commented at 10:13 am on January 21, 2026: contributor

    ACK ac6c2bd06485ff6e7ed538a234f20491d3cf47cb

    Nice and straight forward, thanks!

    I think we can do better by showing more info, but this does not make the current PR worse.

  16. DrahtBot removed the label CI failed on Jan 22, 2026
  17. andrewtoth force-pushed on Jan 25, 2026
  18. andrewtoth commented at 6:32 pm on January 25, 2026: contributor

    Thank you for your review @vasild. I have taken all your suggestions except tracking the “received” in #34329 (review), since I’m not sure yet how we should store info after we get the transaction back.

    git diff ac6c2bd06485ff6e7ed538a234f20491d3cf47cb..61078dab77c86a26b38afc3b1bb2ab97b8263fb0

  19. andrewtoth force-pushed on Jan 25, 2026
  20. DrahtBot added the label CI failed on Jan 25, 2026
  21. DrahtBot commented at 7:10 pm on January 25, 2026: contributor

    🚧 At least one of the CI tasks failed. Task test max 6 ancestor commits: https://github.com/bitcoin/bitcoin/actions/runs/21337454291/job/61411547620 LLM reason (✨ experimental): Compilation failed: PickTxForSend called with only 1 argument in private_broadcast_tests.cpp, missing the will_send_to_address parameter.

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  22. DrahtBot removed the label CI failed on Jan 25, 2026
  23. in src/test/private_broadcast_tests.cpp:133 in ccf91e48a3 outdated
    124+        const auto& unconfirmed_tx{*it_unconfirmed};
    125+
    126+        BOOST_CHECK_EQUAL(confirmed_tx.peers.size(), 1);
    127+        BOOST_CHECK_EQUAL(unconfirmed_tx.peers.size(), 1);
    128+        BOOST_CHECK(confirmed_tx.peers[0].received.has_value());
    129+        BOOST_CHECK(!unconfirmed_tx.peers[0].received.has_value());
    


    danielabrozzoni commented at 11:10 am on January 26, 2026:

    nit: we could also check that the node addresses match. I took a stab at adding this:

     0diff --git a/src/test/private_broadcast_tests.cpp b/src/test/private_broadcast_tests.cpp
     1index 4dbc768d74..9a86137c02 100644
     2--- a/src/test/private_broadcast_tests.cpp
     3+++ b/src/test/private_broadcast_tests.cpp
     4@@ -30,7 +30,9 @@ BOOST_AUTO_TEST_CASE(basic)
     5 
     6     PrivateBroadcast pb;
     7     const NodeId recipient1{1};
     8-    const CService addr1{};
     9+    in_addr ipv4Addr;
    10+    ipv4Addr.s_addr = 0xa0b0c001;
    11+    const CService addr1(ipv4Addr, 1111);
    12 
    13     // No transactions initially.
    14     BOOST_CHECK(!pb.PickTxForSend(/*will_send_to_nodeid=*/recipient1, /*will_send_to_address=*/addr1).has_value());
    15@@ -71,7 +73,7 @@ BOOST_AUTO_TEST_CASE(basic)
    16 
    17     // A second pick must return the other transaction.
    18     const NodeId recipient2{2};
    19-    const CService addr2{};
    20+    const CService addr2(ipv4Addr, 2222);
    21     const auto tx_for_recipient2{pb.PickTxForSend(/*will_send_to_nodeid=*/recipient2, /*will_send_to_address=*/addr2).value()};
    22     BOOST_CHECK(tx_for_recipient2 == tx1 || tx_for_recipient2 == tx2);
    23     BOOST_CHECK_NE(tx_for_recipient1, tx_for_recipient2);
    24@@ -124,7 +126,9 @@ BOOST_AUTO_TEST_CASE(basic)
    25         const auto& unconfirmed_tx{*it_unconfirmed};
    26 
    27         BOOST_CHECK_EQUAL(confirmed_tx.peers.size(), 1);
    28+        BOOST_CHECK_EQUAL(confirmed_tx.peers[0].address.ToStringAddrPort(), addr1.ToStringAddrPort());
    29         BOOST_CHECK_EQUAL(unconfirmed_tx.peers.size(), 1);
    30+        BOOST_CHECK_EQUAL(unconfirmed_tx.peers[0].address.ToStringAddrPort(), addr2.ToStringAddrPort());
    31         BOOST_CHECK(confirmed_tx.peers[0].received.has_value());
    32         BOOST_CHECK(!unconfirmed_tx.peers[0].received.has_value());
    33     }
    

    andrewtoth commented at 3:04 am on January 28, 2026:
    Taken, added you as co-author.
  24. in src/rpc/mempool.cpp:222 in 2ac1cc811a outdated
    217+            {
    218+                {RPCResult::Type::ARR, "removed_transactions", "Transactions removed from the private broadcast queue",
    219+                    {
    220+                        {RPCResult::Type::OBJ, "", "",
    221+                            {
    222+                                {RPCResult::Type::STR_HEX, "txid", "The transaction hash in hex"},
    


    danielabrozzoni commented at 11:50 am on January 26, 2026:
    I think it would be useful to show how many peers the transaction was sent to and how many acknowledged receipt, so users are not surprised if a transaction is later mined after aborting a private broadcast. Happy to work on this in a follow up PR if you think that makes sense :)

    andrewtoth commented at 3:04 am on January 28, 2026:
    Yes, I think this PR is big enough now as it is. A follow up to add this would be great.
  25. danielabrozzoni approved
  26. danielabrozzoni commented at 12:53 pm on January 26, 2026: member

    tACK 61078dab77c86a26b38afc3b1bb2ab97b8263fb0

    Code looks good to me, I left a couple of non-blocking nits.

    I tested locally using a signet node, I tried broadcasting transactions, seeing the confirmation/reception times and aborting the broadcast.

  27. DrahtBot requested review from vasild on Jan 26, 2026
  28. in src/rpc/mempool.cpp:161 in 61078dab77
    156+                                {RPCResult::Type::ARR, "peers", "Per-peer send and acknowledgment information for this transaction",
    157+                                    {
    158+                                        {RPCResult::Type::OBJ, "", "",
    159+                                            {
    160+                                                {RPCResult::Type::STR, "address", "The address of the peer to which the transaction was sent"},
    161+                                                {RPCResult::Type::NUM_TIME, "sent", "The time this transaction was sent to this peer via private broadcast (seconds since epoch)"},
    


    vasild commented at 2:07 pm on January 26, 2026:

    “was picked for sending” (or selected) would be more accurate than “was sent”.

    0                                                {RPCResult::Type::NUM_TIME, "sent", "The time this transaction was picked for sending to this peer via private broadcast (seconds since epoch)"},
    

    andrewtoth commented at 3:04 am on January 28, 2026:
    Done, added you as co-author.
  29. vasild approved
  30. vasild commented at 2:24 pm on January 26, 2026: contributor

    ACK 61078dab77c86a26b38afc3b1bb2ab97b8263fb0

    … except tracking the “received” in #34329 (review), since I’m not sure yet how we should store info after we get the transaction back

    I think at this point, within the scope of this PR, we could keep the transactions in the same place - PrivateBroadcast::m_transactions:

    • Don’t delete entries from m_transactions when they are received back after a round-trip through the network - rename Remove() to e.g. StopBroadcasting(). Same for when giving up on stale transactions that don’t make it to the mempool anymore or manually abandoned by the user.
    • Make sure those “stopped” entries are not considered by the methods PickTxForSend(), HavePendingTransactions() and GetStale().
    • Add some cap to the size of m_transactions.
    • Add a status field in the RPC output per transaction, which could be “success: received back from 1.2.3.4:8333”, “abandoned: not eligible for our mempool: xyz”, “abandoned by the user via RPC”.
    • Plus a timestamp of the status field.
  31. andrewtoth force-pushed on Jan 28, 2026
  32. andrewtoth force-pushed on Jan 28, 2026
  33. andrewtoth commented at 3:03 am on January 28, 2026: contributor

    @danielabrozzoni @vasild thank you for your reviews. I have taken most of your suggestions. Also rebased due to #34267.

    New commits added to faciliate extra information in getprivatebroadcastinfo:

    • Rename Remove to StopBroadcasting and pass either the node’s address if received or a message if aborted. 7a83e570fa94bc572233bf0f3c62f7b010e57e55
    • Store private broadcast transactions even after they are stopped, storing the received from node address or message, and a timestamp. adf16cb57e880c77415a5ee47017150136e68cac
    • Prune finished transactions if there are move than 1000. 4194ab3c93c1d87ecdacbaef7da91f280edd46c3

    git range-diff 22bde74d1d8f861323eabb8dc60401bbf1226544..61078dab77c86a26b38afc3b1bb2ab97b8263fb0 c0e6556e4f51b454c3f6e9069044761c34e99f81..37f7e5577853f3655de08710e688256e144e3cf1

  34. andrewtoth force-pushed on Jan 28, 2026
  35. DrahtBot added the label CI failed on Jan 28, 2026
  36. DrahtBot commented at 3:09 am on January 28, 2026: contributor

    🚧 At least one of the CI tasks failed. Task lint: https://github.com/bitcoin/bitcoin/actions/runs/21423156818/job/61686652517 LLM reason (✨ experimental): Trailing whitespace detected in src/private_broadcast.h, causing the lint check to fail.

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  37. DrahtBot removed the label CI failed on Jan 28, 2026
  38. in src/net_processing.cpp:1871 in db23ae3935
    1866+    to_stop.reserve(snapshot.size());
    1867+    for (const auto& entry : snapshot) {
    1868+        if (entry.tx->GetHash().ToUint256() == id || entry.tx->GetWitnessHash().ToUint256() == id) {
    1869+            to_stop.push_back(entry.tx);
    1870+        }
    1871+    }
    


    w0xlt commented at 8:50 am on February 1, 2026:

    The function iterates over all transactions including finished ones from GetBroadcastInfo(), but only active (non-finished) transactions are abortable. to_stop.reserve(snapshot.size()) over-reserves since it is including finished transactions.

    0    for (const auto& entry : snapshot) {
    1      if (entry.final_state.has_value()) continue; // Skip already finished
    2      if (entry.tx->GetHash().ToUint256() == id || entry.tx->GetWitnessHash().ToUint256() == id) {
    3        to_stop.push_back(entry.tx);
    4      }
    5    }
    

    andrewtoth commented at 7:59 pm on February 1, 2026:
    Done, also collapsed the 2 loops into one loop.
  39. in src/rpc/mempool.cpp:242 in db23ae3935 outdated
    254+            const NodeContext& node{EnsureAnyNodeContext(request.context)};
    255+            PeerManager& peerman{EnsurePeerman(node)};
    256+
    257+            const auto removed_txs{peerman.AbortPrivateBroadcast(id)};
    258+            if (removed_txs.empty()) {
    259+                throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Transaction not in private broadcast queue. Check getprivatebroadcastinfo.");
    


    w0xlt commented at 8:56 am on February 1, 2026:
    RPC_INVALID_ADDRESS_OR_KEY is typically used for Bitcoin address/key validation errors. Maybe RPC_INVALID_PARAMETER would be more accurate.

    andrewtoth commented at 7:39 pm on February 1, 2026:

    This error is typically used when a transaction or block is not found by hash. In mempool.cpp there are several that throw with error message Transaction not in mempool. In blockchain.cpp, there are several that throw with message Block not found. In rawtransaction.cpp it is returned for getrawtransaction when the tx is not found.

    I think this is the correct error code to use in this case.

  40. andrewtoth force-pushed on Feb 1, 2026
  41. andrewtoth commented at 7:59 pm on February 1, 2026: contributor

    Thank you for your review @w0xlt. Addressed #34329 (review).

    git diff db23ae39358168823593d28c3114590900d7e887..5d3c79a96fab2018a3e13bbe8d6ca6080e38ec91.

  42. in src/net_processing.cpp:1873 in 5d3c79a96f
    1868+        if (entry.final_state.has_value()) continue;
    1869+        if (entry.tx->GetHash().ToUint256() != id && entry.tx->GetWitnessHash().ToUint256() != id) continue;
    1870+        to_stop.push_back(entry.tx);
    1871+        const auto peer_acks{m_tx_for_private_broadcast.StopBroadcasting(entry.tx, "Manually aborted via abortprivatebroadcast RPC")};
    1872+        if (!peer_acks.has_value()) continue;
    1873+        if (NUM_PRIVATE_BROADCAST_PER_TX > peer_acks.value()) {
    


    w0xlt commented at 9:21 am on February 2, 2026:

    Only add entry.tx to to_stop if StopBroadcasting returns a non-nullopt value. Another thread may have already stopped it.

    0        const auto peer_acks{m_tx_for_private_broadcast.StopBroadcasting(entry.tx, "Manually aborted via abortprivatebroadcast RPC")};
    1        if (!peer_acks.has_value()) continue;
    2        to_stop.push_back(entry.tx);
    3        if (NUM_PRIVATE_BROADCAST_PER_TX > peer_acks.value()) {
    
  43. in test/functional/p2p_private_broadcast.py:406 in 5d3c79a96f outdated
    401+        assert_equal(len(info), 1)
    402+        assert_equal(info[0]["final_state"]["aborted"], "Manually aborted via abortprivatebroadcast RPC")
    403+        # The abort time is recorded on the node; allow a little slack around the RPC call.
    404+        assert abort_time_before - 5 <= info[0]["final_state"]["time"] <= abort_time_after + 5
    405+        assert "received_from" not in info[0]["final_state"]
    406+
    


    w0xlt commented at 9:21 am on February 2, 2026:
    0        self.log.info("Checking abortprivatebroadcast fails for non-existent transaction")
    1        assert_raises_rpc_error(-5, "Transaction not in private broadcast queue",
    2                                tx_originator.abortprivatebroadcast, "0" * 64)
    
  44. in src/test/private_broadcast_tests.cpp:170 in 5d3c79a96f
    170 
    171-    BOOST_CHECK(!pb.PickTxForSend(/*will_send_to_nodeid=*/nonexistent_recipient).has_value());
    172+    // Fill up to the cap with other finished transactions that all have newer finish times.
    173+    SetMockTime(Now<NodeSeconds>() + 1h);
    174+    for (uint32_t i = 0; i < MAX_STORED_TRANSACTIONS - 1; ++i) {
    175+        auto tx{MakeDummyTx(/*id=*/i + 2, /*num_witness=*/0)};
    


    vasild commented at 11:12 am on February 2, 2026:

    Maybe would be more readable:

    0    for (uint32_t i = 2; i <= MAX_STORED_TRANSACTIONS; ++i) {
    1        auto tx{MakeDummyTx(/*id=*/i, /*num_witness=*/0)};
    
  45. in src/test/private_broadcast_tests.cpp:156 in 5d3c79a96f
    155+    BOOST_CHECK(!pb.PickTxForSend(/*will_send_to_nodeid=*/nonexistent_recipient, /*will_send_to_address=*/CService{}).has_value());
    156+}
    157+
    158+BOOST_AUTO_TEST_CASE(prune_finished)
    159+{
    160+    SetMockTime(Now<NodeSeconds>());
    


    vasild commented at 11:14 am on February 2, 2026:

    The pattern:

    0SetMockTime(Now<NodeSeconds>());
    1SetMockTime(Now<NodeSeconds>() - 1h);
    2SetMockTime(Now<NodeSeconds>() + 1h);
    

    while unlikely makes it possible something unexpected to happen if 1h passes in between (👀). Would be more robust to assign to a variable, whose value will not change during the test:

    0const auto start = Now<NodeSeconds>();
    1SetMockTime(start);
    2SetMockTime(start - 1h);
    3SetMockTime(start + 1h);
    
  46. in src/test/private_broadcast_tests.cpp:223 in 5d3c79a96f
    223+        BOOST_CHECK_EQUAL(it->final_state->result.error(), "aborted");
    224+        time_aborted = TicksSinceEpoch<std::chrono::seconds>(it->final_state->time);
    225+    }
    226+
    227+    // Further abort attempts should return nullopt and must not overwrite the aborted final state.
    228+    SetMockTime(Now<NodeSeconds>() + 1s);
    


    vasild commented at 11:23 am on February 2, 2026:
    Same here. If the system clock, for some reason (ntp adjustment?) goes back for 1 second, then this will have unintended result.
  47. in src/net_processing.cpp:1661 in 5d3c79a96f
    1657@@ -1658,7 +1658,7 @@ void PeerManagerImpl::ReattemptPrivateBroadcast(CScheduler& scheduler)
    1658                 LogDebug(BCLog::PRIVBROADCAST, "Giving up broadcast attempts for txid=%s wtxid=%s: %s",
    1659                          stale_tx->GetHash().ToString(), stale_tx->GetWitnessHash().ToString(),
    1660                          mempool_acceptable.m_state.ToString());
    1661-                m_tx_for_private_broadcast.Remove(stale_tx);
    1662+                m_tx_for_private_broadcast.StopBroadcasting(stale_tx, mempool_acceptable.m_state.ToString());
    


    vasild commented at 11:50 am on February 2, 2026:
    m_state.ToString() could be quite short, e.g. “tx-size”, “transaction failed” or “bad-txns-inputs-missingorspent”, etc. Maybe prefix this with “Not acceptable to our mempool: %s”.
  48. in src/private_broadcast.h:89 in 5d3c79a96f outdated
    92+     * @retval nullopt The transaction was not in the storage, or was already stopped.
    93+     */
    94+    std::optional<size_t> StopBroadcasting(const CTransactionRef& tx, std::string aborted)
    95+        EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)
    96+    {
    97+        return StopBroadcasting(tx, util::Unexpected<std::string>{std::move(aborted)});
    


    vasild commented at 12:00 pm on February 2, 2026:
    It is a good convention to pass in parameters as const reference. I think passing by value and then moving from the temporary might be as effective, but I find the const reference parameters more readable.

    andrewtoth commented at 5:13 am on February 3, 2026:
    I think std::string_view has made passing const std::string& obsolete. In this case we want to own the std::string, so passing by value is appropriate. The callers can move the string into the function or a temporary is automatically moved. Using a const ref here would always require making an extra copy.

    andrewtoth commented at 3:51 pm on February 5, 2026:
    Actually, I think these should be passed as rvalue refs. Updated.
  49. in src/private_broadcast.h:74 in 5d3c79a96f outdated
    77-    std::optional<size_t> Remove(const CTransactionRef& tx)
    78-        EXCLUSIVE_LOCKS_REQUIRED(!m_mutex);
    79+    std::optional<size_t> StopBroadcasting(const CTransactionRef& tx, const CService& received_from)
    80+        EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)
    81+    {
    82+        return StopBroadcasting(tx, util::Expected<CService, std::string>{received_from});
    


    vasild commented at 12:02 pm on February 2, 2026:
    I find this usage of util::Expected unusual. Is not std::variant more suitable for this?

    andrewtoth commented at 5:13 am on February 3, 2026:
    util::Expected is a std::variant under the hood. It is generally used for when you have an expected happy path result or an unexpected error, which I think is appropriate for storing this data.

    vasild commented at 5:12 pm on February 3, 2026:
    I find this very confusing because I see nothing “expected” or “unexpected” about stopping a transaction by “address” (received back) or by “string” (manually aborted, or not suitable for our mempool). I mean both stop-methods are equally (un)expected to me.

    andrewtoth commented at 11:28 pm on February 3, 2026:
    When a tx gets submitted for private broadcast, we expect that it will find its way to a peer and make it back to our mempool. If it gets stale and then has a mempool conflict, something unexpected has happened. When it was initially submitted the mempool checks passed. If it is submitted, a user intended to broadcast the tx and expects to have it make its way back to us. They could unexpectedly decide to abort the tx before this happens.

    w0xlt commented at 8:14 pm on February 5, 2026:

    util::Expected<CService, std::string> is being used here as an union (received-from-address vs. abort-reason-string) rather than for its intended success/error semantics.

    A std::variant<CService, std::string> would better communicate intent. The current approach works but can confuse future maintainers.


    andrewtoth commented at 1:24 am on February 6, 2026:
    Updated to a std::variant.
  50. in src/private_broadcast.cpp:36 in 5d3c79a96f
    32+        struct FinishedEntry {
    33+            NodeClock::time_point time;
    34+            CTransactionRef tx;
    35+        };
    36+        std::vector<FinishedEntry> finished;
    37+        finished.reserve(m_transactions.size());
    


    vasild commented at 12:35 pm on February 2, 2026:
    This micro optimization might overshoot and turn out to have negative performance impact. Maybe just drop it.
  51. in src/private_broadcast.cpp:39 in 5d3c79a96f outdated
    36+        std::vector<FinishedEntry> finished;
    37+        finished.reserve(m_transactions.size());
    38+        for (const auto& [txref, state] : m_transactions) {
    39+            if (const auto final_state{state.final_state}) {
    40+                finished.push_back(FinishedEntry{.time = state.final_state->time, .tx = txref});
    41+            }
    


    vasild commented at 12:35 pm on February 2, 2026:
    The variable final_state is not used?
  52. in src/private_broadcast.cpp:29 in 5d3c79a96f outdated
    25+        it->second.sent_to.clear();
    26+        inserted = true;
    27+    }
    28+
    29+    // Bound memory usage by pruning oldest finished transactions. If we still exceed the cap
    30+    // but all transactions are unfinished, that's ok.
    


    vasild commented at 12:44 pm on February 2, 2026:
    I think the current code is fine, but just mentioning: trimming only during “add” could leave us with more than 1000 finalized transactions. For example, if 1100 are added, all of them pending, then no new ones are added (no calls to Add(), so the trimming code is never executed anymore) and one by one those 1100 are finalized. So, in theory, to enforce this, the trimming code should be run also when a state changes from non-finalized to finalized. Maybe an overkill, feel free to ignore.

    andrewtoth commented at 5:13 am on February 3, 2026:
    Good observation. I think it’s unlikely to happen, and not a big deal if it does. If there are already 1100 entries, then keeping them there until another is added is not making anything worse. This would also require a user to add a lot of txs at once, and then stop adding altogether. I’m not sure if that’s a common usage pattern. We can deal with this in a follow-up when we add persistence to this data.
  53. andrewtoth force-pushed on Feb 3, 2026
  54. andrewtoth commented at 5:14 am on February 3, 2026: contributor

    Thank you for your reviews @w0xlt and @vasild. I have taken most of your suggestions, and added you both as co-authors.

    git diff 5d3c79a96fab2018a3e13bbe8d6ca6080e38ec91..e0c75536ce6e1f9392c50ebf022a1840a46d0dfd

  55. andrewtoth force-pushed on Feb 5, 2026
  56. andrewtoth commented at 3:52 pm on February 5, 2026: contributor

    Addressed #34329 (review). Pass stop reason strings as rvalue refs.

    git diff e0c75536ce6e1f9392c50ebf022a1840a46d0dfd..844e08c0677f04288ed0cb38d05c68e09bae7e67

  57. achow101 added this to the milestone 31.0 on Feb 5, 2026
  58. andrewtoth force-pushed on Feb 5, 2026
  59. andrewtoth force-pushed on Feb 6, 2026
  60. andrewtoth commented at 1:41 am on February 6, 2026: contributor

    Addressed #34329 (review). Thanks @vasild and @w0xlt.

    git diff 844e08c0677f04288ed0cb38d05c68e09bae7e67..0649a6a01ff74659af432443ae6e1f60606c844a

  61. DrahtBot added the label CI failed on Feb 6, 2026
  62. l0rinc commented at 7:32 pm on February 7, 2026: contributor

    Sending a transaction via private broadcast does not have any way for a user to track the status of the transaction before it gets returned by another peer.

    +1, I understand why this is important to get into the same release as the private broadcast, but not sure about the abortprivatebroadcast and related refactors, I think those could be done after the freeze as well and in separate PRs, since this change is a bit bigger than I anticipated. Could you maybe split it into an urgent minimal PR and follow-ups that aren’t necessary for this release?

  63. net: Store recipient node address in private broadcast 573bb542be
  64. net: Add PrivateBroadcast::GetBroadcastInfo
    Co-authored-by: Daniela Brozzoni <danielabrozzoni@protonmail.com>
    cc10837fc1
  65. rpc: Add getprivatebroadcastinfo
    Co-authored-by: Vasil Dimov <vd@freebsd.org>
    115e117b83
  66. test: Cover getprivatebroadcastinfo in p2p_private_broadcast a10e5a176e
  67. rpc: Add abortprivatebroadcast 043d9cde4d
  68. test: Cover abortprivatebroadcast in p2p_private_broadcast 42f8a10d5a
  69. doc: Mention private broadcast RPCs in release notes a034fbd7d9
  70. andrewtoth force-pushed on Feb 10, 2026
  71. andrewtoth commented at 3:09 pm on February 10, 2026: contributor

    @l0rinc thanks for that feedback. Indeed, the changes made to persist finished private broadcast transactions in memory are difficult to review. I do feel that abortprivatebroadcast is an important RPC to have in the upcoming release though. There can be many cases where a transaction might never return to a node’s mempool, and right now the only way to stop opening connections in these cases is to restart the node.

    I’ve reverted the changes up to 61078dab77c86a26b38afc3b1bb2ab97b8263fb0, which had 2 ACKs from @danielabrozzoni and @vasild. I’ve rebased due to #34267, and addressed comments #34329 (review) and #34329 (review). I think this makes it much easier to review, and we can figure out persisting finished transactions in memory or on disk in a follow-up after v31.

    git range-diff 22bde74d1d8f861323eabb8dc60401bbf1226544..61078dab77c86a26b38afc3b1bb2ab97b8263fb0 64294c89094d5ab10d87236729cc267fde0a24ca..a034fbd7d907da99726c20e6bf09239d2dfd711b

  72. DrahtBot removed the label CI failed on Feb 10, 2026
  73. nervana21 commented at 0:03 am on February 11, 2026: contributor
    Concept ACK
  74. in src/net_processing.cpp:1870 in a034fbd7d9
    1865+std::vector<CTransactionRef> PeerManagerImpl::AbortPrivateBroadcast(const uint256& id)
    1866+{
    1867+    const auto snapshot{m_tx_for_private_broadcast.GetBroadcastInfo()};
    1868+    std::vector<CTransactionRef> to_remove;
    1869+    to_remove.reserve(snapshot.size());
    1870+    for (const auto& entry : snapshot) {
    


    l0rinc commented at 10:33 am on February 11, 2026:

    I think there might be a small race here : we take a snapshot and then remove in a separate pass, but we don’t hold one lock across both operations.

    If another thread removes a tx between GetBroadcastInfo() and Remove(), this method can currently return a tx that this call did not actually remove. Given the contract (Transactions removed from the private broadcast queue), should we return only txs removed by this call (or otherwise relax the docs)?

    Maybe something like:

     0std::vector<CTransactionRef> PeerManagerImpl::AbortPrivateBroadcast(const uint256& id)
     1{
     2    const auto snapshot{m_tx_for_private_broadcast.GetBroadcastInfo()};
     3    std::vector<CTransactionRef> removed_txs;
     4    removed_txs.reserve(snapshot.size());
     5
     6    size_t connections_cancelled{0};
     7    for (const auto& [tx, _] : snapshot) {
     8        if (tx->GetHash().ToUint256() != id && tx->GetWitnessHash().ToUint256() != id) continue;
     9        if (const auto peer_acks{m_tx_for_private_broadcast.Remove(tx)}) {
    10            removed_txs.push_back(tx);
    11            if (NUM_PRIVATE_BROADCAST_PER_TX > *peer_acks) {
    12                connections_cancelled += (NUM_PRIVATE_BROADCAST_PER_TX - *peer_acks);
    13            }
    14        }
    15    }
    16    m_connman.m_private_broadcast.NumToOpenSub(connections_cancelled);
    17
    18    return removed_txs;
    19}
    
  75. in src/net_processing.cpp:1886 in a034fbd7d9
    1881+            connections_cancelled += (NUM_PRIVATE_BROADCAST_PER_TX - peer_acks.value());
    1882+        }
    1883+    }
    1884+
    1885+    if (connections_cancelled > 0) {
    1886+        m_connman.m_private_broadcast.NumToOpenSub(connections_cancelled);
    


    l0rinc commented at 10:36 am on February 11, 2026:
    shouldn’t NumToOpenSub handle connections_cancelled > 0 explicitly? It seems to handle it implicitly, so the guard may not be needed.
  76. in test/functional/p2p_private_broadcast.py:394 in a034fbd7d9
    389+        assert_equal(len(abort_res["removed_transactions"]), 1)
    390+        assert_equal(abort_res["removed_transactions"][0]["txid"], tx_abort["txid"])
    391+        assert_equal(abort_res["removed_transactions"][0]["wtxid"], tx_abort["wtxid"])
    392+        assert_equal(abort_res["removed_transactions"][0]["hex"].lower(), tx_abort["hex"].lower())
    393+        assert all(t["wtxid"] != tx_abort["wtxid"] for t in tx_originator.getprivatebroadcastinfo()["transactions"])
    394+
    


    l0rinc commented at 10:46 am on February 11, 2026:

    Might make sense to verify that abortprivatebroadcast fails for missing ids:

    0        self.log.info("Checking abortprivatebroadcast fails for non-existent transaction")
    1        assert_raises_rpc_error(
    2            -5,
    3            "Transaction not in private broadcast queue",
    4            tx_originator.abortprivatebroadcast,
    5            "0" * 64,
    6        )
    
  77. in src/test/private_broadcast_tests.cpp:57 in 573bb542be
    54     BOOST_CHECK(tx_for_recipient1 == tx1 || tx_for_recipient1 == tx2);
    55 
    56     // A second pick must return the other transaction.
    57     const NodeId recipient2{2};
    58-    const auto tx_for_recipient2{pb.PickTxForSend(/*will_send_to_nodeid=*/recipient2).value()};
    59+    const CService addr2{};
    


    l0rinc commented at 11:53 am on February 11, 2026:

    573bb542be80b63b1713a0b76bedaa5e37c3783f Could we use distinct non-default addresses (or at least distinct ports) for addr1/addr2 here? With both as CService{}, this test won’t catch address propagation mixups, something like:

     0diff --git a/src/test/private_broadcast_tests.cpp b/src/test/private_broadcast_tests.cpp
     1--- a/src/test/private_broadcast_tests.cpp	(revision 573bb542be80b63b1713a0b76bedaa5e37c3783f)
     2+++ b/src/test/private_broadcast_tests.cpp	(date 1770810667358)
     3@@ -28,8 +28,9 @@
     4     SetMockTime(Now<NodeSeconds>());
     5 
     6     PrivateBroadcast pb;
     7-    const NodeId recipient1{1};
     8-    const CService addr1{};
     9+    constexpr NodeId recipient1{1};
    10+    constexpr in_addr ipv4_addr{0x01010101}; // Dummy IPv4 address: 1.1.1.1.
    11+    const CService addr1{ipv4_addr, /*port=*/1111};
    12 
    13     // No transactions initially.
    14     BOOST_CHECK(!pb.PickTxForSend(/*will_send_to_nodeid=*/recipient1, /*will_send_to_address=*/addr1).has_value());
    15@@ -53,13 +54,13 @@
    16     BOOST_CHECK(tx_for_recipient1 == tx1 || tx_for_recipient1 == tx2);
    17 
    18     // A second pick must return the other transaction.
    19-    const NodeId recipient2{2};
    20-    const CService addr2{};
    21+    constexpr NodeId recipient2{2};
    22+    const CService addr2{ipv4_addr, /*port=*/2222};
    23     const auto tx_for_recipient2{pb.PickTxForSend(/*will_send_to_nodeid=*/recipient2, /*will_send_to_address=*/addr2).value()};
    24     BOOST_CHECK(tx_for_recipient2 == tx1 || tx_for_recipient2 == tx2);
    25     BOOST_CHECK_NE(tx_for_recipient1, tx_for_recipient2);
    26 
    27-    const NodeId nonexistent_recipient{0};
    28+    constexpr NodeId nonexistent_recipient{0};
    29 
    30     // Confirm transactions <-> recipients mapping is correct.
    31     BOOST_CHECK(!pb.GetTxForNode(nonexistent_recipient).has_value());
    32@@ -92,7 +93,8 @@
    33     BOOST_CHECK_EQUAL(pb.Remove(tx_for_recipient2).value(), 0);
    34     BOOST_CHECK(!pb.Remove(tx_for_recipient2).has_value());
    35 
    36-    BOOST_CHECK(!pb.PickTxForSend(/*will_send_to_nodeid=*/nonexistent_recipient, /*will_send_to_address=*/CService{}).has_value());
    37+    const CService addr_nonexistent{ipv4_addr, /*port=*/3333};
    38+    BOOST_CHECK(!pb.PickTxForSend(/*will_send_to_nodeid=*/nonexistent_recipient, /*will_send_to_address=*/addr_nonexistent).has_value());
    39 }
    40 
    41 BOOST_AUTO_TEST_SUITE_END()
    

    That would simplify the next commit so that it can focus purely on GetBroadcastInfo changes

  78. in src/test/private_broadcast_tests.cpp:69 in cc10837fc1
    64+        const auto& info1{*it1};
    65+        const auto& info2{*it2};
    66+
    67+        BOOST_CHECK_EQUAL(info1.peers.size(), 0);
    68+        BOOST_CHECK_EQUAL(info2.peers.size(), 0);
    69+    }
    


    l0rinc commented at 12:05 pm on February 11, 2026:

    there’s a lot of repetition here alone and parts of this code block are repeated a few more times here - can we add a local helper lambdas to dedup these?

     0diff --git a/src/test/private_broadcast_tests.cpp b/src/test/private_broadcast_tests.cpp
     1--- a/src/test/private_broadcast_tests.cpp	(revision a2c339144982836e4f0773604b2d99c46dd143b8)
     2+++ b/src/test/private_broadcast_tests.cpp	(date 1770811873480)
     3@@ -52,21 +52,19 @@
     4     BOOST_REQUIRE(tx1->GetWitnessHash() != tx2->GetWitnessHash());
     5 
     6     BOOST_CHECK(pb.Add(tx2));
     7-
     8-    {
     9+    const auto find_tx_info{[](auto& infos, const CTransactionRef& tx) -> const PrivateBroadcast::TxBroadcastInfo& {
    10+        const auto it{std::ranges::find(infos, tx->GetWitnessHash(), [](const auto& info) { return info.tx->GetWitnessHash(); })};
    11+        BOOST_REQUIRE(it != infos.end());
    12+        return *it;
    13+    }};
    14+    const auto check_peer_counts{[&](size_t tx1_peer_count, size_t tx2_peer_count) {
    15         const auto infos{pb.GetBroadcastInfo()};
    16         BOOST_CHECK_EQUAL(infos.size(), 2);
    17+        BOOST_CHECK_EQUAL(find_tx_info(infos, tx1).peers.size(), tx1_peer_count);
    18+        BOOST_CHECK_EQUAL(find_tx_info(infos, tx2).peers.size(), tx2_peer_count);
    19+    }};
    20 
    21-        const auto it1{std::ranges::find_if(infos, [&](const auto& info) { return info.tx->GetWitnessHash() == tx1->GetWitnessHash(); })};
    22-        const auto it2{std::ranges::find_if(infos, [&](const auto& info) { return info.tx->GetWitnessHash() == tx2->GetWitnessHash(); })};
    23-        BOOST_REQUIRE(it1 != infos.end());
    24-        BOOST_REQUIRE(it2 != infos.end());
    25-        const auto& info1{*it1};
    26-        const auto& info2{*it2};
    27-
    28-        BOOST_CHECK_EQUAL(info1.peers.size(), 0);
    29-        BOOST_CHECK_EQUAL(info2.peers.size(), 0);
    30-    }
    31+    check_peer_counts(/*tx1_peer_count=*/0, /*tx2_peer_count=*/0);
    32 
    33     const auto tx_for_recipient1{pb.PickTxForSend(/*will_send_to_nodeid=*/recipient1, /*will_send_to_address=*/addr1).value()};
    34     BOOST_CHECK(tx_for_recipient1 == tx1 || tx_for_recipient1 == tx2);
    35@@ -78,20 +76,7 @@
    36     BOOST_CHECK(tx_for_recipient2 == tx1 || tx_for_recipient2 == tx2);
    37     BOOST_CHECK_NE(tx_for_recipient1, tx_for_recipient2);
    38 
    39-    {
    40-        const auto infos{pb.GetBroadcastInfo()};
    41-        BOOST_CHECK_EQUAL(infos.size(), 2);
    42-
    43-        const auto it1{std::ranges::find_if(infos, [&](const auto& info) { return info.tx->GetWitnessHash() == tx1->GetWitnessHash(); })};
    44-        const auto it2{std::ranges::find_if(infos, [&](const auto& info) { return info.tx->GetWitnessHash() == tx2->GetWitnessHash(); })};
    45-        BOOST_REQUIRE(it1 != infos.end());
    46-        BOOST_REQUIRE(it2 != infos.end());
    47-        const auto& info1{*it1};
    48-        const auto& info2{*it2};
    49-
    50-        BOOST_CHECK_EQUAL(info1.peers.size(), 1);
    51-        BOOST_CHECK_EQUAL(info2.peers.size(), 1);
    52-    }
    53+    check_peer_counts(/*tx1_peer_count=*/1, /*tx2_peer_count=*/1);
    54 
    55     const NodeId nonexistent_recipient{0};
    56 
    57@@ -114,23 +99,19 @@
    58     BOOST_CHECK(pb.DidNodeConfirmReception(recipient1));
    59     BOOST_CHECK(!pb.DidNodeConfirmReception(recipient2));
    60 
    61-    {
    62-        const auto infos{pb.GetBroadcastInfo()};
    63-        BOOST_CHECK_EQUAL(infos.size(), 2);
    64-
    65-        const auto it_confirmed{std::ranges::find_if(infos, [&](const auto& info) { return info.tx->GetWitnessHash() == tx_for_recipient1->GetWitnessHash(); })};
    66-        const auto it_unconfirmed{std::ranges::find_if(infos, [&](const auto& info) { return info.tx->GetWitnessHash() == tx_for_recipient2->GetWitnessHash(); })};
    67-        BOOST_REQUIRE(it_confirmed != infos.end());
    68-        BOOST_REQUIRE(it_unconfirmed != infos.end());
    69-        const auto& confirmed_tx{*it_confirmed};
    70-        const auto& unconfirmed_tx{*it_unconfirmed};
    71-
    72-        BOOST_CHECK_EQUAL(confirmed_tx.peers.size(), 1);
    73-        BOOST_CHECK_EQUAL(confirmed_tx.peers[0].address.ToStringAddrPort(), addr1.ToStringAddrPort());
    74-        BOOST_CHECK_EQUAL(unconfirmed_tx.peers.size(), 1);
    75-        BOOST_CHECK_EQUAL(unconfirmed_tx.peers[0].address.ToStringAddrPort(), addr2.ToStringAddrPort());
    76-        BOOST_CHECK(confirmed_tx.peers[0].received.has_value());
    77-        BOOST_CHECK(!unconfirmed_tx.peers[0].received.has_value());
    78+    const auto infos{pb.GetBroadcastInfo()};
    79+    BOOST_CHECK_EQUAL(infos.size(), 2);
    80+    {
    81+        const auto& [tx, peers]{find_tx_info(infos, tx_for_recipient1)};
    82+        BOOST_CHECK_EQUAL(peers.size(), 1);
    83+        BOOST_CHECK_EQUAL(peers[0].address.ToStringAddrPort(), addr1.ToStringAddrPort());
    84+        BOOST_CHECK(peers[0].received.has_value());
    85+    }
    86+    {
    87+        const auto& [tx, peers]{find_tx_info(infos, tx_for_recipient2)};
    88+        BOOST_CHECK_EQUAL(peers.size(), 1);
    89+        BOOST_CHECK_EQUAL(peers[0].address.ToStringAddrPort(), addr2.ToStringAddrPort());
    90+        BOOST_CHECK(!peers[0].received.has_value());
    91     }
    92 
    93     BOOST_CHECK_EQUAL(pb.GetStale().size(), 1);
    
  79. in src/private_broadcast.cpp:126 in cc10837fc1
    121+                .sent = status.picked,
    122+                .received = status.confirmed,
    123+            });
    124+        }
    125+        entries.push_back(std::move(info));
    126+    }
    


    l0rinc commented at 12:30 pm on February 11, 2026:

    nit: alternatively we could just construct the peers before attempting to add them to the info and filling it later:

    0    for (const auto& [tx, sent_to] : m_transactions) {
    1        std::vector<PeerSendInfo> peers;
    2        peers.reserve(sent_to.size());
    3        for (const auto& status : sent_to) {
    4            peers.emplace_back(PeerSendInfo{.address = status.address, .sent = status.picked, .received = status.confirmed});
    5        }
    6        entries.emplace_back(TxBroadcastInfo{.tx = tx, .peers = std::move(peers)});
    7    }
    

    or maybe even

    0    for (const auto& sent_to : m_transactions | std::views::values) {
    1        std::vector<PeerSendInfo> peers;
    2        peers.reserve(sent_to.size());
    3        std::ranges::transform(sent_to, std::back_inserter(peers), [](const auto& status) {
    4            return PeerSendInfo{.address = status.address, .sent = status.picked, .received = status.confirmed};
    5        });
    6    }
    
  80. in test/functional/p2p_private_broadcast.py:312 in a10e5a176e
    307+        assert_equal(len(pending), 1)
    308+        assert_equal(pending[0]["hex"].lower(), tx["hex"].lower())
    309+        peers = pending[0]["peers"]
    310+        assert len(peers) >= NUM_PRIVATE_BROADCAST_PER_TX
    311+        assert all("address" in p and "sent" in p for p in peers)
    312+        assert sum(1 for p in peers if "received" in p) >= NUM_PRIVATE_BROADCAST_PER_TX
    


    l0rinc commented at 1:47 pm on February 11, 2026:

    some people prefer to use the dedicated asserts that return better :

    0        assert_greater_than_or_equal(sum(1 for p in peers if "received" in p), NUM_PRIVATE_BROADCAST_PER_TX)
    
  81. in test/functional/p2p_private_broadcast.py:382 in a10e5a176e
    375@@ -366,6 +376,11 @@ def run_test(self):
    376         self.log.info("Waiting for normal broadcast to another peer")
    377         self.destinations[1]["node"].wait_for_inv([inv])
    378 
    379+        self.log.info("Checking getprivatebroadcastinfo no longer reports the transaction after it is received back")
    380+        pbinfo = tx_originator.getprivatebroadcastinfo()
    381+        pending = [t for t in pbinfo["transactions"] if t["txid"] == txs[0]["txid"] and t["wtxid"] == txs[0]["wtxid"]]
    382+        assert_equal(len(pending), 0)
    


    l0rinc commented at 1:51 pm on February 11, 2026:

    alternatively we can avoid stopping after the first match is found:

    0        assert not any(t["txid"] == txs[0]["txid"] and t["wtxid"] == txs[0]["wtxid"] for t in pbinfo["transactions"])
    
  82. l0rinc changes_requested
  83. l0rinc commented at 2:03 pm on February 11, 2026: contributor

    I went over the change, my biggest concern is the race in remove and the verbose tests and some minor commit churn - left examples for each suggestion to speed up the process.

    Concept ACK


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-02-12 00:13 UTC

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