diff options
author | fanquake <fanquake@gmail.com> | 2023-03-10 14:28:21 +0100 |
---|---|---|
committer | fanquake <fanquake@gmail.com> | 2023-03-10 14:34:34 +0100 |
commit | 3e7dd4ff3369d808eb6003d16a94e0ff8292d11d (patch) | |
tree | f90ff525fe4fa280c5615e369758cee52c4baae3 /test | |
parent | 6f5eb7a39e719a8d762b2de66ad475eb3105522c (diff) | |
parent | 89cd20cbedbba344bab92dd1d71dac9c84320a70 (diff) | |
download | bitcoin-3e7dd4ff3369d808eb6003d16a94e0ff8292d11d.tar.xz |
Merge bitcoin/bitcoin#27171: test: add coverage for sigop limit policy (`-bytespersigop` setting)
89cd20cbedbba344bab92dd1d71dac9c84320a70 test: add coverage for sigop limit policy (`-bytespersigop` setting) (Sebastian Falbesoner)
Pull request description:
This PR adds missing test coverage for the `-bytespersigop` option, which determines how pre-taproot signature operations (OP_CHECKSIG{VERIFY}, OP_CHECKMULTIGSIG{VERIFY}) affect fee handling calculations. The setting was introduced in PR #7081 for mitigating the [sigop spam attack](https://bitcointalk.org/index.php?topic=1166928.0); the initial implementation rejected txs exceeding the limit, but was changed in #8365 later to account for higher sizes in the mempool (i.e. exceeding the sigop limit is possible, but has to be compensated by higher fees).
For each combination of `-bytespersigop` setting and sigops count, the test first creates a P2WSH spending transaction with a witness script that puts sigops in a non-executing branch (OP_FALSE OP_IF OP_CHECKMULTISIG ... OP_CHECKSIG ... OP_ENDIF). This tx is then bumped up to reach exactly the _sig-op limit equivalent vsize_ by padding its datacarrier output. Based on that, increasing the tx's vsize should still reflect a vsize increase in the mempool, while a decrease of the tx's vsize should lead to the mempool treating the tx's vsize to be the _sig-op limit equivalent vsize_, since the limit was exceeded.
I assume that this parameter is almost never set explicitly by users (also it is not relevant for taproot spends), but it doesn't hurt to have a test for it. See also https://bitcoin.stackexchange.com/a/87958 for another explanation.
ACKs for top commit:
glozow:
light review ACK 89cd20cbed
MarcoFalke:
nice ACK 89cd20cbedbba344bab92dd1d71dac9c84320a70 📁
Tree-SHA512: 06998ce93bf9d5ce6143db2996a43f13990c415f97afe684227ad469349e73952bf4f6c871c1e6349e07606f4d45db64408848873a86a89481cdca5a134e5e60
Diffstat (limited to 'test')
-rwxr-xr-x | test/functional/mempool_sigoplimit.py | 125 | ||||
-rwxr-xr-x | test/functional/test_runner.py | 1 |
2 files changed, 126 insertions, 0 deletions
diff --git a/test/functional/mempool_sigoplimit.py b/test/functional/mempool_sigoplimit.py new file mode 100755 index 0000000000..87934da681 --- /dev/null +++ b/test/functional/mempool_sigoplimit.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +# Copyright (c) 2023 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test sigop limit mempool policy (`-bytespersigop` parameter)""" +from math import ceil + +from test_framework.messages import ( + COutPoint, + CTransaction, + CTxIn, + CTxInWitness, + CTxOut, + WITNESS_SCALE_FACTOR, +) +from test_framework.script import ( + CScript, + OP_CHECKMULTISIG, + OP_CHECKSIG, + OP_ENDIF, + OP_FALSE, + OP_IF, + OP_RETURN, + OP_TRUE, +) +from test_framework.script_util import ( + script_to_p2wsh_script, +) +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + assert_greater_than_or_equal, +) +from test_framework.wallet import MiniWallet + + +DEFAULT_BYTES_PER_SIGOP = 20 # default setting + + +class BytesPerSigOpTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 1 + # allow large datacarrier output to pad transactions + self.extra_args = [['-datacarriersize=100000']] + + def create_p2wsh_spending_tx(self, witness_script, output_script): + """Create a 1-input-1-output P2WSH spending transaction with only the + witness script in the witness stack and the given output script.""" + # create P2WSH address and fund it via MiniWallet first + txid, vout = self.wallet.send_to( + from_node=self.nodes[0], + scriptPubKey=script_to_p2wsh_script(witness_script), + amount=1000000, + ) + + # create spending transaction + tx = CTransaction() + tx.vin = [CTxIn(COutPoint(int(txid, 16), vout))] + tx.wit.vtxinwit = [CTxInWitness()] + tx.wit.vtxinwit[0].scriptWitness.stack = [bytes(witness_script)] + tx.vout = [CTxOut(500000, output_script)] + return tx + + def test_sigops_limit(self, bytes_per_sigop, num_sigops): + sigop_equivalent_vsize = ceil(num_sigops * bytes_per_sigop / WITNESS_SCALE_FACTOR) + self.log.info(f"- {num_sigops} sigops (equivalent size of {sigop_equivalent_vsize} vbytes)") + + # create a template tx with the specified sigop cost in the witness script + # (note that the sigops count even though being in a branch that's not executed) + num_multisigops = num_sigops // 20 + num_singlesigops = num_sigops % 20 + witness_script = CScript( + [OP_FALSE, OP_IF] + + [OP_CHECKMULTISIG]*num_multisigops + + [OP_CHECKSIG]*num_singlesigops + + [OP_ENDIF, OP_TRUE] + ) + # use a 256-byte data-push as lower bound in the output script, in order + # to avoid having to compensate for tx size changes caused by varying + # length serialization sizes (both for scriptPubKey and data-push lengths) + tx = self.create_p2wsh_spending_tx(witness_script, CScript([OP_RETURN, b'X'*256])) + + # bump the tx to reach the sigop-limit equivalent size by padding the datacarrier output + assert_greater_than_or_equal(sigop_equivalent_vsize, tx.get_vsize()) + vsize_to_pad = sigop_equivalent_vsize - tx.get_vsize() + tx.vout[0].scriptPubKey = CScript([OP_RETURN, b'X'*(256+vsize_to_pad)]) + assert_equal(sigop_equivalent_vsize, tx.get_vsize()) + + res = self.nodes[0].testmempoolaccept([tx.serialize().hex()])[0] + assert_equal(res['allowed'], True) + assert_equal(res['vsize'], sigop_equivalent_vsize) + + # increase the tx's vsize to be right above the sigop-limit equivalent size + # => tx's vsize in mempool should also grow accordingly + tx.vout[0].scriptPubKey = CScript([OP_RETURN, b'X'*(256+vsize_to_pad+1)]) + res = self.nodes[0].testmempoolaccept([tx.serialize().hex()])[0] + assert_equal(res['allowed'], True) + assert_equal(res['vsize'], sigop_equivalent_vsize+1) + + # decrease the tx's vsize to be right below the sigop-limit equivalent size + # => tx's vsize in mempool should stick at the sigop-limit equivalent + # bytes level, as it is higher than the tx's serialized vsize + # (the maximum of both is taken) + tx.vout[0].scriptPubKey = CScript([OP_RETURN, b'X'*(256+vsize_to_pad-1)]) + res = self.nodes[0].testmempoolaccept([tx.serialize().hex()])[0] + assert_equal(res['allowed'], True) + assert_equal(res['vsize'], sigop_equivalent_vsize) + + def run_test(self): + self.wallet = MiniWallet(self.nodes[0]) + + for bytes_per_sigop in (DEFAULT_BYTES_PER_SIGOP, 43, 81, 165, 327, 649, 1072): + if bytes_per_sigop == DEFAULT_BYTES_PER_SIGOP: + self.log.info(f"Test default sigops limit setting ({bytes_per_sigop} bytes per sigop)...") + else: + bytespersigop_parameter = f"-bytespersigop={bytes_per_sigop}" + self.log.info(f"Test sigops limit setting {bytespersigop_parameter}...") + self.restart_node(0, extra_args=[bytespersigop_parameter] + self.extra_args[0]) + + for num_sigops in (69, 101, 142, 183, 222): + self.test_sigops_limit(bytes_per_sigop, num_sigops) + + +if __name__ == '__main__': + BytesPerSigOpTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 1d560706c2..f8869ae6bc 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -323,6 +323,7 @@ BASE_SCRIPTS = [ 'mempool_compatibility.py', 'mempool_accept_wtxid.py', 'mempool_dust.py', + 'mempool_sigoplimit.py', 'rpc_deriveaddresses.py', 'rpc_deriveaddresses.py --usecli', 'p2p_ping.py', |