|
| 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 math import ceil |
| 7 | + |
| 8 | +from test_framework.messages import ( |
| 9 | + COutPoint, |
| 10 | + CTransaction, |
| 11 | + CTxIn, |
| 12 | + CTxInWitness, |
| 13 | + CTxOut, |
| 14 | + WITNESS_SCALE_FACTOR, |
| 15 | +) |
| 16 | +from test_framework.script import ( |
| 17 | + CScript, |
| 18 | + OP_CHECKMULTISIG, |
| 19 | + OP_CHECKSIG, |
| 20 | + OP_ENDIF, |
| 21 | + OP_FALSE, |
| 22 | + OP_IF, |
| 23 | + OP_RETURN, |
| 24 | + OP_TRUE, |
| 25 | +) |
| 26 | +from test_framework.script_util import ( |
| 27 | + script_to_p2wsh_script, |
| 28 | +) |
| 29 | +from test_framework.test_framework import BitcoinTestFramework |
| 30 | +from test_framework.util import ( |
| 31 | + assert_equal, |
| 32 | + assert_greater_than_or_equal, |
| 33 | +) |
| 34 | +from test_framework.wallet import MiniWallet |
| 35 | + |
| 36 | + |
| 37 | +DEFAULT_BYTES_PER_SIGOP = 20 # default setting |
| 38 | + |
| 39 | + |
| 40 | +class BytesPerSigOpTest(BitcoinTestFramework): |
| 41 | + def set_test_params(self): |
| 42 | + self.num_nodes = 1 |
| 43 | + # allow large datacarrier output to pad transactions |
| 44 | + self.extra_args = [['-datacarriersize=100000']] |
| 45 | + |
| 46 | + def create_p2wsh_spending_tx(self, witness_script, output_script): |
| 47 | + """Create a 1-input-1-output P2WSH spending transaction with only the |
| 48 | + witness script in the witness stack and the given output script.""" |
| 49 | + # create P2WSH address and fund it via MiniWallet first |
| 50 | + txid, vout = self.wallet.send_to( |
| 51 | + from_node=self.nodes[0], |
| 52 | + scriptPubKey=script_to_p2wsh_script(witness_script), |
| 53 | + amount=1000000, |
| 54 | + ) |
| 55 | + |
| 56 | + # create spending transaction |
| 57 | + tx = CTransaction() |
| 58 | + tx.vin = [CTxIn(COutPoint(int(txid, 16), vout))] |
| 59 | + tx.wit.vtxinwit = [CTxInWitness()] |
| 60 | + tx.wit.vtxinwit[0].scriptWitness.stack = [bytes(witness_script)] |
| 61 | + tx.vout = [CTxOut(500000, output_script)] |
| 62 | + return tx |
| 63 | + |
| 64 | + def test_sigops_limit(self, bytes_per_sigop, num_sigops): |
| 65 | + sigop_equivalent_vsize = ceil(num_sigops * bytes_per_sigop / WITNESS_SCALE_FACTOR) |
| 66 | + self.log.info(f"- {num_sigops} sigops (equivalent size of {sigop_equivalent_vsize} vbytes)") |
| 67 | + |
| 68 | + # create a template tx with the specified sigop cost in the witness script |
| 69 | + # (note that the sigops count even though being in a branch that's not executed) |
| 70 | + num_multisigops = num_sigops // 20 |
| 71 | + num_singlesigops = num_sigops % 20 |
| 72 | + witness_script = CScript( |
| 73 | + [OP_FALSE, OP_IF] + |
| 74 | + [OP_CHECKMULTISIG]*num_multisigops + |
| 75 | + [OP_CHECKSIG]*num_singlesigops + |
| 76 | + [OP_ENDIF, OP_TRUE] |
| 77 | + ) |
| 78 | + # use a 256-byte data-push as lower bound in the output script, in order |
| 79 | + # to avoid having to compensate for tx size changes caused by varying |
| 80 | + # length serialization sizes (both for scriptPubKey and data-push lengths) |
| 81 | + tx = self.create_p2wsh_spending_tx(witness_script, CScript([OP_RETURN, b'X'*256])) |
| 82 | + |
| 83 | + # bump the tx to reach the sigop-limit equivalent size by padding the datacarrier output |
| 84 | + assert_greater_than_or_equal(sigop_equivalent_vsize, tx.get_vsize()) |
| 85 | + vsize_to_pad = sigop_equivalent_vsize - tx.get_vsize() |
| 86 | + tx.vout[0].scriptPubKey = CScript([OP_RETURN, b'X'*(256+vsize_to_pad)]) |
| 87 | + assert_equal(sigop_equivalent_vsize, tx.get_vsize()) |
| 88 | + |
| 89 | + res = self.nodes[0].testmempoolaccept([tx.serialize().hex()])[0] |
| 90 | + assert_equal(res['allowed'], True) |
| 91 | + assert_equal(res['vsize'], sigop_equivalent_vsize) |
| 92 | + |
| 93 | + # increase the tx's vsize to be right above the sigop-limit equivalent size |
| 94 | + # => tx's vsize in mempool should also grow accordingly |
| 95 | + tx.vout[0].scriptPubKey = CScript([OP_RETURN, b'X'*(256+vsize_to_pad+1)]) |
| 96 | + res = self.nodes[0].testmempoolaccept([tx.serialize().hex()])[0] |
| 97 | + assert_equal(res['allowed'], True) |
| 98 | + assert_equal(res['vsize'], sigop_equivalent_vsize+1) |
| 99 | + |
| 100 | + # decrease the tx's vsize to be right below the sigop-limit equivalent size |
| 101 | + # => tx's vsize in mempool should stick at the sigop-limit equivalent |
| 102 | + # bytes level, as it is higher than the tx's serialized vsize |
| 103 | + # (the maximum of both is taken) |
| 104 | + tx.vout[0].scriptPubKey = CScript([OP_RETURN, b'X'*(256+vsize_to_pad-1)]) |
| 105 | + res = self.nodes[0].testmempoolaccept([tx.serialize().hex()])[0] |
| 106 | + assert_equal(res['allowed'], True) |
| 107 | + assert_equal(res['vsize'], sigop_equivalent_vsize) |
| 108 | + |
| 109 | + def run_test(self): |
| 110 | + self.wallet = MiniWallet(self.nodes[0]) |
| 111 | + |
| 112 | + for bytes_per_sigop in (DEFAULT_BYTES_PER_SIGOP, 43, 81, 165, 327, 649, 1072): |
| 113 | + if bytes_per_sigop == DEFAULT_BYTES_PER_SIGOP: |
| 114 | + self.log.info(f"Test default sigops limit setting ({bytes_per_sigop} bytes per sigop)...") |
| 115 | + else: |
| 116 | + bytespersigop_parameter = f"-bytespersigop={bytes_per_sigop}" |
| 117 | + self.log.info(f"Test sigops limit setting {bytespersigop_parameter}...") |
| 118 | + self.restart_node(0, extra_args=[bytespersigop_parameter] + self.extra_args[0]) |
| 119 | + |
| 120 | + for num_sigops in (69, 101, 142, 183, 222): |
| 121 | + self.test_sigops_limit(bytes_per_sigop, num_sigops) |
| 122 | + |
| 123 | + |
| 124 | +if __name__ == '__main__': |
| 125 | + BytesPerSigOpTest().main() |
0 commit comments