mempool_sigoplimit.py
1 #!/usr/bin/env python3 2 # Copyright (c) 2023-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 sigop limit mempool policy (`-bytespersigop` parameter)""" 6 from copy import deepcopy 7 from decimal import Decimal 8 from math import ceil 9 10 from test_framework.messages import ( 11 COutPoint, 12 CTransaction, 13 CTxIn, 14 CTxInWitness, 15 CTxOut, 16 WITNESS_SCALE_FACTOR, 17 tx_from_hex, 18 ) 19 from test_framework.script import ( 20 CScript, 21 OP_2DUP, 22 OP_CHECKMULTISIG, 23 OP_CHECKSIG, 24 OP_DROP, 25 OP_ENDIF, 26 OP_FALSE, 27 OP_IF, 28 OP_NOT, 29 OP_RETURN, 30 OP_TRUE, 31 ) 32 from test_framework.script_util import ( 33 keys_to_multisig_script, 34 script_to_p2wsh_script, 35 script_to_p2sh_script, 36 MAX_STD_LEGACY_SIGOPS, 37 MAX_STD_P2SH_SIGOPS, 38 ) 39 from test_framework.test_framework import BitcoinTestFramework 40 from test_framework.util import ( 41 assert_equal, 42 assert_greater_than, 43 assert_greater_than_or_equal, 44 assert_raises_rpc_error, 45 ) 46 from test_framework.wallet import MiniWallet 47 from test_framework.wallet_util import generate_keypair 48 49 DEFAULT_BYTES_PER_SIGOP = 20 # default setting 50 MAX_PUBKEYS_PER_MULTISIG = 20 51 52 class BytesPerSigOpTest(BitcoinTestFramework): 53 def set_test_params(self): 54 self.num_nodes = 1 55 56 def create_p2wsh_spending_tx(self, witness_script, output_script): 57 """Create a 1-input-1-output P2WSH spending transaction with only the 58 witness script in the witness stack and the given output script.""" 59 # create P2WSH address and fund it via MiniWallet first 60 fund = self.wallet.send_to( 61 from_node=self.nodes[0], 62 scriptPubKey=script_to_p2wsh_script(witness_script), 63 amount=1000000, 64 ) 65 66 # create spending transaction 67 tx = CTransaction() 68 tx.vin = [CTxIn(COutPoint(int(fund["txid"], 16), fund["sent_vout"]))] 69 tx.wit.vtxinwit = [CTxInWitness()] 70 tx.wit.vtxinwit[0].scriptWitness.stack = [bytes(witness_script)] 71 tx.vout = [CTxOut(500000, output_script)] 72 return tx 73 74 def test_sigops_limit(self, bytes_per_sigop, num_sigops): 75 sigop_equivalent_vsize = ceil(num_sigops * bytes_per_sigop / WITNESS_SCALE_FACTOR) 76 self.log.info(f"- {num_sigops} sigops (equivalent size of {sigop_equivalent_vsize} vbytes)") 77 78 # create a template tx with the specified sigop cost in the witness script 79 # (note that the sigops count even though being in a branch that's not executed) 80 num_multisigops = num_sigops // 20 81 num_singlesigops = num_sigops % 20 82 witness_script = CScript( 83 [OP_FALSE, OP_IF] + 84 [OP_CHECKMULTISIG]*num_multisigops + 85 [OP_CHECKSIG]*num_singlesigops + 86 [OP_ENDIF, OP_TRUE] 87 ) 88 # use a 256-byte data-push as lower bound in the output script, in order 89 # to avoid having to compensate for tx size changes caused by varying 90 # length serialization sizes (both for scriptPubKey and data-push lengths) 91 tx = self.create_p2wsh_spending_tx(witness_script, CScript([OP_RETURN, b'X'*256])) 92 93 # bump the tx to reach the sigop-limit equivalent size by padding the datacarrier output 94 assert_greater_than_or_equal(sigop_equivalent_vsize, tx.get_vsize()) 95 vsize_to_pad = sigop_equivalent_vsize - tx.get_vsize() 96 tx.vout[0].scriptPubKey = CScript([OP_RETURN, b'X'*(256+vsize_to_pad)]) 97 assert_equal(sigop_equivalent_vsize, tx.get_vsize()) 98 99 res = self.nodes[0].testmempoolaccept([tx.serialize().hex()])[0] 100 assert_equal(res['allowed'], True) 101 assert_equal(res['vsize'], sigop_equivalent_vsize) 102 103 # increase the tx's vsize to be right above the sigop-limit equivalent size 104 # => tx's vsize in mempool should also grow accordingly 105 tx.vout[0].scriptPubKey = CScript([OP_RETURN, b'X'*(256+vsize_to_pad+1)]) 106 res = self.nodes[0].testmempoolaccept([tx.serialize().hex()])[0] 107 assert_equal(res['allowed'], True) 108 assert_equal(res['vsize'], sigop_equivalent_vsize+1) 109 110 # decrease the tx's vsize to be right below the sigop-limit equivalent size 111 # => tx's vsize in mempool should stick at the sigop-limit equivalent 112 # bytes level, as it is higher than the tx's serialized vsize 113 # (the maximum of both is taken) 114 tx.vout[0].scriptPubKey = CScript([OP_RETURN, b'X'*(256+vsize_to_pad-1)]) 115 res = self.nodes[0].testmempoolaccept([tx.serialize().hex()])[0] 116 assert_equal(res['allowed'], True) 117 assert_equal(res['vsize'], sigop_equivalent_vsize) 118 119 # check that the ancestor and descendant size calculations in the mempool 120 # also use the same max(sigop_equivalent_vsize, serialized_vsize) logic 121 # (to keep it simple, we only test the case here where the sigop vsize 122 # is much larger than the serialized vsize, i.e. we create a small child 123 # tx by getting rid of the large padding output) 124 tx.vout[0].scriptPubKey = CScript([OP_RETURN, b'test123']) 125 assert_greater_than(sigop_equivalent_vsize, tx.get_vsize()) 126 self.nodes[0].sendrawtransaction(hexstring=tx.serialize().hex(), maxburnamount='1.0') 127 128 # fetch parent tx, which doesn't contain any sigops 129 parent_txid = tx.vin[0].prevout.hash.to_bytes(32, 'big').hex() 130 parent_tx = tx_from_hex(self.nodes[0].getrawtransaction(txid=parent_txid)) 131 132 entry_child = self.nodes[0].getmempoolentry(tx.txid_hex) 133 assert_equal(entry_child['descendantcount'], 1) 134 assert_equal(entry_child['descendantsize'], sigop_equivalent_vsize) 135 assert_equal(entry_child['ancestorcount'], 2) 136 assert_equal(entry_child['ancestorsize'], sigop_equivalent_vsize + parent_tx.get_vsize()) 137 138 entry_parent = self.nodes[0].getmempoolentry(parent_tx.txid_hex) 139 assert_equal(entry_parent['ancestorcount'], 1) 140 assert_equal(entry_parent['ancestorsize'], parent_tx.get_vsize()) 141 assert_equal(entry_parent['descendantcount'], 2) 142 assert_equal(entry_parent['descendantsize'], parent_tx.get_vsize() + sigop_equivalent_vsize) 143 144 def test_sigops_package(self): 145 self.log.info("Test a overly-large sigops-vbyte hits package limits") 146 # Make a 2-transaction package which fails vbyte checks even though 147 # separately they would work. 148 self.restart_node(0, extra_args=["-bytespersigop=5000","-permitbaremultisig=1"]) 149 150 def create_bare_multisig_tx(utxo_to_spend=None): 151 _, pubkey = generate_keypair() 152 amount_for_bare = 50000 153 tx_dict = self.wallet.create_self_transfer(fee=Decimal("3"), utxo_to_spend=utxo_to_spend) 154 tx_utxo = tx_dict["new_utxo"] 155 tx = tx_dict["tx"] 156 tx.vout.append(CTxOut(amount_for_bare, keys_to_multisig_script([pubkey], k=1))) 157 tx.vout[0].nValue -= amount_for_bare 158 tx_utxo["txid"] = tx.txid_hex 159 tx_utxo["value"] -= Decimal("0.00005000") 160 return (tx_utxo, tx) 161 162 tx_parent_utxo, tx_parent = create_bare_multisig_tx() 163 _tx_child_utxo, tx_child = create_bare_multisig_tx(tx_parent_utxo) 164 165 # Separately, the parent tx is ok 166 parent_individual_testres = self.nodes[0].testmempoolaccept([tx_parent.serialize().hex()])[0] 167 assert parent_individual_testres["allowed"] 168 max_multisig_vsize = MAX_PUBKEYS_PER_MULTISIG * 5000 169 assert_equal(parent_individual_testres["vsize"], max_multisig_vsize) 170 171 # But together, it's exceeding limits in the *package* context. If sigops adjusted vsize wasn't being checked 172 # here, it would get further in validation and give too-large-cluster error instead. 173 packet_test = self.nodes[0].testmempoolaccept([tx_parent.serialize().hex(), tx_child.serialize().hex()]) 174 expected_package_error = "too-large-cluster" 175 assert_equal([x["package-error"] for x in packet_test], [expected_package_error] * 2) 176 177 # When we actually try to submit, the parent makes it into the mempool, but the child would exceed cluster vsize limits 178 res = self.nodes[0].submitpackage([tx_parent.serialize().hex(), tx_child.serialize().hex()]) 179 assert "too-large-cluster" in res["tx-results"][tx_child.wtxid_hex]["error"] 180 assert tx_parent.txid_hex in self.nodes[0].getrawmempool() 181 182 # Transactions are tiny in weight 183 assert_greater_than(2000, tx_parent.get_weight() + tx_child.get_weight()) 184 185 def test_legacy_sigops_stdness(self): 186 self.log.info("Test a transaction with too many legacy sigops in its inputs is non-standard.") 187 188 # Restart with the default settings 189 self.restart_node(0) 190 191 # Create a P2SH script with 15 sigops. 192 _, dummy_pubkey = generate_keypair() 193 packed_redeem_script = [dummy_pubkey] 194 for _ in range(MAX_STD_P2SH_SIGOPS - 1): 195 packed_redeem_script += [OP_2DUP, OP_CHECKSIG, OP_DROP] 196 packed_redeem_script = CScript(packed_redeem_script + [OP_CHECKSIG, OP_NOT]) 197 packed_p2sh_script = script_to_p2sh_script(packed_redeem_script) 198 199 # Create enough outputs to reach the sigops limit when spending them all at once. 200 outpoints = [] 201 for _ in range(int(MAX_STD_LEGACY_SIGOPS / MAX_STD_P2SH_SIGOPS) + 1): 202 res = self.wallet.send_to(from_node=self.nodes[0], scriptPubKey=packed_p2sh_script, amount=1_000) 203 txid = int.from_bytes(bytes.fromhex(res["txid"]), byteorder="big") 204 outpoints.append(COutPoint(txid, res["sent_vout"])) 205 self.generate(self.nodes[0], 1) 206 207 # Spending all these outputs at once accounts for 2505 legacy sigops and is non-standard. 208 nonstd_tx = CTransaction() 209 nonstd_tx.vin = [CTxIn(op, CScript([b"", packed_redeem_script])) for op in outpoints] 210 nonstd_tx.vout = [CTxOut(0, CScript([OP_RETURN, b""]))] 211 assert_raises_rpc_error(-26, "bad-txns-nonstandard-inputs", self.nodes[0].sendrawtransaction, nonstd_tx.serialize().hex()) 212 213 # Spending one less accounts for 2490 legacy sigops and is standard. 214 std_tx = deepcopy(nonstd_tx) 215 std_tx.vin.pop() 216 self.nodes[0].sendrawtransaction(std_tx.serialize().hex()) 217 218 # Make sure the original, non-standard, transaction can be mined. 219 self.generateblock(self.nodes[0], output="raw(42)", transactions=[nonstd_tx.serialize().hex()]) 220 221 def run_test(self): 222 self.wallet = MiniWallet(self.nodes[0]) 223 224 for bytes_per_sigop in (DEFAULT_BYTES_PER_SIGOP, 43, 81, 165, 327, 649, 1072): 225 if bytes_per_sigop == DEFAULT_BYTES_PER_SIGOP: 226 self.log.info(f"Test default sigops limit setting ({bytes_per_sigop} bytes per sigop)...") 227 else: 228 bytespersigop_parameter = f"-bytespersigop={bytes_per_sigop}" 229 self.log.info(f"Test sigops limit setting {bytespersigop_parameter}...") 230 self.restart_node(0, extra_args=[bytespersigop_parameter]) 231 232 for num_sigops in (69, 101, 142, 183, 222): 233 self.test_sigops_limit(bytes_per_sigop, num_sigops) 234 235 self.generate(self.wallet, 1) 236 237 self.test_sigops_package() 238 self.test_legacy_sigops_stdness() 239 240 241 if __name__ == '__main__': 242 BytesPerSigOpTest(__file__).main()