Skip to content

Commit 16f5ad2

Browse files
committed
test: add functional test for OP_CAT spends
The goal of this functional test is to ensure OP_CAT spends are still disabled by default in segwitv0 and legacy spends. Spending such inputs should result in `mandatory-script-verify-flag-failed (Attempted to use a disabled opcode)`. While spending OP_CAT inputs in tapscript should be discouraged under the default `STANDARD_SCRIPT_VERIFY_FLAGS`.
1 parent 05efa9e commit 16f5ad2

File tree

2 files changed

+249
-0
lines changed

2 files changed

+249
-0
lines changed

test/functional/feature_opcat.py

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2014-2024 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 (OP_CAT)
6+
"""
7+
8+
from test_framework.blocktools import (
9+
create_coinbase,
10+
create_block,
11+
add_witness_commitment,
12+
)
13+
14+
from test_framework.messages import (
15+
CTransaction,
16+
CTxOut,
17+
CTxIn,
18+
CTxInWitness,
19+
COutPoint,
20+
COIN,
21+
sha256
22+
)
23+
from test_framework.address import (
24+
hash160,
25+
)
26+
from test_framework.p2p import P2PInterface
27+
from test_framework.script import (
28+
CScript,
29+
OP_CAT,
30+
OP_HASH160,
31+
OP_EQUAL,
32+
taproot_construct,
33+
)
34+
from test_framework.script_util import script_to_p2sh_script
35+
from test_framework.key import ECKey, compute_xonly_pubkey
36+
from test_framework.test_framework import BitcoinTestFramework
37+
from test_framework.util import assert_equal, assert_raises_rpc_error
38+
from test_framework.wallet import MiniWallet, MiniWalletMode
39+
from decimal import Decimal
40+
import random
41+
from io import BytesIO
42+
from test_framework.address import script_to_p2sh
43+
44+
DISABLED_OP_CODE = (
45+
"mandatory-script-verify-flag-failed (Attempted to use a disabled opcode)"
46+
)
47+
48+
DISCOURAGED_CAT_ERROR = (
49+
"OP_SUCCESSx reserved for soft-fork upgrades"
50+
)
51+
52+
53+
def random_bytes(n):
54+
return bytes(random.getrandbits(8) for i in range(n))
55+
56+
57+
def random_p2sh():
58+
return script_to_p2sh_script(random_bytes(20))
59+
60+
61+
def create_transaction_to_script(node, wallet, txid, script, *, amount_sats):
62+
"""Return signed transaction spending the first output of the
63+
input txid. Note that the node must be able to sign for the
64+
output that is being spent, and the node must not be running
65+
multiple wallets.
66+
"""
67+
random_address = script_to_p2sh(CScript())
68+
output = wallet.get_utxo(txid=txid)
69+
rawtx = node.createrawtransaction(
70+
inputs=[{"txid": output["txid"], "vout": output["vout"]}],
71+
outputs={random_address: Decimal(amount_sats) / COIN},
72+
)
73+
tx = CTransaction()
74+
tx.deserialize(BytesIO(bytes.fromhex(rawtx)))
75+
# Replace with our script
76+
tx.vout[0].scriptPubKey = script
77+
# Sign
78+
wallet.sign_tx(tx)
79+
return tx
80+
81+
82+
class CatTest(BitcoinTestFramework):
83+
def set_test_params(self):
84+
self.num_nodes = 1
85+
self.extra_args = [
86+
["-par=1"]
87+
] # Use only one script thread to get the exact reject reason for testing
88+
self.setup_clean_chain = True
89+
self.rpc_timeout = 120
90+
91+
def get_block(self, txs):
92+
self.tip = self.nodes[0].getbestblockhash()
93+
self.height = self.nodes[0].getblockcount()
94+
self.log.debug(self.height)
95+
block = create_block(
96+
int(self.tip, 16), create_coinbase(self.height + 1))
97+
block.vtx.extend(txs)
98+
add_witness_commitment(block)
99+
block.hashMerkleRoot = block.calc_merkle_root()
100+
block.solve()
101+
return block.serialize(True).hex(), block.hash
102+
103+
def add_block(self, txs):
104+
block, h = self.get_block(txs)
105+
reason = self.nodes[0].submitblock(block)
106+
if reason:
107+
self.log.debug("Reject Reason: [%s]", reason)
108+
assert_equal(self.nodes[0].getbestblockhash(), h)
109+
return h
110+
111+
def run_test(self):
112+
# The goal of this test suite is to rest OP_CAT is disabled by default in segwitv0 and p2sh script.
113+
# and discourage tapscript.
114+
115+
wallet = MiniWallet(self.nodes[0], mode=MiniWalletMode.RAW_P2PK)
116+
self.nodes[0].add_p2p_connection(P2PInterface())
117+
118+
BLOCKS = 200
119+
self.log.info("Mining %d blocks for mature coinbases", BLOCKS)
120+
# Drop the last 100 as they're unspendable!
121+
coinbase_txids = [
122+
self.nodes[0].getblock(b)["tx"][0]
123+
for b in self.generate(wallet, BLOCKS)[:-100]
124+
]
125+
def get_coinbase(): return coinbase_txids.pop()
126+
self.log.info("Creating setup transactions")
127+
128+
outputs = [CTxOut(i * 1000, random_p2sh()) for i in range(1, 11)]
129+
# Add some fee
130+
amount_sats = sum(out.nValue for out in outputs) + 200 * 500
131+
132+
private_key = ECKey()
133+
# use simple deterministic private key (k=1)
134+
private_key.set((1).to_bytes(32, "big"), False)
135+
assert private_key.is_valid
136+
public_key, _ = compute_xonly_pubkey(private_key.get_bytes())
137+
138+
op_cat_script = CScript([
139+
# Calling CAT on an empty stack
140+
# The content of the stack doesn't really matter for what we are testing
141+
# The interpreter should never get to the point where its executing this OP_CAT instruction
142+
OP_CAT,
143+
])
144+
145+
self.log.info("Creating a CAT tapscript funding tx")
146+
taproot_op_cat = taproot_construct(
147+
public_key, [("only-path", op_cat_script, 0xC0)])
148+
taproot_op_cat_funding_tx = create_transaction_to_script(
149+
self.nodes[0],
150+
wallet,
151+
get_coinbase(),
152+
taproot_op_cat.scriptPubKey,
153+
amount_sats=amount_sats,
154+
)
155+
156+
self.log.info("Creating a CAT segwit funding tx")
157+
segwit_cat_funding_tx = create_transaction_to_script(
158+
self.nodes[0],
159+
wallet,
160+
get_coinbase(),
161+
CScript([0, sha256(op_cat_script)]),
162+
amount_sats=amount_sats,
163+
)
164+
165+
self.log.info("Create p2sh OP_CAT funding tx")
166+
p2sh_cat_funding_tx = create_transaction_to_script(
167+
self.nodes[0],
168+
wallet,
169+
get_coinbase(),
170+
CScript([OP_HASH160, hash160(op_cat_script), OP_EQUAL]),
171+
amount_sats=amount_sats,
172+
)
173+
174+
funding_txs = [
175+
taproot_op_cat_funding_tx,
176+
segwit_cat_funding_tx,
177+
p2sh_cat_funding_tx
178+
]
179+
(
180+
taproot_op_cat_outpoint,
181+
segwit_op_cat_outpoint,
182+
bare_op_cat_outpoint,
183+
) = [COutPoint(tx.txid_int, 0) for tx in funding_txs]
184+
185+
self.log.info("Funding all outputs")
186+
self.add_block(funding_txs)
187+
188+
self.log.info("Testing tapscript OP_CAT usage is discouraged")
189+
taproot_op_cat_transaction = CTransaction()
190+
taproot_op_cat_transaction.vin = [
191+
CTxIn(taproot_op_cat_outpoint)]
192+
taproot_op_cat_transaction.vout = outputs
193+
taproot_op_cat_transaction.wit.vtxinwit += [
194+
CTxInWitness()]
195+
taproot_op_cat_transaction.wit.vtxinwit[0].scriptWitness.stack = [
196+
op_cat_script,
197+
bytes([0xC0 + taproot_op_cat.negflag]) +
198+
taproot_op_cat.internal_pubkey,
199+
]
200+
201+
assert_raises_rpc_error(
202+
-26,
203+
DISCOURAGED_CAT_ERROR,
204+
self.nodes[0].sendrawtransaction,
205+
taproot_op_cat_transaction.serialize().hex(),
206+
)
207+
self.log.info(
208+
"Tapscript OP_CAT spend not accepted by sendrawtransaction"
209+
)
210+
211+
self.log.info("Testing Segwitv0 OP_CAT usage is disabled")
212+
segwitv0_op_cat_transaction = CTransaction()
213+
segwitv0_op_cat_transaction.vin = [
214+
CTxIn(segwit_op_cat_outpoint)]
215+
segwitv0_op_cat_transaction.vout = outputs
216+
segwitv0_op_cat_transaction.wit.vtxinwit += [
217+
CTxInWitness()]
218+
segwitv0_op_cat_transaction.wit.vtxinwit[0].scriptWitness.stack = [
219+
op_cat_script,
220+
]
221+
222+
assert_raises_rpc_error(
223+
-26,
224+
DISABLED_OP_CODE,
225+
self.nodes[0].sendrawtransaction,
226+
segwitv0_op_cat_transaction.serialize().hex(),
227+
)
228+
self.log.info("Segwitv0 OP_CAT spend failed with expected error")
229+
230+
self.log.info("Testing p2sh script OP_CAT usage is disabled")
231+
p2sh_op_cat_transaction = CTransaction()
232+
p2sh_op_cat_transaction.vin = [
233+
CTxIn(bare_op_cat_outpoint)]
234+
p2sh_op_cat_transaction.vin[0].scriptSig = CScript(
235+
[op_cat_script])
236+
p2sh_op_cat_transaction.vout = outputs
237+
238+
assert_raises_rpc_error(
239+
-26,
240+
DISABLED_OP_CODE,
241+
self.nodes[0].sendrawtransaction,
242+
p2sh_op_cat_transaction.serialize().hex(),
243+
)
244+
self.log.info("p2sh OP_CAT spend failed with expected error")
245+
246+
247+
if __name__ == "__main__":
248+
CatTest(__file__).main()

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@
354354
'rpc_mempool_info.py',
355355
'rpc_help.py',
356356
'tool_rpcauth.py',
357+
'feature_opcat.py',
357358
'p2p_handshake.py',
358359
'p2p_handshake.py --v2transport',
359360
'feature_dirsymlinks.py',

0 commit comments

Comments
 (0)