p2p_private_broadcast_cap.py raw

   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