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) 2023-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"""Test sigop limit mempool policy (`-bytespersigop` parameter)"""6from copy import deepcopy7from decimal import Decimal8from math import ceil10from test_framework.messages import (11COutPoint,12CTransaction,13CTxIn,14CTxInWitness,15CTxOut,16WITNESS_SCALE_FACTOR,17tx_from_hex,18)19from test_framework.script import (20CScript,21OP_2DUP,22OP_CHECKMULTISIG,23OP_CHECKSIG,24OP_DROP,25OP_ENDIF,26OP_FALSE,27OP_IF,28OP_NOT,29OP_RETURN,30OP_TRUE,31)32from test_framework.script_util import (33keys_to_multisig_script,34script_to_p2wsh_script,35script_to_p2sh_script,36MAX_STD_LEGACY_SIGOPS,37MAX_STD_P2SH_SIGOPS,38)39from test_framework.test_framework import BitcoinTestFramework40from test_framework.util import (41assert_equal,42assert_greater_than,43assert_greater_than_or_equal,44assert_not_equal,45assert_raises_rpc_error,46)47from test_framework.wallet import MiniWallet48from test_framework.wallet_util import generate_keypair50DEFAULT_BYTES_PER_SIGOP = 20 # default setting51MAX_PUBKEYS_PER_MULTISIG = 2053class BytesPerSigOpTest(BitcoinTestFramework):54def set_test_params(self):55self.num_nodes = 157def create_p2wsh_spending_tx(self, witness_script, output_script):58"""Create a 1-input-1-output P2WSH spending transaction with only the59witness script in the witness stack and the given output script."""60# create P2WSH address and fund it via MiniWallet first61fund = self.wallet.send_to(62from_node=self.nodes[0],63scriptPubKey=script_to_p2wsh_script(witness_script),64amount=1000000,65)67# create spending transaction68tx = CTransaction()69tx.vin = [CTxIn(COutPoint(int(fund["txid"], 16), fund["sent_vout"]))]70tx.wit.vtxinwit = [CTxInWitness()]71tx.wit.vtxinwit[0].scriptWitness.stack = [bytes(witness_script)]72tx.vout = [CTxOut(500000, output_script)]73return tx75def test_sigops_limit(self, bytes_per_sigop, num_sigops):76sigop_equivalent_vsize = ceil(num_sigops * bytes_per_sigop / WITNESS_SCALE_FACTOR)77self.log.info(f"- {num_sigops} sigops (equivalent size of {sigop_equivalent_vsize} vbytes)")79# create a template tx with the specified sigop cost in the witness script80# (note that the sigops count even though being in a branch that's not executed)81num_multisigops = num_sigops // 2082num_singlesigops = num_sigops % 2083witness_script = CScript(84[OP_FALSE, OP_IF] +85[OP_CHECKMULTISIG]*num_multisigops +86[OP_CHECKSIG]*num_singlesigops +87[OP_ENDIF, OP_TRUE]88)89# use a 256-byte data-push as lower bound in the output script, in order90# to avoid having to compensate for tx size changes caused by varying91# length serialization sizes (both for scriptPubKey and data-push lengths)92tx = self.create_p2wsh_spending_tx(witness_script, CScript([OP_RETURN, b'X'*256]))94# bump the tx to reach the sigop-limit equivalent size by padding the datacarrier output95assert_greater_than_or_equal(sigop_equivalent_vsize, tx.get_vsize())96vsize_to_pad = sigop_equivalent_vsize - tx.get_vsize()97tx.vout[0].scriptPubKey = CScript([OP_RETURN, b'X'*(256+vsize_to_pad)])98assert_equal(sigop_equivalent_vsize, tx.get_vsize())100res = self.nodes[0].testmempoolaccept([tx.serialize().hex()])[0]101assert_equal(res['allowed'], True)102assert_equal(sigop_equivalent_vsize, tx.get_vsize())103assert_equal(res['vsize'], sigop_equivalent_vsize)104assert_equal(res['vsize_adjusted'], sigop_equivalent_vsize)105assert_equal(res['vsize_bip141'], tx.get_vsize())107# increase the tx's vsize to be right above the sigop-limit equivalent size108# => tx's vsize in mempool should also grow accordingly109tx.vout[0].scriptPubKey = CScript([OP_RETURN, b'X'*(256+vsize_to_pad+1)])110res = self.nodes[0].testmempoolaccept([tx.serialize().hex()])[0]111assert_equal(res['allowed'], True)112assert_equal(sigop_equivalent_vsize + 1, tx.get_vsize())113assert_equal(res['vsize'], sigop_equivalent_vsize+1)114assert_equal(res['vsize_adjusted'], sigop_equivalent_vsize + 1)115assert_equal(res['vsize_bip141'], tx.get_vsize())117# decrease the tx's vsize to be right below the sigop-limit equivalent size118# => tx's vsize in mempool should stick at the sigop-limit equivalent119# bytes level, as it is higher than the tx's serialized vsize120# (the maximum of both is taken)121tx.vout[0].scriptPubKey = CScript([OP_RETURN, b'X'*(256+vsize_to_pad-1)])122res = self.nodes[0].testmempoolaccept([tx.serialize().hex()])[0]123assert_equal(res['allowed'], True)124assert_not_equal(sigop_equivalent_vsize, tx.get_vsize())125assert_equal(res['vsize'], sigop_equivalent_vsize)126assert_equal(res['vsize_adjusted'], sigop_equivalent_vsize)127assert_equal(res['vsize_bip141'], tx.get_vsize())129# check that the ancestor and descendant size calculations in the mempool130# also use the same max(sigop_equivalent_vsize, serialized_vsize) logic131# (to keep it simple, we only test the case here where the sigop vsize132# is much larger than the serialized vsize, i.e. we create a small child133# tx by getting rid of the large padding output)134tx.vout[0].scriptPubKey = CScript([OP_RETURN, b'test123'])135assert_greater_than(sigop_equivalent_vsize, tx.get_vsize())136self.nodes[0].sendrawtransaction(hexstring=tx.serialize().hex(), maxburnamount='1.0')138# fetch parent tx, which doesn't contain any sigops139parent_txid = tx.vin[0].prevout.hash.to_bytes(32, 'big').hex()140parent_tx = tx_from_hex(self.nodes[0].getrawtransaction(txid=parent_txid))142entry_child = self.nodes[0].getmempoolentry(tx.txid_hex)143assert_equal(entry_child['descendantcount'], 1)144assert_equal(entry_child['descendantsize'], sigop_equivalent_vsize)145assert_equal(entry_child['ancestorcount'], 2)146assert_equal(entry_child['ancestorsize'], sigop_equivalent_vsize + parent_tx.get_vsize())148entry_parent = self.nodes[0].getmempoolentry(parent_tx.txid_hex)149assert_equal(entry_parent['ancestorcount'], 1)150assert_equal(entry_parent['ancestorsize'], parent_tx.get_vsize())151assert_equal(entry_parent['descendantcount'], 2)152assert_equal(entry_parent['descendantsize'], parent_tx.get_vsize() + sigop_equivalent_vsize)154tx_hex = tx.serialize().hex()155txid = tx.txid_hex156self.log.info(f"Pushing padded tx {txid} into mempool for getrawtransaction sigopsize field check")157self.nodes[0].sendrawtransaction(hexstring=tx_hex, maxburnamount='1.0')158# Fetch with verbosity=1 and assert sigopsize is present and correct159raw = self.nodes[0].getrawtransaction(txid, 1)160sigop_equivalent_vsize = ceil(num_sigops * bytes_per_sigop / WITNESS_SCALE_FACTOR)161assert_equal(raw['vsize_adjusted'], sigop_equivalent_vsize)162assert_greater_than(raw['vsize_adjusted'], raw['vsize'])164def test_sigops_package(self):165self.log.info("Test a overly-large sigops-vbyte hits package limits")166# Make a 2-transaction package which fails vbyte checks even though167# separately they would work.168self.restart_node(0, extra_args=["-bytespersigop=5000","-permitbaremultisig=1"])170def create_bare_multisig_tx(utxo_to_spend=None):171_, pubkey = generate_keypair()172amount_for_bare = 50000173tx_dict = self.wallet.create_self_transfer(fee=Decimal("3"), utxo_to_spend=utxo_to_spend)174tx_utxo = tx_dict["new_utxo"]175tx = tx_dict["tx"]176tx.vout.append(CTxOut(amount_for_bare, keys_to_multisig_script([pubkey], k=1)))177tx.vout[0].nValue -= amount_for_bare178tx_utxo["txid"] = tx.txid_hex179tx_utxo["value"] -= Decimal("0.00005000")180return (tx_utxo, tx)182tx_parent_utxo, tx_parent = create_bare_multisig_tx()183_tx_child_utxo, tx_child = create_bare_multisig_tx(tx_parent_utxo)185# Separately, the parent tx is ok186parent_individual_testres = self.nodes[0].testmempoolaccept([tx_parent.serialize().hex()])[0]187assert parent_individual_testres["allowed"]188max_multisig_vsize = MAX_PUBKEYS_PER_MULTISIG * 5000189assert_equal(parent_individual_testres["vsize_adjusted"], max_multisig_vsize)191# But together, it's exceeding limits in the *package* context. If sigops adjusted vsize wasn't being checked192# here, it would get further in validation and give too-large-cluster error instead.193packet_test = self.nodes[0].testmempoolaccept([tx_parent.serialize().hex(), tx_child.serialize().hex()])194expected_package_error = "too-large-cluster"195assert_equal([x["package-error"] for x in packet_test], [expected_package_error] * 2)197# When we actually try to submit, the parent makes it into the mempool, but the child would exceed cluster vsize limits198res = self.nodes[0].submitpackage([tx_parent.serialize().hex(), tx_child.serialize().hex()])199assert "too-large-cluster" in res["tx-results"][tx_child.wtxid_hex]["error"]200assert tx_parent.txid_hex in self.nodes[0].getrawmempool()202# Transactions are tiny in weight203assert_greater_than(2000, tx_parent.get_weight() + tx_child.get_weight())205def test_legacy_sigops_stdness(self):206self.log.info("Test a transaction with too many legacy sigops in its inputs is non-standard.")208# Restart with the default settings209self.restart_node(0)211# Create a P2SH script with 15 sigops.212_, dummy_pubkey = generate_keypair()213packed_redeem_script = [dummy_pubkey]214for _ in range(MAX_STD_P2SH_SIGOPS - 1):215packed_redeem_script += [OP_2DUP, OP_CHECKSIG, OP_DROP]216packed_redeem_script = CScript(packed_redeem_script + [OP_CHECKSIG, OP_NOT])217packed_p2sh_script = script_to_p2sh_script(packed_redeem_script)219# Create enough outputs to reach the sigops limit when spending them all at once.220outpoints = []221for _ in range(int(MAX_STD_LEGACY_SIGOPS / MAX_STD_P2SH_SIGOPS) + 1):222res = self.wallet.send_to(from_node=self.nodes[0], scriptPubKey=packed_p2sh_script, amount=1_000)223txid = int.from_bytes(bytes.fromhex(res["txid"]), byteorder="big")224outpoints.append(COutPoint(txid, res["sent_vout"]))225self.generate(self.nodes[0], 1)227# Spending all these outputs at once accounts for 2505 legacy sigops and is non-standard.228nonstd_tx = CTransaction()229nonstd_tx.vin = [CTxIn(op, CScript([b"", packed_redeem_script])) for op in outpoints]230nonstd_tx.vout = [CTxOut(0, CScript([OP_RETURN, b""]))]231assert_raises_rpc_error(-26, "bad-txns-nonstandard-inputs, non-witness sigops exceed bip54 limit",232self.nodes[0].sendrawtransaction, nonstd_tx.serialize().hex())234# Spending one less accounts for 2490 legacy sigops and is standard.235std_tx = deepcopy(nonstd_tx)236std_tx.vin.pop()237self.nodes[0].sendrawtransaction(std_tx.serialize().hex())239# Make sure the original, non-standard, transaction can be mined.240self.generateblock(self.nodes[0], output="raw(42)", transactions=[nonstd_tx.serialize().hex()])242def run_test(self):243self.wallet = MiniWallet(self.nodes[0])245for bytes_per_sigop in (DEFAULT_BYTES_PER_SIGOP, 43, 81, 165, 327, 649, 1072):246if bytes_per_sigop == DEFAULT_BYTES_PER_SIGOP:247self.log.info(f"Test default sigops limit setting ({bytes_per_sigop} bytes per sigop)...")248else:249bytespersigop_parameter = f"-bytespersigop={bytes_per_sigop}"250self.log.info(f"Test sigops limit setting {bytespersigop_parameter}...")
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.