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