mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-04-29 06:49:38 -04:00
test: add OP_CHECKTEMPLATEVERIFY functional tests
Co-authored-by: James O'Beirne <github@au92.org>
This commit is contained in:
parent
2da4f34da9
commit
2b74adfb20
4 changed files with 755 additions and 2 deletions
738
test/functional/feature_checktemplateverify.py
Executable file
738
test/functional/feature_checktemplateverify.py
Executable file
|
@ -0,0 +1,738 @@
|
|||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2015-2021 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 OP_CHECKTEMPLATEVERIFY (BIP-119)
|
||||
"""
|
||||
|
||||
from test_framework.blocktools import (
|
||||
create_coinbase,
|
||||
create_block,
|
||||
add_witness_commitment,
|
||||
)
|
||||
from test_framework.messages import (
|
||||
CTransaction,
|
||||
CTxOut,
|
||||
CTxIn,
|
||||
CTxInWitness,
|
||||
COutPoint,
|
||||
COIN,
|
||||
sha256,
|
||||
)
|
||||
from test_framework.p2p import P2PInterface
|
||||
from test_framework.script import (
|
||||
CScript,
|
||||
OP_TRUE,
|
||||
OP_DEPTH,
|
||||
OP_ENDIF,
|
||||
OP_IF,
|
||||
OP_CHECKTEMPLATEVERIFY,
|
||||
OP_FALSE,
|
||||
OP_DROP,
|
||||
taproot_construct,
|
||||
)
|
||||
from test_framework.script_util import script_to_p2sh_script
|
||||
from test_framework.key import ECKey, compute_xonly_pubkey
|
||||
from test_framework.test_framework import BitcoinTestFramework
|
||||
from test_framework.util import assert_equal, assert_raises_rpc_error
|
||||
from test_framework.wallet import MiniWallet, MiniWalletMode
|
||||
from decimal import Decimal
|
||||
import random
|
||||
from io import BytesIO
|
||||
from test_framework.address import script_to_p2sh
|
||||
|
||||
CHECKTEMPLATEVERIFY_ERROR = "mandatory-script-verify-flag-failed (Script failed an OP_CHECKTEMPLATEVERIFY operation)"
|
||||
DISCOURAGED_ERROR = (
|
||||
"non-mandatory-script-verify-flag (NOPx reserved for soft-fork upgrades)"
|
||||
)
|
||||
STACK_TOO_SHORT_ERROR = (
|
||||
"mandatory-script-verify-flag-failed (Operation not valid with the current stack size)"
|
||||
)
|
||||
|
||||
|
||||
def random_bytes(n):
|
||||
return bytes(random.getrandbits(8) for i in range(n))
|
||||
|
||||
|
||||
def template_hash_for_outputs(outputs, nIn=0, nVin=1, vin_override=None):
|
||||
c = CTransaction()
|
||||
c.version = 2
|
||||
c.vin = vin_override
|
||||
if vin_override is None:
|
||||
c.vin = [CTxIn()] * nVin
|
||||
c.vout = outputs
|
||||
return c.get_standard_template_hash(nIn)
|
||||
|
||||
|
||||
def random_p2sh():
|
||||
return script_to_p2sh_script(random_bytes(20))
|
||||
|
||||
|
||||
def random_real_outputs_and_script(n, nIn=0, nVin=1, vin_override=None):
|
||||
outputs = [CTxOut((x + 1) * 1000, random_p2sh()) for x in range(n)]
|
||||
script = CScript(
|
||||
[
|
||||
template_hash_for_outputs(outputs, nIn, nVin, vin_override),
|
||||
OP_CHECKTEMPLATEVERIFY,
|
||||
]
|
||||
)
|
||||
return outputs, script
|
||||
|
||||
|
||||
def random_secure_tree(depth):
|
||||
leaf_nodes = [
|
||||
CTxOut(nValue=100, scriptPubKey=CScript(bytes([0, 0x14]) + random_bytes(20)))
|
||||
for x in range(2 ** depth)
|
||||
]
|
||||
outputs_tree = [[CTxOut()] * (2 ** i) for i in range(depth)] + [leaf_nodes]
|
||||
for d in range(1, depth + 2):
|
||||
idxs = zip(
|
||||
range(0, len(outputs_tree[-d]), 2), range(1, len(outputs_tree[-d]), 2)
|
||||
)
|
||||
for (idx, (a, b)) in enumerate(
|
||||
[(outputs_tree[-d][i], outputs_tree[-d][j]) for (i, j) in idxs]
|
||||
):
|
||||
s = CScript(
|
||||
bytes([0x20])
|
||||
+ template_hash_for_outputs([a, b])
|
||||
+ bytes([OP_CHECKTEMPLATEVERIFY])
|
||||
)
|
||||
a = sum(o.nValue for o in [a, b])
|
||||
t = CTxOut(a + 1000, s)
|
||||
outputs_tree[-d - 1][idx] = t
|
||||
return outputs_tree
|
||||
|
||||
|
||||
def create_transaction_to_script(node, wallet, txid, script, *, amount_sats):
|
||||
"""
|
||||
Return signed transaction spending the first output of the
|
||||
input txid. Note that the node must be able to sign for the
|
||||
output that is being spent, and the node must not be running
|
||||
multiple wallets.
|
||||
"""
|
||||
random_address = script_to_p2sh(CScript())
|
||||
output = wallet.get_utxo(txid=txid)
|
||||
rawtx = node.createrawtransaction(
|
||||
inputs=[{"txid": output["txid"], "vout": output["vout"]}],
|
||||
outputs={random_address: Decimal(amount_sats) / COIN},
|
||||
)
|
||||
tx = CTransaction()
|
||||
tx.deserialize(BytesIO(bytes.fromhex(rawtx)))
|
||||
# Replace with our script
|
||||
tx.vout[0].scriptPubKey = script
|
||||
# Sign
|
||||
wallet.sign_tx(tx)
|
||||
return tx
|
||||
|
||||
|
||||
class CheckTemplateVerifyTest(BitcoinTestFramework):
|
||||
def set_test_params(self):
|
||||
self.num_nodes = 1
|
||||
# Use only one script thread to get the exact reject reason for testing
|
||||
self.extra_args = [
|
||||
["-par=1"]
|
||||
]
|
||||
self.setup_clean_chain = True
|
||||
self.rpc_timeout = 120
|
||||
|
||||
def get_block(self, txs):
|
||||
self.tip = self.nodes[0].getbestblockhash()
|
||||
self.height = self.nodes[0].getblockcount()
|
||||
self.log.debug(self.height)
|
||||
block = create_block(int(self.tip, 16), create_coinbase(self.height + 1))
|
||||
block.vtx.extend(txs)
|
||||
add_witness_commitment(block)
|
||||
block.hashMerkleRoot = block.calc_merkle_root()
|
||||
block.solve()
|
||||
return block.serialize(True).hex(), block.hash
|
||||
|
||||
def add_block(self, txs):
|
||||
block, h = self.get_block(txs)
|
||||
reason = self.nodes[0].submitblock(block)
|
||||
if reason:
|
||||
self.log.debug("Reject Reason: [%s]", reason)
|
||||
assert_equal(self.nodes[0].getbestblockhash(), h)
|
||||
return h
|
||||
|
||||
def fail_block(self, txs, cause=CHECKTEMPLATEVERIFY_ERROR):
|
||||
block, h = self.get_block(txs)
|
||||
assert_equal(self.nodes[0].submitblock(block), cause)
|
||||
assert_equal(self.nodes[0].getbestblockhash(), self.tip)
|
||||
|
||||
def run_test(self):
|
||||
"""
|
||||
The goal is to test a number of circumstances and combinations of parameters. Roughly:
|
||||
|
||||
- Taproot OP_CTV
|
||||
- SegWit OP_CTV
|
||||
- SegWit OP_CTV wrong size on stack
|
||||
- fails policy
|
||||
- passes consensus
|
||||
- SegWit OP_CTV no argument in stack from program
|
||||
- fails policy and consensus with empty stack
|
||||
- passes consensus and policy when argument is the correct hash
|
||||
- passes consensus when argument is non 32 bytes
|
||||
- fails policy when argument is non 32 bytes
|
||||
- P2SH OP_CTV (impossible to spend w/o hash cycle!)
|
||||
- Bare OP_CTV
|
||||
- OP_CTV at vin index 0
|
||||
- OP_CTV at vin index > 0
|
||||
- OP_CTV with scriptSigs set
|
||||
- OP_CTV without scriptSigs set
|
||||
- OP_CTV with multiple inputs
|
||||
- accepting correct parameters
|
||||
- rejecting incorrect parameters
|
||||
- OP_CTV in a tree
|
||||
|
||||
A few tests may seem redundant, but it is because they are testing the cached computation of the hash
|
||||
at vin index 0
|
||||
"""
|
||||
wallet = MiniWallet(self.nodes[0], mode=MiniWalletMode.RAW_P2PK)
|
||||
self.nodes[0].add_p2p_connection(P2PInterface())
|
||||
|
||||
BLOCKS = 115
|
||||
self.log.info("Mining %d blocks for mature coinbases", BLOCKS)
|
||||
# Drop the last 100 as they're unspendable!
|
||||
coinbase_txids = [
|
||||
self.nodes[0].getblock(b)["tx"][0]
|
||||
for b in self.generate(wallet, BLOCKS)[:-100]
|
||||
]
|
||||
get_coinbase = lambda: coinbase_txids.pop()
|
||||
|
||||
self.log.info("Creating setup transactions")
|
||||
|
||||
self.log.info("Creating script for 10 random outputs")
|
||||
outputs, script = random_real_outputs_and_script(10)
|
||||
# Add some fee satoshis
|
||||
amount_sats = sum(out.nValue for out in outputs) + 200 * 500
|
||||
|
||||
self.log.info("Creating funding txn for 10 random outputs as a Taproot script")
|
||||
private_key = ECKey()
|
||||
# use simple deterministic private key (k=1)
|
||||
private_key.set((1).to_bytes(32, "big"), False)
|
||||
assert private_key.is_valid
|
||||
public_key, _ = compute_xonly_pubkey(private_key.get_bytes())
|
||||
taproot = taproot_construct(public_key, [("only-path", script, 0xC0)])
|
||||
taproot_ctv_funding_tx = create_transaction_to_script(
|
||||
self.nodes[0],
|
||||
wallet,
|
||||
get_coinbase(),
|
||||
taproot.scriptPubKey,
|
||||
amount_sats=amount_sats,
|
||||
)
|
||||
|
||||
self.log.info("Creating funding txn for 10 random outputs as a segwit script")
|
||||
segwit_ctv_funding_tx = create_transaction_to_script(
|
||||
self.nodes[0],
|
||||
wallet,
|
||||
get_coinbase(),
|
||||
CScript([0, sha256(script)]),
|
||||
amount_sats=amount_sats,
|
||||
)
|
||||
|
||||
self.log.info("Creating a CTV with a non 32 byte stack segwit script")
|
||||
segwit_ctv_funding_tx_wrongsize_stack = create_transaction_to_script(
|
||||
self.nodes[0],
|
||||
wallet,
|
||||
get_coinbase(),
|
||||
CScript([0, sha256(CScript([OP_TRUE, OP_CHECKTEMPLATEVERIFY]))]),
|
||||
amount_sats=amount_sats,
|
||||
)
|
||||
|
||||
self.log.info("Creating a CTV with an empty stack segwit script")
|
||||
# allows either calling with empty witness stack or with a 32 byte hash (cleanstack rule)
|
||||
empty_stack_script = CScript(
|
||||
[OP_CHECKTEMPLATEVERIFY, OP_DEPTH, OP_IF, OP_DROP, OP_ENDIF, OP_TRUE]
|
||||
)
|
||||
segwit_ctv_funding_tx_empty_stack = create_transaction_to_script(
|
||||
self.nodes[0],
|
||||
wallet,
|
||||
get_coinbase(),
|
||||
CScript([0, sha256(empty_stack_script)]),
|
||||
amount_sats=amount_sats,
|
||||
)
|
||||
|
||||
self.log.info(
|
||||
"Creating funding txn for 10 random outputs as a p2sh script (impossible to spend)"
|
||||
)
|
||||
p2sh_ctv_funding_tx = create_transaction_to_script(
|
||||
self.nodes[0],
|
||||
wallet,
|
||||
get_coinbase(),
|
||||
script_to_p2sh_script(script),
|
||||
amount_sats=amount_sats,
|
||||
)
|
||||
|
||||
# Small tree size 4 for test speed.
|
||||
# Can be set to a large value like 16 (i.e., 65K txns).
|
||||
TREE_SIZE = 4
|
||||
self.log.info(f"Creating script for tree size depth {TREE_SIZE}")
|
||||
congestion_tree_txo = random_secure_tree(TREE_SIZE)
|
||||
|
||||
self.log.info(f"Creating funding txn for tree size depth {TREE_SIZE}")
|
||||
bare_ctv_tree_funding_tx = create_transaction_to_script(
|
||||
self.nodes[0],
|
||||
wallet,
|
||||
get_coinbase(),
|
||||
congestion_tree_txo[0][0].scriptPubKey,
|
||||
amount_sats=congestion_tree_txo[0][0].nValue,
|
||||
)
|
||||
|
||||
self.log.info("Creating script for spend at position 2")
|
||||
outputs_position_2, script_position_2 = random_real_outputs_and_script(10, 1, 2)
|
||||
# Add some fee satoshis
|
||||
amount_position_2 = sum(out.nValue for out in outputs_position_2) + 200 * 500
|
||||
|
||||
self.log.info("Creating funding txn for spend at position 2")
|
||||
bare_ctv_position_2_funding_tx = create_transaction_to_script(
|
||||
self.nodes[0],
|
||||
wallet,
|
||||
get_coinbase(),
|
||||
script_position_2,
|
||||
amount_sats=amount_position_2,
|
||||
)
|
||||
|
||||
self.log.info(
|
||||
"Creating script for spend at position 1 with 2 non-null scriptsigs"
|
||||
)
|
||||
(
|
||||
outputs_specific_scriptSigs,
|
||||
script_specific_scriptSigs,
|
||||
) = random_real_outputs_and_script(
|
||||
10,
|
||||
0,
|
||||
2,
|
||||
[CTxIn(scriptSig=CScript([OP_TRUE])), CTxIn(scriptSig=CScript([OP_FALSE]))],
|
||||
)
|
||||
# Add some fee satoshis
|
||||
amount_specific_scriptSigs = (
|
||||
sum(out.nValue for out in outputs_specific_scriptSigs) + 200 * 500
|
||||
)
|
||||
|
||||
self.log.info(
|
||||
"Creating funding txn for spend at position 1 with 2 non-null scriptsigs"
|
||||
)
|
||||
bare_ctv_specific_scriptSigs_funding_tx = create_transaction_to_script(
|
||||
self.nodes[0],
|
||||
wallet,
|
||||
get_coinbase(),
|
||||
script_specific_scriptSigs,
|
||||
amount_sats=amount_specific_scriptSigs,
|
||||
)
|
||||
|
||||
self.log.info(
|
||||
"Creating script for spend at position 2 with 2 non-null scriptsigs"
|
||||
)
|
||||
(
|
||||
outputs_specific_scriptSigs_position_2,
|
||||
script_specific_scriptSigs_position_2,
|
||||
) = random_real_outputs_and_script(
|
||||
10,
|
||||
1,
|
||||
2,
|
||||
[CTxIn(scriptSig=CScript([OP_TRUE])), CTxIn(scriptSig=CScript([OP_FALSE]))],
|
||||
)
|
||||
# Add some fee satoshis
|
||||
amount_specific_scriptSigs_position_2 = (
|
||||
sum(out.nValue for out in outputs_specific_scriptSigs_position_2)
|
||||
+ 200 * 500
|
||||
)
|
||||
|
||||
self.log.info(
|
||||
"Creating funding txn for spend at position 2 with 2 non-null scriptsigs"
|
||||
)
|
||||
bare_ctv_specific_scriptSigs_position_2_funding_tx = (
|
||||
create_transaction_to_script(
|
||||
self.nodes[0],
|
||||
wallet,
|
||||
get_coinbase(),
|
||||
script_specific_scriptSigs_position_2,
|
||||
amount_sats=amount_specific_scriptSigs_position_2,
|
||||
)
|
||||
)
|
||||
|
||||
self.log.info("Creating funding txns for some anyone can spend outputs")
|
||||
anyone_can_spend_funding_tx = create_transaction_to_script(
|
||||
self.nodes[0],
|
||||
wallet,
|
||||
get_coinbase(),
|
||||
CScript([OP_TRUE]),
|
||||
amount_sats=amount_sats,
|
||||
)
|
||||
bare_anyone_can_spend_funding_tx = create_transaction_to_script(
|
||||
self.nodes[0],
|
||||
wallet,
|
||||
get_coinbase(),
|
||||
CScript([OP_TRUE]),
|
||||
amount_sats=amount_sats,
|
||||
)
|
||||
|
||||
funding_txs = [
|
||||
taproot_ctv_funding_tx,
|
||||
segwit_ctv_funding_tx,
|
||||
segwit_ctv_funding_tx_wrongsize_stack,
|
||||
segwit_ctv_funding_tx_empty_stack,
|
||||
p2sh_ctv_funding_tx,
|
||||
anyone_can_spend_funding_tx,
|
||||
bare_ctv_tree_funding_tx,
|
||||
bare_ctv_position_2_funding_tx,
|
||||
bare_anyone_can_spend_funding_tx,
|
||||
bare_ctv_specific_scriptSigs_funding_tx,
|
||||
bare_ctv_specific_scriptSigs_position_2_funding_tx,
|
||||
]
|
||||
|
||||
self.log.info("Obtaining TXIDs")
|
||||
(
|
||||
taproot_ctv_outpoint,
|
||||
segwit_ctv_outpoint,
|
||||
segwit_ctv_wrongsize_stack_outpoint,
|
||||
segwit_ctv_empty_stack_outpoint,
|
||||
p2sh_ctv_outpoint,
|
||||
anyone_can_spend_outpoint,
|
||||
bare_ctv_tree_outpoint,
|
||||
bare_ctv_position_2_outpoint,
|
||||
bare_anyone_can_spend_outpoint,
|
||||
bare_ctv_specific_scriptSigs_outpoint,
|
||||
bare_ctv_specific_scriptSigs_position_2_outpoint,
|
||||
) = [COutPoint(int(tx.rehash(), 16), 0) for tx in funding_txs]
|
||||
|
||||
self.log.info("Funding all outputs")
|
||||
self.add_block(funding_txs)
|
||||
|
||||
self.log.info("Testing Taproot OP_CHECKTEMPLATEVERIFY spend")
|
||||
# Test sendrawtransaction
|
||||
taproot_check_template_verify_tx = CTransaction()
|
||||
taproot_check_template_verify_tx.version = 2
|
||||
taproot_check_template_verify_tx.vin = [CTxIn(taproot_ctv_outpoint)]
|
||||
taproot_check_template_verify_tx.vout = outputs
|
||||
taproot_check_template_verify_tx.wit.vtxinwit += [CTxInWitness()]
|
||||
taproot_check_template_verify_tx.wit.vtxinwit[0].scriptWitness.stack = [
|
||||
script,
|
||||
bytes([0xC0 + taproot.negflag]) + taproot.internal_pubkey,
|
||||
]
|
||||
assert_equal(
|
||||
self.nodes[0].sendrawtransaction(
|
||||
taproot_check_template_verify_tx.serialize().hex()
|
||||
),
|
||||
taproot_check_template_verify_tx.rehash(),
|
||||
)
|
||||
self.log.info(
|
||||
"Taproot OP_CHECKTEMPLATEVERIFY spend accepted by sendrawtransaction"
|
||||
)
|
||||
|
||||
# Now we verify that a block with this transaction is also valid
|
||||
blockhash = self.add_block([taproot_check_template_verify_tx])
|
||||
self.log.info("Taproot OP_CHECKTEMPLATEVERIFY spend accepted in a block")
|
||||
|
||||
self.log.info("Rolling back the block")
|
||||
# Reset tip
|
||||
self.nodes[0].invalidateblock(blockhash)
|
||||
|
||||
self.log.info("Testing Segwit OP_CHECKTEMPLATEVERIFY spend")
|
||||
# Test sendrawtransaction
|
||||
check_template_verify_tx = CTransaction()
|
||||
check_template_verify_tx.version = 2
|
||||
check_template_verify_tx.vin = [CTxIn(segwit_ctv_outpoint)]
|
||||
check_template_verify_tx.vout = outputs
|
||||
|
||||
check_template_verify_tx.wit.vtxinwit += [CTxInWitness()]
|
||||
check_template_verify_tx.wit.vtxinwit[0].scriptWitness.stack = [script]
|
||||
assert_equal(
|
||||
self.nodes[0].sendrawtransaction(
|
||||
check_template_verify_tx.serialize().hex()
|
||||
),
|
||||
check_template_verify_tx.rehash(),
|
||||
)
|
||||
self.log.info(
|
||||
"Segwit OP_CHECKTEMPLATEVERIFY spend accepted by sendrawtransaction"
|
||||
)
|
||||
|
||||
# Now we verify that a block with this transaction is also valid
|
||||
blockhash = self.add_block([check_template_verify_tx])
|
||||
self.log.info("Segwit OP_CHECKTEMPLATEVERIFY spend accepted in a block")
|
||||
|
||||
self.log.info("Rolling back the block")
|
||||
# Reset tip
|
||||
self.nodes[0].invalidateblock(blockhash)
|
||||
|
||||
# Show any modification will break the validity
|
||||
self.log.info(
|
||||
"Modifying Segwit OP_CHECKTEMPLATEVERIFY spend, block should fail"
|
||||
)
|
||||
check_template_verify_tx_mutated_amount = check_template_verify_tx
|
||||
check_template_verify_tx_mutated_amount.vout[0].nValue += 1
|
||||
check_template_verify_tx_mutated_amount.rehash()
|
||||
self.fail_block([check_template_verify_tx_mutated_amount])
|
||||
self.log.info("Modified Segwit OP_CHECKTEMPLATEVERIFY spend failed to confirm")
|
||||
|
||||
# Now show that only one input allowed
|
||||
self.log.info("Testing that multiple inputs are disallowed when specified")
|
||||
check_template_verify_two_inputs = check_template_verify_tx
|
||||
check_template_verify_two_inputs.vin += [CTxIn(anyone_can_spend_outpoint)]
|
||||
check_template_verify_two_inputs.rehash()
|
||||
self.fail_block([check_template_verify_two_inputs])
|
||||
|
||||
self.log.info(
|
||||
"Testing that the second input specified was actually spendable by itself"
|
||||
)
|
||||
|
||||
# Second UTXO was actually spendable
|
||||
spendtx = CTransaction()
|
||||
spendtx.version = 2
|
||||
spendtx.vin = [CTxIn(anyone_can_spend_outpoint)]
|
||||
spendtx.vout += [CTxOut(int(amount_sats - 1000), random_p2sh())]
|
||||
spendtx.rehash()
|
||||
blockhash = self.add_block([spendtx])
|
||||
# Reset tip
|
||||
self.nodes[0].invalidateblock(blockhash)
|
||||
|
||||
self.log.info(
|
||||
"Testing Segwit OP_CHECKTEMPLATEVERIFY spend with a wrong size argument"
|
||||
)
|
||||
# Test sendrawtransaction
|
||||
check_template_verify_tx_wrongsize_stack = CTransaction()
|
||||
check_template_verify_tx_wrongsize_stack.version = 2
|
||||
check_template_verify_tx_wrongsize_stack.vin = [
|
||||
CTxIn(segwit_ctv_wrongsize_stack_outpoint)
|
||||
]
|
||||
check_template_verify_tx_wrongsize_stack.vout = outputs
|
||||
|
||||
check_template_verify_tx_wrongsize_stack.wit.vtxinwit += [CTxInWitness()]
|
||||
check_template_verify_tx_wrongsize_stack.wit.vtxinwit[0].scriptWitness.stack = [
|
||||
CScript([OP_TRUE, OP_CHECKTEMPLATEVERIFY])
|
||||
]
|
||||
|
||||
assert_raises_rpc_error(
|
||||
-26,
|
||||
DISCOURAGED_ERROR,
|
||||
self.nodes[0].sendrawtransaction,
|
||||
check_template_verify_tx_wrongsize_stack.serialize().hex(),
|
||||
)
|
||||
self.log.info(
|
||||
"OP_CHECKTEMPLATEVERIFY with wrong size stack rejected by sendrawtransaction as discouraged"
|
||||
)
|
||||
|
||||
# Now we verify that a block with this transaction is valid
|
||||
blockhash = self.add_block([check_template_verify_tx_wrongsize_stack])
|
||||
self.log.info(
|
||||
"Segwit OP_CHECKTEMPLATEVERIFY with wrong size stack spend accepted in a block (soft fork upgradable)"
|
||||
)
|
||||
|
||||
self.log.info(
|
||||
"Testing Segwit OP_CHECKTEMPLATEVERIFY spend with an empty stack argument"
|
||||
)
|
||||
# Test sendrawtransaction
|
||||
check_template_verify_tx_empty_stack = CTransaction()
|
||||
check_template_verify_tx_empty_stack.version = 2
|
||||
check_template_verify_tx_empty_stack.vin = [
|
||||
CTxIn(segwit_ctv_empty_stack_outpoint)
|
||||
]
|
||||
check_template_verify_tx_empty_stack.vout = outputs
|
||||
|
||||
check_template_verify_tx_empty_stack.wit.vtxinwit += [CTxInWitness()]
|
||||
check_template_verify_tx_empty_stack.wit.vtxinwit[0].scriptWitness.stack = [
|
||||
empty_stack_script
|
||||
]
|
||||
|
||||
assert_raises_rpc_error(
|
||||
-26,
|
||||
STACK_TOO_SHORT_ERROR,
|
||||
self.nodes[0].sendrawtransaction,
|
||||
check_template_verify_tx_empty_stack.serialize().hex(),
|
||||
)
|
||||
|
||||
self.log.info(
|
||||
"OP_CHECKTEMPLATEVERIFY with wrong size stack rejected by sendrawtransaction as discouraged"
|
||||
)
|
||||
|
||||
# Now we verify that a block with this transaction is invalid
|
||||
self.fail_block([check_template_verify_tx_empty_stack], STACK_TOO_SHORT_ERROR)
|
||||
self.log.info(
|
||||
"Segwit OP_CHECKTEMPLATEVERIFY with wrong size stack spend rejected from block"
|
||||
)
|
||||
|
||||
# Show that putting some element on the stack makes it succeed for consensus but fail policy
|
||||
self.log.info(
|
||||
"Segwit OP_CHECKTEMPLATEVERIFY with CTV argument in the program should fail policy pass consensus if witness stack is not 32 bytes"
|
||||
)
|
||||
check_template_verify_tx_empty_stack.wit.vtxinwit[0].scriptWitness.stack = [
|
||||
CScript([OP_TRUE]),
|
||||
empty_stack_script,
|
||||
]
|
||||
assert_raises_rpc_error(
|
||||
-26,
|
||||
DISCOURAGED_ERROR,
|
||||
self.nodes[0].sendrawtransaction,
|
||||
check_template_verify_tx_empty_stack.serialize().hex(),
|
||||
)
|
||||
self.log.info(
|
||||
"OP_CHECKTEMPLATEVERIFY with wrong size argument on the witness stack rejected by sendrawtransaction as discouraged"
|
||||
)
|
||||
|
||||
# Now we verify that a block with this transaction is valid
|
||||
blockhash = self.add_block([check_template_verify_tx_empty_stack])
|
||||
self.log.info(
|
||||
"Segwit OP_CHECKTEMPLATEVERIFY with empty stack spend accepted in a block with something on the witness stack"
|
||||
)
|
||||
self.log.info("Rolling back the block")
|
||||
# Reset tip
|
||||
self.nodes[0].invalidateblock(blockhash)
|
||||
|
||||
# Put the correct hash on
|
||||
self.log.info(
|
||||
"Testing OP_CHECKTEMPLATEVERIFY spend with template hash from the witness stack"
|
||||
)
|
||||
h = check_template_verify_tx_empty_stack.get_standard_template_hash(0)
|
||||
check_template_verify_tx_empty_stack.wit.vtxinwit[0].scriptWitness.stack = [
|
||||
h,
|
||||
empty_stack_script,
|
||||
]
|
||||
assert_equal(
|
||||
self.nodes[0].sendrawtransaction(
|
||||
check_template_verify_tx_empty_stack.serialize().hex()
|
||||
),
|
||||
check_template_verify_tx_empty_stack.rehash(),
|
||||
)
|
||||
self.log.info(
|
||||
"Witness stack defined OP_CHECKTEMPLATEVERIFY spend accepted by sendrawtransaction"
|
||||
)
|
||||
# Now we verify that a block with this transaction is also valid
|
||||
blockhash = self.add_block([check_template_verify_tx_empty_stack])
|
||||
self.log.info(
|
||||
"Witness stack defined OP_CHECKTEMPLATEVERIFY spend accepted in a block"
|
||||
)
|
||||
self.log.info("Rolling back the block")
|
||||
# Reset tip
|
||||
self.nodes[0].invalidateblock(blockhash)
|
||||
|
||||
self.log.info(
|
||||
"Testing that other 32 byte arguments passed from the witness stack fail"
|
||||
)
|
||||
h = h[::-1]
|
||||
check_template_verify_tx_empty_stack.wit.vtxinwit[0].scriptWitness.stack = [
|
||||
h,
|
||||
empty_stack_script,
|
||||
]
|
||||
self.fail_block([check_template_verify_tx_empty_stack])
|
||||
|
||||
# Test sendrawtransaction with P2SH
|
||||
# Mathematically, this test cannot succeed since P2SH requires that the
|
||||
# P2SH script is in the scriptsig (which contains H), and H much
|
||||
# contain a commitment to the scriptsig.
|
||||
#
|
||||
# Note that this relies on RIPEMD160 being hard to find a cycle in.
|
||||
# This *could* break one day (P2SH is no longer recommended), but
|
||||
# there's no capability that would be gained by doing so (such as a
|
||||
# recursive covenant), it'd just be a "party trick" (and you could
|
||||
# also possibly steal all existing P2SH addresses).
|
||||
p2sh_check_template_verify_tx = CTransaction()
|
||||
p2sh_check_template_verify_tx.version = 2
|
||||
p2sh_check_template_verify_tx.vin = [
|
||||
CTxIn(p2sh_ctv_outpoint, CScript([script]))
|
||||
]
|
||||
p2sh_check_template_verify_tx.vout = outputs
|
||||
|
||||
assert_raises_rpc_error(
|
||||
-26,
|
||||
CHECKTEMPLATEVERIFY_ERROR,
|
||||
self.nodes[0].sendrawtransaction,
|
||||
p2sh_check_template_verify_tx.serialize().hex(),
|
||||
)
|
||||
self.log.info(
|
||||
"P2SH OP_CHECKTEMPLATEVERIFY spend rejected by sendrawtransaction"
|
||||
)
|
||||
|
||||
# Now we verify that a block with this transaction is also invalid
|
||||
self.fail_block([p2sh_check_template_verify_tx])
|
||||
self.log.info("P2SH OP_CHECKTEMPLATEVERIFY spend rejected in a block")
|
||||
|
||||
self.log.info(
|
||||
"Testing a congestion control tree using bare OP_CHECKTEMPLATEVERIFY"
|
||||
)
|
||||
# Expand Congestion Control Tree to one specific input
|
||||
out = bare_ctv_tree_outpoint
|
||||
txs = []
|
||||
for level in congestion_tree_txo[1:]:
|
||||
spendtx = CTransaction()
|
||||
spendtx.version = 2
|
||||
spendtx.vin += [CTxIn(out)]
|
||||
spendtx.vout += level[:2]
|
||||
out = COutPoint(int(spendtx.rehash(), 16), 0)
|
||||
txs.append(spendtx)
|
||||
self.add_block(txs)
|
||||
|
||||
self.log.info("Testing bare OP_CHECKTEMPLATEVERIFY with CTV at position 2")
|
||||
check_template_verify_tx_pos_2 = CTransaction()
|
||||
check_template_verify_tx_pos_2.version = 2
|
||||
check_template_verify_tx_pos_2.vin = [CTxIn(bare_ctv_position_2_outpoint)]
|
||||
check_template_verify_tx_pos_2.vout = outputs_position_2
|
||||
self.log.info(
|
||||
"Testing that the transaction fails because we have too few inputs"
|
||||
)
|
||||
self.fail_block([check_template_verify_tx_pos_2])
|
||||
check_template_verify_tx_pos_2.vin += [CTxIn(bare_anyone_can_spend_outpoint)]
|
||||
check_template_verify_tx_pos_2.rehash()
|
||||
self.log.info(
|
||||
"Testing that the transaction fails because the inputs are in the wrong order"
|
||||
)
|
||||
self.fail_block([check_template_verify_tx_pos_2])
|
||||
self.log.info(
|
||||
"Testing that the transaction succeeds when the inputs are in the correct order"
|
||||
)
|
||||
check_template_verify_tx_pos_2.vin.reverse()
|
||||
check_template_verify_tx_pos_2.rehash()
|
||||
blockhash = self.add_block([check_template_verify_tx_pos_2])
|
||||
self.nodes[0].invalidateblock(blockhash)
|
||||
check_template_verify_tx_pos_2.vin[0].scriptSig = CScript([OP_TRUE])
|
||||
check_template_verify_tx_pos_2.rehash()
|
||||
self.log.info(
|
||||
"Testing that the transaction fails because the scriptSig on the other input has been modified"
|
||||
)
|
||||
self.fail_block([check_template_verify_tx_pos_2])
|
||||
|
||||
self.log.info(
|
||||
"Testing bare OP_CHECKTEMPLATEVERIFY with CTV at position 1 with specific scriptSigs"
|
||||
)
|
||||
check_template_verify_tx_specific_scriptSigs = CTransaction()
|
||||
check_template_verify_tx_specific_scriptSigs.version = 2
|
||||
check_template_verify_tx_specific_scriptSigs.vin = [
|
||||
CTxIn(bare_ctv_specific_scriptSigs_outpoint, CScript([OP_TRUE])),
|
||||
CTxIn(bare_anyone_can_spend_outpoint, CScript([OP_TRUE])),
|
||||
]
|
||||
check_template_verify_tx_specific_scriptSigs.vout = outputs_specific_scriptSigs
|
||||
check_template_verify_tx_specific_scriptSigs.rehash()
|
||||
self.log.info(
|
||||
"Testing bare OP_CHECKTEMPLATEVERIFY rejects incorrect scriptSigs"
|
||||
)
|
||||
self.fail_block([check_template_verify_tx_specific_scriptSigs])
|
||||
|
||||
self.log.info("Testing bare OP_CHECKTEMPLATEVERIFY accepts correct scriptSigs")
|
||||
check_template_verify_tx_specific_scriptSigs.vin[1].scriptSig = CScript(
|
||||
[OP_FALSE]
|
||||
)
|
||||
check_template_verify_tx_specific_scriptSigs.rehash()
|
||||
blockhash = self.add_block([check_template_verify_tx_specific_scriptSigs])
|
||||
self.nodes[0].invalidateblock(blockhash)
|
||||
|
||||
self.log.info(
|
||||
"Testing bare OP_CHECKTEMPLATEVERIFY with CTV at position 2 with specific scriptSigs"
|
||||
)
|
||||
# This is only really to test that uncached values work correctly with scriptSig set
|
||||
check_template_verify_tx_specific_scriptSigs_position_2 = CTransaction()
|
||||
check_template_verify_tx_specific_scriptSigs_position_2.version = 2
|
||||
check_template_verify_tx_specific_scriptSigs_position_2.vin = [
|
||||
CTxIn(bare_anyone_can_spend_outpoint, CScript([OP_TRUE])),
|
||||
CTxIn(
|
||||
bare_ctv_specific_scriptSigs_position_2_outpoint, CScript([OP_FALSE])
|
||||
),
|
||||
]
|
||||
check_template_verify_tx_specific_scriptSigs_position_2.vout = (
|
||||
outputs_specific_scriptSigs_position_2
|
||||
)
|
||||
check_template_verify_tx_specific_scriptSigs_position_2.rehash()
|
||||
self.add_block([check_template_verify_tx_specific_scriptSigs_position_2])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
CheckTemplateVerifyTest(__file__).main()
|
|
@ -25,6 +25,7 @@ from io import BytesIO
|
|||
import math
|
||||
import random
|
||||
import socket
|
||||
import struct
|
||||
import time
|
||||
import unittest
|
||||
|
||||
|
@ -633,6 +634,19 @@ class CTransaction:
|
|||
r += self.nLockTime.to_bytes(4, "little")
|
||||
return r
|
||||
|
||||
def get_standard_template_hash(self, nIn):
|
||||
r = b""
|
||||
r += self.version.to_bytes(4, "little")
|
||||
r += self.nLockTime.to_bytes(4, "little")
|
||||
if any(inp.scriptSig for inp in self.vin):
|
||||
r += sha256(b"".join(ser_string(inp.scriptSig) for inp in self.vin))
|
||||
r += struct.pack("<I", len(self.vin))
|
||||
r += sha256(b"".join(struct.pack("<I", inp.nSequence) for inp in self.vin))
|
||||
r += struct.pack("<I", len(self.vout))
|
||||
r += sha256(b"".join(out.serialize() for out in self.vout))
|
||||
r += struct.pack("<I", nIn)
|
||||
return sha256(r)
|
||||
|
||||
# Only serialize with witness when explicitly called for
|
||||
def serialize_with_witness(self):
|
||||
flags = 0
|
||||
|
|
|
@ -238,7 +238,7 @@ OP_CHECKMULTISIGVERIFY = CScriptOp(0xaf)
|
|||
OP_NOP1 = CScriptOp(0xb0)
|
||||
OP_CHECKLOCKTIMEVERIFY = CScriptOp(0xb1)
|
||||
OP_CHECKSEQUENCEVERIFY = CScriptOp(0xb2)
|
||||
OP_NOP4 = CScriptOp(0xb3)
|
||||
OP_CHECKTEMPLATEVERIFY = CScriptOp(0xb3)
|
||||
OP_NOP5 = CScriptOp(0xb4)
|
||||
OP_NOP6 = CScriptOp(0xb5)
|
||||
OP_NOP7 = CScriptOp(0xb6)
|
||||
|
@ -356,7 +356,7 @@ OPCODE_NAMES.update({
|
|||
OP_NOP1: 'OP_NOP1',
|
||||
OP_CHECKLOCKTIMEVERIFY: 'OP_CHECKLOCKTIMEVERIFY',
|
||||
OP_CHECKSEQUENCEVERIFY: 'OP_CHECKSEQUENCEVERIFY',
|
||||
OP_NOP4: 'OP_NOP4',
|
||||
OP_CHECKTEMPLATEVERIFY : 'OP_CHECKTEMPLATEVERIFY',
|
||||
OP_NOP5: 'OP_NOP5',
|
||||
OP_NOP6: 'OP_NOP6',
|
||||
OP_NOP7: 'OP_NOP7',
|
||||
|
|
|
@ -312,6 +312,7 @@ BASE_SCRIPTS = [
|
|||
'wallet_balance.py --descriptors',
|
||||
'p2p_initial_headers_sync.py',
|
||||
'feature_nulldummy.py',
|
||||
'feature_checktemplateverify.py',
|
||||
'mempool_accept.py',
|
||||
'mempool_expiry.py',
|
||||
'wallet_import_with_label.py --legacy-wallet',
|
||||
|
|
Loading…
Add table
Reference in a new issue