|
| 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() |
0 commit comments