mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-09-11 21:20:39 +02:00
The server isn't running out of memory when the private broadcast transaction queue is full. Add a new RPC_LIMIT_EXCEEDED code that can be used whenever a resource is bound and currently at capacity.
104 lines
4.8 KiB
Python
Executable File
104 lines
4.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# Copyright (c) 2026-present The Bitcoin Core developers
|
|
# Distributed under the MIT software license, see the accompanying
|
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
|
"""Test the private-broadcast queue size cap: submissions beyond the cap are
|
|
rejected (the queue is not modified), rather than evicting existing entries.
|
|
"""
|
|
|
|
from test_framework.test_framework import BitcoinTestFramework
|
|
from test_framework.util import assert_equal, assert_raises_rpc_error
|
|
from test_framework.wallet import MiniWallet
|
|
|
|
|
|
# Must match PrivateBroadcast::MAX_TRANSACTIONS
|
|
MAX_TRANSACTIONS = 10_000
|
|
OVER_CAP = 5
|
|
|
|
|
|
class PrivateBroadcastCapTest(BitcoinTestFramework):
|
|
def set_test_params(self):
|
|
self.num_nodes = 1
|
|
# -privatebroadcast is incompatible with the framework's default
|
|
# -connect=0; allow autoconnect (no actual peers will succeed though).
|
|
self.disable_autoconnect = False
|
|
self.extra_args = [[
|
|
"-privatebroadcast",
|
|
# Fake I2P reachability so the privatebroadcast startup precondition passes.
|
|
"-i2psam=127.0.0.1:1",
|
|
"-proxy=127.0.0.1:1",
|
|
]]
|
|
|
|
def setup_network(self):
|
|
# Skip the framework's default connect_nodes loop. We have a single
|
|
# node and don't need any peer connections.
|
|
self.setup_nodes()
|
|
|
|
def run_test(self):
|
|
node = self.nodes[0]
|
|
wallet = MiniWallet(node)
|
|
# Mature one coinbase to spend.
|
|
self.generate(wallet, 101)
|
|
|
|
# Build a parent that fans out to MAX_TRANSACTIONS + OVER_CAP outputs.
|
|
# Inject it directly via generateblock since -privatebroadcast bypasses
|
|
# the mempool
|
|
utxo = wallet.get_utxo()
|
|
parent = wallet.create_self_transfer_multi(
|
|
utxos_to_spend=[utxo],
|
|
num_outputs=MAX_TRANSACTIONS + OVER_CAP,
|
|
fee_per_output=500,
|
|
)
|
|
self.generateblock(node, wallet.get_address(), [parent["hex"]])
|
|
|
|
children = [wallet.create_self_transfer(utxo_to_spend=u)
|
|
for u in parent["new_utxos"]]
|
|
assert_equal(len(children), MAX_TRANSACTIONS + OVER_CAP)
|
|
|
|
# Fill the queue exactly to the cap; every distinct submission succeeds.
|
|
self.log.info(f"Filling private broadcast queue to cap ({MAX_TRANSACTIONS} txns)")
|
|
for child in children[:MAX_TRANSACTIONS]:
|
|
node.sendrawtransaction(child["hex"])
|
|
|
|
pbinfo = node.getprivatebroadcastinfo()
|
|
assert_equal(len(pbinfo["transactions"]), MAX_TRANSACTIONS)
|
|
present_wtxids = {t["wtxid"] for t in pbinfo["transactions"]}
|
|
for i, child in enumerate(children[:MAX_TRANSACTIONS]):
|
|
assert child["wtxid"] in present_wtxids, \
|
|
f"tx index {i} (wtxid={child['wtxid']}) should be in the queue"
|
|
|
|
# Further distinct submissions are rejected with an RPC error, and the
|
|
# queue is left unchanged (nothing evicted to make room).
|
|
self.log.info(f"Submitting {OVER_CAP} more; each should be rejected (queue full)")
|
|
for child in children[MAX_TRANSACTIONS:]:
|
|
assert_raises_rpc_error(-37, "Private broadcast queue is full",
|
|
node.sendrawtransaction, child["hex"])
|
|
|
|
assert_equal(pbinfo["transactions"], node.getprivatebroadcastinfo()["transactions"])
|
|
|
|
self.log.info("Checking abortprivatebroadcast frees a slot for a new submission")
|
|
abort_res = node.abortprivatebroadcast(children[1]["txid"])
|
|
assert_equal([t["wtxid"] for t in abort_res["removed_transactions"]],
|
|
[children[1]["wtxid"]])
|
|
wtxids = {t["wtxid"] for t in node.getprivatebroadcastinfo()["transactions"]}
|
|
assert_equal(len(wtxids), MAX_TRANSACTIONS - 1)
|
|
assert children[1]["wtxid"] not in wtxids, "aborted tx should be gone from the queue"
|
|
|
|
new_child = children[MAX_TRANSACTIONS] # first previously-rejected tx
|
|
node.sendrawtransaction(new_child["hex"])
|
|
wtxids = {t["wtxid"] for t in node.getprivatebroadcastinfo()["transactions"]}
|
|
assert_equal(len(wtxids), MAX_TRANSACTIONS)
|
|
assert new_child["wtxid"] in wtxids, "freed slot should now hold the new tx"
|
|
assert children[1]["wtxid"] not in wtxids, "aborted tx should not reappear"
|
|
|
|
# Re-submitting an already-queued transaction is a no-op, not an error,
|
|
# even when the queue is full.
|
|
self.log.info("Re-submitting an already-queued tx should not error")
|
|
node.sendrawtransaction(children[0]["hex"])
|
|
wtxids = {t["wtxid"] for t in node.getprivatebroadcastinfo()["transactions"]}
|
|
assert_equal(len(wtxids), MAX_TRANSACTIONS)
|
|
assert children[0]["wtxid"] in wtxids, "re-submitted tx should remain queued"
|
|
|
|
if __name__ == "__main__":
|
|
PrivateBroadcastCapTest(__file__).main()
|