/ test / functional / mempool_sigoplimit.py
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()