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) 2024-present 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"""6Test opportunistic 1p1c package submission logic.7"""9from decimal import Decimal10import random11import time13from test_framework.blocktools import MAX_STANDARD_TX_WEIGHT14from test_framework.mempool_util import (15create_large_orphan,16DEFAULT_MIN_RELAY_TX_FEE,17fill_mempool,18)19from test_framework.messages import (20CInv,21COIN,22COutPoint,23CTransaction,24CTxIn,25CTxOut,26CTxInWitness,27MAX_BIP125_RBF_SEQUENCE,28MSG_WTX,29msg_inv,30msg_tx,31tx_from_hex,32)33from test_framework.p2p import (34NONPREF_PEER_TX_DELAY,35P2PInterface,36TXID_RELAY_DELAY,37)38from test_framework.script import (39CScript,40OP_NOP,41OP_RETURN,42)43from test_framework.test_framework import BitcoinTestFramework44from test_framework.util import (45assert_equal,46assert_greater_than,47assert_greater_than_or_equal,48)49from test_framework.wallet import (50MiniWallet,51MiniWalletMode,52)54# 1sat/vB feerate denominated in BTC/KvB55FEERATE_1SAT_VB = Decimal("0.00001000")56# Number of seconds to wait to ensure no getdata is received57GETDATA_WAIT = 6059def cleanup(func):60def wrapper(self, *args, **kwargs):61func(self, *args, **kwargs)63self.nodes[0].disconnect_p2ps()64# Do not clear the node's mempool, as each test requires mempool min feerate > min65# relay feerate. However, do check that this is the case.66assert_greater_than(self.nodes[0].getmempoolinfo()["mempoolminfee"], self.nodes[0].getnetworkinfo()["relayfee"])67# Ensure we do not try to spend the same UTXOs in subsequent tests, as they will look like RBF attempts.68self.wallet.rescan_utxos(include_mempool=True)70# Resets if mocktime was used71self.nodes[0].setmocktime(0)72return wrapper74class PackageRelayTest(BitcoinTestFramework):75def set_test_params(self):76self.setup_clean_chain = True77self.num_nodes = 178self.extra_args = [[79"-maxmempool=5","-inboundrelaypercent=100"80]]82def create_tx_below_mempoolminfee(self, wallet, utxo_to_spend=None):83"""Create a 1-input 0.1sat/vB transaction using a confirmed UTXO. Decrement and use84self.sequence so that subsequent calls to this function result in unique transactions."""86self.sequence -= 187assert_greater_than(self.nodes[0].getmempoolinfo()["mempoolminfee"], Decimal(DEFAULT_MIN_RELAY_TX_FEE) / COIN)89return wallet.create_self_transfer(fee_rate=Decimal(DEFAULT_MIN_RELAY_TX_FEE) / COIN, sequence=self.sequence, utxo_to_spend=utxo_to_spend, confirmed_only=True)91@cleanup92def test_basic_child_then_parent(self):93node = self.nodes[0]94self.log.info("Check that opportunistic 1p1c logic works when child is received before parent")95node.setmocktime(int(time.time()))97low_fee_parent = self.create_tx_below_mempoolminfee(self.wallet)98high_fee_child = self.wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=20*FEERATE_1SAT_VB)100peer_sender = node.add_p2p_connection(P2PInterface())102# 1. Child is received first (perhaps the low feerate parent didn't meet feefilter or the requests were sent to different nodes). It is missing an input.103high_child_wtxid_int = high_fee_child["tx"].wtxid_int104peer_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=high_child_wtxid_int)]))105node.bumpmocktime(NONPREF_PEER_TX_DELAY)106peer_sender.wait_for_getdata([high_child_wtxid_int])107peer_sender.send_and_ping(msg_tx(high_fee_child["tx"]))109# 2. Node requests the missing parent by txid.110parent_txid_int = int(low_fee_parent["txid"], 16)111node.bumpmocktime(NONPREF_PEER_TX_DELAY + TXID_RELAY_DELAY)112peer_sender.wait_for_getdata([parent_txid_int])114# 3. Sender relays the parent. Parent+Child are evaluated as a package and accepted.115peer_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))117# 4. Both transactions should now be in mempool.118node_mempool = node.getrawmempool()119assert low_fee_parent["txid"] in node_mempool120assert high_fee_child["txid"] in node_mempool122node.disconnect_p2ps()124@cleanup125def test_basic_parent_then_child(self, wallet):126node = self.nodes[0]127node.setmocktime(int(time.time()))128low_fee_parent = self.create_tx_below_mempoolminfee(wallet)129high_fee_child = wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=20*FEERATE_1SAT_VB)131peer_sender = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=1, connection_type="outbound-full-relay")132peer_ignored = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=2, connection_type="outbound-full-relay")134# 1. Parent is relayed first. It is too low feerate.135parent_wtxid_int = low_fee_parent["tx"].wtxid_int136peer_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=parent_wtxid_int)]))137peer_sender.wait_for_getdata([parent_wtxid_int])138peer_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))139assert low_fee_parent["txid"] not in node.getrawmempool()141# Send again from peer_ignored, check that it is ignored142peer_ignored.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=parent_wtxid_int)]))143assert "getdata" not in peer_ignored.last_message145# 2. Child is relayed next. It is missing an input.146high_child_wtxid_int = high_fee_child["tx"].wtxid_int147peer_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=high_child_wtxid_int)]))148peer_sender.wait_for_getdata([high_child_wtxid_int])149peer_sender.send_and_ping(msg_tx(high_fee_child["tx"]))151# 3. Node requests the missing parent by txid.152# It should do so even if it has previously rejected that parent for being too low feerate.153parent_txid_int = int(low_fee_parent["txid"], 16)154node.bumpmocktime(TXID_RELAY_DELAY)155peer_sender.wait_for_getdata([parent_txid_int])157# 4. Sender re-relays the parent. Parent+Child are evaluated as a package and accepted.158peer_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))160# 5. Both transactions should now be in mempool.161node_mempool = node.getrawmempool()162assert low_fee_parent["txid"] in node_mempool163assert high_fee_child["txid"] in node_mempool165@cleanup166def test_low_and_high_child(self, wallet):167node = self.nodes[0]168node.setmocktime(int(time.time()))169low_fee_parent = self.create_tx_below_mempoolminfee(wallet)170# This feerate is above mempoolminfee, but not enough to also bump the low feerate parent.171feerate_just_above = node.getmempoolinfo()["mempoolminfee"]172med_fee_child = wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=feerate_just_above)173high_fee_child = wallet.create_self_transfer(utxo_to_spend=low_fee_parent["new_utxo"], fee_rate=999*FEERATE_1SAT_VB)175peer_sender = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=1, connection_type="outbound-full-relay")176peer_ignored = node.add_outbound_p2p_connection(P2PInterface(), p2p_idx=2, connection_type="outbound-full-relay")178self.log.info("Check that tx caches low fee parent + low fee child package rejections")180# 1. Send parent, rejected for being low feerate.181parent_wtxid_int = low_fee_parent["tx"].wtxid_int182peer_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=parent_wtxid_int)]))183peer_sender.wait_for_getdata([parent_wtxid_int])184peer_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))185assert low_fee_parent["txid"] not in node.getrawmempool()187# Send again from peer_ignored, check that it is ignored188peer_ignored.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=parent_wtxid_int)]))189assert "getdata" not in peer_ignored.last_message191# 2. Send an (orphan) child that has a higher feerate, but not enough to bump the parent.192med_child_wtxid_int = med_fee_child["tx"].wtxid_int193peer_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=med_child_wtxid_int)]))194peer_sender.wait_for_getdata([med_child_wtxid_int])195peer_sender.send_and_ping(msg_tx(med_fee_child["tx"]))197# 3. Node requests the orphan's missing parent.198parent_txid_int = int(low_fee_parent["txid"], 16)199node.bumpmocktime(TXID_RELAY_DELAY)200peer_sender.wait_for_getdata([parent_txid_int])202# 4. The low parent + low child are submitted as a package. They are not accepted due to low package feerate.203peer_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))205assert low_fee_parent["txid"] not in node.getrawmempool()206assert med_fee_child["txid"] not in node.getrawmempool()208# If peer_ignored announces the low feerate child, it should be ignored209peer_ignored.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=med_child_wtxid_int)]))210assert "getdata" not in peer_ignored.last_message211# If either peer sends the parent again, package evaluation should not be attempted212peer_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))213peer_ignored.send_and_ping(msg_tx(low_fee_parent["tx"]))215assert low_fee_parent["txid"] not in node.getrawmempool()216assert med_fee_child["txid"] not in node.getrawmempool()218# 5. Send the high feerate (orphan) child219high_child_wtxid_int = high_fee_child["tx"].wtxid_int220peer_sender.send_and_ping(msg_inv([CInv(t=MSG_WTX, h=high_child_wtxid_int)]))221peer_sender.wait_for_getdata([high_child_wtxid_int])222peer_sender.send_and_ping(msg_tx(high_fee_child["tx"]))224# 6. Node requests the orphan's parent, even though it has already been rejected, both by225# itself and with a child. This is necessary, otherwise high_fee_child can be censored.226parent_txid_int = int(low_fee_parent["txid"], 16)227node.bumpmocktime(TXID_RELAY_DELAY)228peer_sender.wait_for_getdata([parent_txid_int])230# 7. The low feerate parent + high feerate child are submitted as a package.231peer_sender.send_and_ping(msg_tx(low_fee_parent["tx"]))233# 8. Both transactions should now be in mempool234node_mempool = node.getrawmempool()235assert low_fee_parent["txid"] in node_mempool236assert high_fee_child["txid"] in node_mempool237assert med_fee_child["txid"] not in node_mempool239@cleanup240def test_orphan_consensus_failure(self):241self.log.info("Check opportunistic 1p1c logic requires parent and child to be from the same peer")242node = self.nodes[0]243node.setmocktime(int(time.time()))244low_fee_parent = self.create_tx_below_mempoolminfee(self.wallet)245coin = low_fee_parent["new_utxo"]246address = node.get_deterministic_priv_key().address247# Create raw transaction spending the parent, but with no signature (a consensus error).248hex_orphan_no_sig = node.createrawtransaction([{"txid": coin["txid"], "vout": coin["vout"]}], {address : coin["value"] - Decimal("0.0001")})249tx_orphan_bad_wit = tx_from_hex(hex_orphan_no_sig)250tx_orphan_bad_wit.wit.vtxinwit.append(CTxInWitness())
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.