Skip to content

Commit 89cd20c

Browse files
committed
test: add coverage for sigop limit policy (-bytespersigop setting)
1 parent be2e748 commit 89cd20c

File tree

2 files changed

+126
-0
lines changed

2 files changed

+126
-0
lines changed

test/functional/mempool_sigoplimit.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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()

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@
323323
'mempool_compatibility.py',
324324
'mempool_accept_wtxid.py',
325325
'mempool_dust.py',
326+
'mempool_sigoplimit.py',
326327
'rpc_deriveaddresses.py',
327328
'rpc_deriveaddresses.py --usecli',
328329
'p2p_ping.py',

0 commit comments

Comments
 (0)