Inside Bitcoin
Opening the selected file…
Inside Bitcoin
Opening the selected file…
The code behind the network, with room to understand it.
Release published 2026-10-28 · Snapshot fetched 2026-11-01. Latest stable release at fetch time; development branches and Git history are excluded. Only the selected code window and visible folders load here.
1#!/usr/bin/env python32# Copyright (c) The Bitcoin Core developers3# Distributed under the MIT software license, see the accompanying4# file COPYING or http://www.opensource.org/licenses/mit-license.php.5"""Test transaction relay rate limiting via token buckets.7With -txsendrate=R, the inbound count bucket has capacity R*30. A broadcast8transaction is relayed immediately while the bucket has tokens; once it is9exhausted the excess transactions queue in a global backlog and drain as the10bucket refills (R tokens/second as mocktime advances).11"""13from decimal import Decimal14import time16from test_framework.blocktools import COINBASE_MATURITY17from test_framework.p2p import P2PTxInvStore18from test_framework.test_framework import BitcoinTestFramework19from test_framework.util import assert_equal20from test_framework.wallet import MiniWallet22SEND_RATE = 2 # -txsendrate value23BUCKET_CAP = SEND_RATE * 30 # count bucket capacity (60)24NUM_TXS = 80 # total transactions to submit27class TxRelayRateLimitTest(BitcoinTestFramework):28def set_test_params(self):29self.num_nodes = 130self.extra_args = [[f'-txsendrate={SEND_RATE}']]32def inbound_backlog(self, node):33return node.getnetworkinfo()['inv_buckets']['inbound']['backlog']35def run_test(self):36node = self.nodes[0]37wallet = MiniWallet(node)39node.setmocktime(int(time.time()))41# Mine enough blocks for mature coinbase UTXOs.42self.generate(wallet, COINBASE_MATURITY + NUM_TXS + 50)44# Connect an inbound peer (negotiates wtxid relay by default)45peer = node.add_p2p_connection(P2PTxInvStore())47# Advance time so the peer's trickle timer initializes48node.bumpmocktime(10)49peer.sync_with_ping()50assert_equal(len(peer.get_invs()), 0)52# Verify the configured send rate53assert_equal(node.getnetworkinfo()['tx_send_rate'], SEND_RATE)55self.test_rate_limit_and_rbf(node, wallet, peer)57def test_rate_limit_and_rbf(self, node, wallet, peer):58self.log.info(f"Submitting {NUM_TXS} transactions at frozen time (bucket capacity {BUCKET_CAP})")60# Prepare an RBF pair: original and replacement spending the same UTXO.61# Both are created upfront so we can submit the replacement later.62rbf_utxo = wallet.get_utxo()63tx_rbf_orig = wallet.create_self_transfer(utxo_to_spend=rbf_utxo)64tx_rbf_repl = wallet.create_self_transfer(utxo_to_spend=rbf_utxo, fee_rate=Decimal("0.009"))66# Submit NUM_TXS transactions at frozen time. Each broadcast is relayed67# immediately while the count bucket has tokens, so the first BUCKET_CAP68# are handed straight to the peer and the rest queue in the backlog. The69# RBF original is placed in the backlogged tail.70RBF_INDEX = NUM_TXS - 571for i in range(NUM_TXS):72if i == RBF_INDEX:73node.sendrawtransaction(tx_rbf_orig['hex'])74else:75wallet.send_self_transfer(from_node=node)77# The excess beyond the bucket capacity is backlogged. Nothing is78# announced yet -- the per-peer trickle timer hasn't fired (frozen time).79self.log.info(f"Backlog after burst: {self.inbound_backlog(node)}")80assert_equal(self.inbound_backlog(node), NUM_TXS - BUCKET_CAP)81assert_equal(len(peer.get_invs()), 0)83# RBF the backlogged original while time is still frozen, so the84# replacement also queues in the backlog (the bucket is exhausted). The85# original's wtxid stays in the backlog vector for now but is dropped86# when the backlog is processed, since it is no longer in the mempool.87self.log.info("RBF'ing a backlogged transaction")88node.sendrawtransaction(tx_rbf_repl['hex'])89assert_equal(self.inbound_backlog(node), NUM_TXS - BUCKET_CAP + 1)91# Advance time so the bucket refills and the backlog drains. Loop until92# the backlog is empty and every surviving tx has trickled out.93self.log.info("Advancing time to drain the backlog")94for _ in range(30):95if self.inbound_backlog(node) == 0 and len(peer.get_invs()) == NUM_TXS:96break97node.bumpmocktime(4)98peer.sync_with_ping()100announced = set(peer.get_invs())101self.log.info(f"Total announced: {len(announced)}")103# Every surviving transaction is announced: the burst minus the dropped104# RBF original, plus the replacement.105assert_equal(self.inbound_backlog(node), 0)106assert_equal(len(announced), NUM_TXS)107assert int(tx_rbf_orig['wtxid'], 16) not in announced108assert int(tx_rbf_repl['wtxid'], 16) in announced110self.log.info("Rate limiting and RBF backlog cleanup test passed")113if __name__ == '__main__':114TxRelayRateLimitTest(__file__).main()
Independent explanations by MyBitcoinExplorer. Not affiliated with or endorsed by Bitcoin Core. Guides are educational and do not replace the source or its tests.
Bitcoin Core source © its contributors, distributed under the MIT license. Original notices and bundled third-party licenses are retained in the source tree.