Skip to content

Commit a118743

Browse files
0xBEEFCAF3ajtowns
authored andcommitted
functional tests for OP_CAT
1 parent 76b12ab commit a118743

File tree

2 files changed

+343
-0
lines changed

2 files changed

+343
-0
lines changed

test/functional/feature_opcat.py

+342
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,342 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2015-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+
from test_framework.messages import (
14+
CTransaction,
15+
CTxOut,
16+
CTxIn,
17+
CTxInWitness,
18+
COutPoint,
19+
COIN,
20+
sha256,
21+
)
22+
from test_framework.p2p import P2PInterface
23+
from test_framework.script import (
24+
CScript,
25+
OP_CAT,
26+
OP_EQUAL,
27+
OP_2,
28+
OP_DUP,
29+
taproot_construct,
30+
)
31+
from test_framework.script_util import script_to_p2sh_script
32+
from test_framework.key import ECKey, compute_xonly_pubkey
33+
from test_framework.test_framework import BitcoinTestFramework
34+
from test_framework.util import assert_equal, assert_raises_rpc_error
35+
from test_framework.wallet import MiniWallet, MiniWalletMode
36+
from decimal import Decimal
37+
import random
38+
from io import BytesIO
39+
from test_framework.address import script_to_p2sh
40+
41+
DISCOURAGED_ERROR = (
42+
"non-mandatory-script-verify-flag (NOPx reserved for soft-fork upgrades)"
43+
)
44+
STACK_TOO_SHORT_ERROR = (
45+
"non-mandatory-script-verify-flag (Operation not valid with the current stack size)"
46+
)
47+
DISABLED_OP_CODE = (
48+
"mandatory-script-verify-flag-failed (Attempted to use a disabled opcode)"
49+
)
50+
MAX_PUSH_ERROR = (
51+
"non-mandatory-script-verify-flag (Push value size limit exceeded)"
52+
)
53+
54+
def random_bytes(n):
55+
return bytes(random.getrandbits(8) for i in range(n))
56+
57+
58+
def random_p2sh():
59+
return script_to_p2sh_script(random_bytes(20))
60+
61+
62+
def create_transaction_to_script(node, wallet, txid, script, *, amount_sats):
63+
"""Return signed transaction spending the first output of the
64+
input txid. Note that the node must be able to sign for the
65+
output that is being spent, and the node must not be running
66+
multiple wallets.
67+
"""
68+
random_address = script_to_p2sh(CScript())
69+
output = wallet.get_utxo(txid=txid)
70+
rawtx = node.createrawtransaction(
71+
inputs=[{"txid": output["txid"], "vout": output["vout"]}],
72+
outputs={random_address: Decimal(amount_sats) / COIN},
73+
)
74+
tx = CTransaction()
75+
tx.deserialize(BytesIO(bytes.fromhex(rawtx)))
76+
# Replace with our script
77+
tx.vout[0].scriptPubKey = script
78+
# Sign
79+
wallet.sign_tx(tx)
80+
return tx
81+
82+
83+
class CatTest(BitcoinTestFramework):
84+
def set_test_params(self):
85+
self.num_nodes = 1
86+
self.extra_args = [
87+
["-par=1"]
88+
] # Use only one script thread to get the exact reject reason for testing
89+
self.setup_clean_chain = True
90+
self.rpc_timeout = 120
91+
92+
def get_block(self, txs):
93+
self.tip = self.nodes[0].getbestblockhash()
94+
self.height = self.nodes[0].getblockcount()
95+
self.log.debug(self.height)
96+
block = create_block(
97+
int(self.tip, 16), create_coinbase(self.height + 1))
98+
block.vtx.extend(txs)
99+
add_witness_commitment(block)
100+
block.hashMerkleRoot = block.calc_merkle_root()
101+
block.solve()
102+
return block.serialize(True).hex(), block.hash
103+
104+
def add_block(self, txs):
105+
block, h = self.get_block(txs)
106+
reason = self.nodes[0].submitblock(block)
107+
if reason:
108+
self.log.debug("Reject Reason: [%s]", reason)
109+
assert_equal(self.nodes[0].getbestblockhash(), h)
110+
return h
111+
112+
def run_test(self):
113+
# The goal is to test a number of circumstances and combinations of parameters. Roughly:
114+
#
115+
# - Taproot OP_CAT
116+
# - CAT should fail when stack limit is hit
117+
# - CAT should fail if there is insuffecient number of element on the stack
118+
# - CAT should be able to concatenate two 8 byte payloads and check the resulting 16 byte payload
119+
# - Segwit v0 OP_CAT
120+
# - Spend should fail due to using disabled opcodes
121+
122+
wallet = MiniWallet(self.nodes[0], mode=MiniWalletMode.RAW_P2PK)
123+
self.nodes[0].add_p2p_connection(P2PInterface())
124+
125+
BLOCKS = 200
126+
self.log.info("Mining %d blocks for mature coinbases", BLOCKS)
127+
# Drop the last 100 as they're unspendable!
128+
coinbase_txids = [
129+
self.nodes[0].getblock(b)["tx"][0]
130+
for b in self.generate(wallet, BLOCKS)[:-100]
131+
]
132+
def get_coinbase(): return coinbase_txids.pop()
133+
self.log.info("Creating setup transactions")
134+
135+
outputs = [CTxOut(i * 1000, random_p2sh()) for i in range(1, 11)]
136+
# Add some fee satoshis
137+
amount_sats = sum(out.nValue for out in outputs) + 200 * 500
138+
139+
self.log.info(
140+
"Creating funding txn for 10 random outputs as a Taproot script")
141+
private_key = ECKey()
142+
# use simple deterministic private key (k=1)
143+
private_key.set((1).to_bytes(32, "big"), False)
144+
assert private_key.is_valid
145+
public_key, _ = compute_xonly_pubkey(private_key.get_bytes())
146+
147+
self.log.info(
148+
"Creating CAT tx with not enough values on the stack")
149+
not_enough_stack_elements_script = CScript([OP_CAT, OP_EQUAL, OP_2])
150+
taproot_not_enough_stack_elements = taproot_construct(
151+
public_key, [("only-path", not_enough_stack_elements_script, 0xC0)])
152+
taproot_not_enough_stack_elements_funding_tx = create_transaction_to_script(
153+
self.nodes[0],
154+
wallet,
155+
get_coinbase(),
156+
taproot_not_enough_stack_elements.scriptPubKey,
157+
amount_sats=amount_sats,
158+
)
159+
160+
self.log.info(
161+
"Creating CAT tx that exceeds the stack element limit size")
162+
# Convert hex value to bytes
163+
hex_bytes = bytes.fromhex(('00' * 8))
164+
stack_limit_script = CScript(
165+
[
166+
hex_bytes,
167+
OP_DUP,
168+
OP_CAT,
169+
# 16 bytes on the stack
170+
OP_DUP,
171+
OP_CAT,
172+
# 32 bytes on the stack
173+
OP_DUP,
174+
OP_CAT,
175+
# 64 bytes on the stack
176+
OP_DUP,
177+
OP_CAT,
178+
# 128 bytes on the stack
179+
OP_DUP,
180+
OP_CAT,
181+
# 256 bytes on the stack
182+
OP_DUP,
183+
OP_CAT,
184+
# 512 bytes on the stack
185+
OP_DUP,
186+
OP_CAT,
187+
])
188+
189+
taproot_stack_limit = taproot_construct(
190+
public_key, [("only-path", stack_limit_script, 0xC0)])
191+
taproot_stack_limit_funding_tx = create_transaction_to_script(
192+
self.nodes[0],
193+
wallet,
194+
get_coinbase(),
195+
taproot_stack_limit.scriptPubKey,
196+
amount_sats=amount_sats,
197+
)
198+
self.log.info(
199+
"Creating CAT tx that concatenates to values and verifies")
200+
hex_value_verify = bytes.fromhex('00' * 16)
201+
op_cat_verify_script = CScript([
202+
hex_bytes,
203+
OP_DUP,
204+
OP_CAT,
205+
hex_value_verify,
206+
OP_EQUAL,
207+
])
208+
209+
taproot_op_cat = taproot_construct(
210+
public_key, [("only-path", op_cat_verify_script, 0xC0)])
211+
taproot_op_cat_funding_tx = create_transaction_to_script(
212+
self.nodes[0],
213+
wallet,
214+
get_coinbase(),
215+
taproot_op_cat.scriptPubKey,
216+
amount_sats=amount_sats,
217+
)
218+
219+
self.log.info("Creating a CAT segwit funding tx")
220+
segwit_cat_funding_tx = create_transaction_to_script(
221+
self.nodes[0],
222+
wallet,
223+
get_coinbase(),
224+
CScript([0, sha256(op_cat_verify_script)]),
225+
amount_sats=amount_sats,
226+
)
227+
228+
funding_txs = [
229+
taproot_not_enough_stack_elements_funding_tx,
230+
taproot_stack_limit_funding_tx,
231+
taproot_op_cat_funding_tx,
232+
segwit_cat_funding_tx,
233+
]
234+
self.log.info("Obtaining TXIDs")
235+
(
236+
taproot_not_enough_stack_elements_outpoint,
237+
taproot_stack_limit_outpoint,
238+
taproot_op_cat_outpoint,
239+
segwit_op_cat_outpoint,
240+
) = [COutPoint(int(tx.rehash(), 16), 0) for tx in funding_txs]
241+
242+
self.log.info("Funding all outputs")
243+
self.add_block(funding_txs)
244+
245+
self.log.info("Testing Taproot not enough stack elements OP_CAT spend")
246+
# Test sendrawtransaction
247+
taproot_op_cat_not_enough_stack_elements_spend = CTransaction()
248+
taproot_op_cat_not_enough_stack_elements_spend.version = 2
249+
taproot_op_cat_not_enough_stack_elements_spend.vin = [
250+
CTxIn(taproot_not_enough_stack_elements_outpoint)]
251+
taproot_op_cat_not_enough_stack_elements_spend.vout = outputs
252+
taproot_op_cat_not_enough_stack_elements_spend.wit.vtxinwit += [
253+
CTxInWitness()]
254+
taproot_op_cat_not_enough_stack_elements_spend.wit.vtxinwit[0].scriptWitness.stack = [
255+
not_enough_stack_elements_script,
256+
bytes([0xC0 + taproot_not_enough_stack_elements.negflag]) +
257+
taproot_not_enough_stack_elements.internal_pubkey,
258+
]
259+
260+
assert_raises_rpc_error(
261+
-26,
262+
STACK_TOO_SHORT_ERROR,
263+
self.nodes[0].sendrawtransaction,
264+
taproot_op_cat_not_enough_stack_elements_spend.serialize().hex(),
265+
)
266+
self.log.info(
267+
"OP_CAT with wrong size stack rejected by sendrawtransaction as discouraged"
268+
)
269+
270+
self.log.info("Testing Taproot tx with stack element size limit")
271+
taproot_op_cat_stack_limit_spend = CTransaction()
272+
taproot_op_cat_stack_limit_spend.version = 2
273+
taproot_op_cat_stack_limit_spend.vin = [
274+
CTxIn(taproot_stack_limit_outpoint)]
275+
taproot_op_cat_stack_limit_spend.vout = outputs
276+
taproot_op_cat_stack_limit_spend.wit.vtxinwit += [
277+
CTxInWitness()]
278+
taproot_op_cat_stack_limit_spend.wit.vtxinwit[0].scriptWitness.stack = [
279+
stack_limit_script,
280+
bytes([0xC0 + taproot_stack_limit.negflag]) +
281+
taproot_stack_limit.internal_pubkey,
282+
]
283+
284+
assert_raises_rpc_error(
285+
-26,
286+
MAX_PUSH_ERROR,
287+
self.nodes[0].sendrawtransaction,
288+
taproot_op_cat_stack_limit_spend.serialize().hex(),
289+
)
290+
self.log.info(
291+
"OP_CAT with stack size limit rejected by sendrawtransaction as discouraged"
292+
)
293+
294+
self.log.info("Testing Taproot OP_CAT usage")
295+
taproot_op_cat_transaction = CTransaction()
296+
taproot_op_cat_transaction.version = 2
297+
taproot_op_cat_transaction.vin = [
298+
CTxIn(taproot_op_cat_outpoint)]
299+
taproot_op_cat_transaction.vout = outputs
300+
taproot_op_cat_transaction.wit.vtxinwit += [
301+
CTxInWitness()]
302+
taproot_op_cat_transaction.wit.vtxinwit[0].scriptWitness.stack = [
303+
op_cat_verify_script,
304+
bytes([0xC0 + taproot_op_cat.negflag]) +
305+
taproot_op_cat.internal_pubkey,
306+
]
307+
308+
assert_equal(
309+
self.nodes[0].sendrawtransaction(
310+
taproot_op_cat_transaction.serialize().hex()),
311+
taproot_op_cat_transaction.rehash(),
312+
)
313+
self.log.info(
314+
"Taproot OP_CAT verify spend accepted by sendrawtransaction"
315+
)
316+
self.add_block([taproot_op_cat_transaction])
317+
318+
self.log.info("Testing Segwitv0 CAT usage")
319+
segwitv0_op_cat_transaction = CTransaction()
320+
segwitv0_op_cat_transaction.version = 2
321+
segwitv0_op_cat_transaction.vin = [
322+
CTxIn(segwit_op_cat_outpoint)]
323+
segwitv0_op_cat_transaction.vout = outputs
324+
segwitv0_op_cat_transaction.wit.vtxinwit += [
325+
CTxInWitness()]
326+
segwitv0_op_cat_transaction.wit.vtxinwit[0].scriptWitness.stack = [
327+
op_cat_verify_script,
328+
]
329+
330+
assert_raises_rpc_error(
331+
-26,
332+
DISABLED_OP_CODE,
333+
self.nodes[0].sendrawtransaction,
334+
segwitv0_op_cat_transaction.serialize().hex(),
335+
)
336+
self.log.info(
337+
"allowed by consensus, disallowed by relay policy"
338+
)
339+
340+
341+
if __name__ == "__main__":
342+
CatTest(__file__).main()

test/functional/test_runner.py

+1
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,7 @@
302302
'p2p_initial_headers_sync.py',
303303
'feature_nulldummy.py',
304304
'feature_checktemplateverify.py',
305+
'feature_opcat.py',
305306
'mempool_accept.py',
306307
'mempool_expiry.py',
307308
'wallet_import_with_label.py --legacy-wallet',

0 commit comments

Comments
 (0)