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