mempool_sigoplimit.py raw
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, non-witness sigops exceed bip54 limit",
212 self.nodes[0].sendrawtransaction, nonstd_tx.serialize().hex())
213
214 # Spending one less accounts for 2490 legacy sigops and is standard.
215 std_tx = deepcopy(nonstd_tx)
216 std_tx.vin.pop()
217 self.nodes[0].sendrawtransaction(std_tx.serialize().hex())
218
219 # Make sure the original, non-standard, transaction can be mined.
220 self.generateblock(self.nodes[0], output="raw(42)", transactions=[nonstd_tx.serialize().hex()])
221
222 def run_test(self):
223 self.wallet = MiniWallet(self.nodes[0])
224
225 for bytes_per_sigop in (DEFAULT_BYTES_PER_SIGOP, 43, 81, 165, 327, 649, 1072):
226 if bytes_per_sigop == DEFAULT_BYTES_PER_SIGOP:
227 self.log.info(f"Test default sigops limit setting ({bytes_per_sigop} bytes per sigop)...")
228 else:
229 bytespersigop_parameter = f"-bytespersigop={bytes_per_sigop}"
230 self.log.info(f"Test sigops limit setting {bytespersigop_parameter}...")
231 self.restart_node(0, extra_args=[bytespersigop_parameter])
232
233 for num_sigops in (69, 101, 142, 183, 222):
234 self.test_sigops_limit(bytes_per_sigop, num_sigops)
235
236 self.generate(self.wallet, 1)
237
238 self.test_sigops_package()
239 self.test_legacy_sigops_stdness()
240
241
242 if __name__ == '__main__':
243 BytesPerSigOpTest(__file__).main()
244