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