1 #!/usr/bin/env python3
2 # Copyright (c) 2026-present The Bitcoin Core developers
3 # Distributed under the MIT software license, see the accompanying
4 # file COPYING or http://www.opensource.org/licenses/mit-license.php.
5 """Test the private-broadcast queue size cap: submissions beyond the cap are
6 rejected (the queue is not modified), rather than evicting existing entries.
7 """
8 9 from test_framework.test_framework import BitcoinTestFramework
10 from test_framework.util import assert_equal, assert_raises_rpc_error
11 from test_framework.wallet import MiniWallet
12 13 14 # Must match PrivateBroadcast::MAX_TRANSACTIONS
15 MAX_TRANSACTIONS = 10_000
16 OVER_CAP = 5
17 18 19 class PrivateBroadcastCapTest(BitcoinTestFramework):
20 def set_test_params(self):
21 self.num_nodes = 1
22 # -privatebroadcast is incompatible with the framework's default
23 # -connect=0; allow autoconnect (no actual peers will succeed though).
24 self.disable_autoconnect = False
25 self.extra_args = [[
26 "-privatebroadcast",
27 # Fake I2P reachability so the privatebroadcast startup precondition passes.
28 "-i2psam=127.0.0.1:1",
29 "-proxy=127.0.0.1:1",
30 ]]
31 32 def setup_network(self):
33 # Skip the framework's default connect_nodes loop. We have a single
34 # node and don't need any peer connections.
35 self.setup_nodes()
36 37 def run_test(self):
38 node = self.nodes[0]
39 wallet = MiniWallet(node)
40 # Mature one coinbase to spend.
41 self.generate(wallet, 101)
42 43 # Build a parent that fans out to MAX_TRANSACTIONS + OVER_CAP outputs.
44 # Inject it directly via generateblock since -privatebroadcast bypasses
45 # the mempool
46 utxo = wallet.get_utxo()
47 parent = wallet.create_self_transfer_multi(
48 utxos_to_spend=[utxo],
49 num_outputs=MAX_TRANSACTIONS + OVER_CAP,
50 fee_per_output=500,
51 )
52 self.generateblock(node, wallet.get_address(), [parent["hex"]])
53 54 children = [wallet.create_self_transfer(utxo_to_spend=u)
55 for u in parent["new_utxos"]]
56 assert_equal(len(children), MAX_TRANSACTIONS + OVER_CAP)
57 58 # Fill the queue exactly to the cap; every distinct submission succeeds.
59 self.log.info(f"Filling private broadcast queue to cap ({MAX_TRANSACTIONS} txns)")
60 for child in children[:MAX_TRANSACTIONS]:
61 node.sendrawtransaction(child["hex"])
62 63 pbinfo = node.getprivatebroadcastinfo()
64 assert_equal(len(pbinfo["transactions"]), MAX_TRANSACTIONS)
65 present_wtxids = {t["wtxid"] for t in pbinfo["transactions"]}
66 for i, child in enumerate(children[:MAX_TRANSACTIONS]):
67 assert child["wtxid"] in present_wtxids, \
68 f"tx index {i} (wtxid={child['wtxid']}) should be in the queue"
69 70 # Further distinct submissions are rejected with an RPC error, and the
71 # queue is left unchanged (nothing evicted to make room).
72 self.log.info(f"Submitting {OVER_CAP} more; each should be rejected (queue full)")
73 for child in children[MAX_TRANSACTIONS:]:
74 assert_raises_rpc_error(-37, "Private broadcast queue is full",
75 node.sendrawtransaction, child["hex"])
76 77 assert_equal(pbinfo["transactions"], node.getprivatebroadcastinfo()["transactions"])
78 79 self.log.info("Checking abortprivatebroadcast frees a slot for a new submission")
80 abort_res = node.abortprivatebroadcast(children[1]["txid"])
81 assert_equal([t["wtxid"] for t in abort_res["removed_transactions"]],
82 [children[1]["wtxid"]])
83 wtxids = {t["wtxid"] for t in node.getprivatebroadcastinfo()["transactions"]}
84 assert_equal(len(wtxids), MAX_TRANSACTIONS - 1)
85 assert children[1]["wtxid"] not in wtxids, "aborted tx should be gone from the queue"
86 87 new_child = children[MAX_TRANSACTIONS] # first previously-rejected tx
88 node.sendrawtransaction(new_child["hex"])
89 wtxids = {t["wtxid"] for t in node.getprivatebroadcastinfo()["transactions"]}
90 assert_equal(len(wtxids), MAX_TRANSACTIONS)
91 assert new_child["wtxid"] in wtxids, "freed slot should now hold the new tx"
92 assert children[1]["wtxid"] not in wtxids, "aborted tx should not reappear"
93 94 # Re-submitting an already-queued transaction is a no-op, not an error,
95 # even when the queue is full.
96 self.log.info("Re-submitting an already-queued tx should not error")
97 node.sendrawtransaction(children[0]["hex"])
98 wtxids = {t["wtxid"] for t in node.getprivatebroadcastinfo()["transactions"]}
99 assert_equal(len(wtxids), MAX_TRANSACTIONS)
100 assert children[0]["wtxid"] in wtxids, "re-submitted tx should remain queued"
101 102 if __name__ == "__main__":
103 PrivateBroadcastCapTest(__file__).main()
104