mirror of
https://github.com/ElementsProject/elements.git
synced 2026-08-13 12:33:42 +02:00
607 lines
34 KiB
Python
Executable file
607 lines
34 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
# Copyright (c) 2014-2016 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 for taproot sighash algorithm with pegins and issuances
|
|
|
|
from random import randint
|
|
from test_framework.util import BITCOIN_ASSET_BYTES, assert_raises_rpc_error, satoshi_round
|
|
from test_framework.key import ECKey, compute_xonly_pubkey, generate_privkey, sign_schnorr
|
|
from test_framework.messages import COIN, COutPoint, CTransaction, CTxIn, CTxInWitness, CTxOut, CTxOutNonce, CTxOutValue, ser_uint256, sha256, tx_from_hex
|
|
from test_framework.test_framework import BitcoinTestFramework
|
|
from test_framework.script import CScript, CScriptNum, CScriptOp, OP_0, OP_1, OP_2, OP_ADD64, OP_AND, OP_CHECKSIGFROMSTACK, OP_DIV64, OP_DROP, OP_DUP, OP_ECMULSCALARVERIFY, OP_EQUAL, OP_EQUALVERIFY, OP_FALSE, OP_FROMALTSTACK, OP_GREATERTHAN64, OP_GREATERTHANOREQUAL64, OP_INSPECTINPUTASSET, OP_INSPECTINPUTISSUANCE, OP_INSPECTINPUTOUTPOINT, OP_INSPECTINPUTSCRIPTPUBKEY, OP_INSPECTINPUTSEQUENCE, OP_INSPECTINPUTVALUE, OP_INSPECTLOCKTIME, OP_INSPECTNUMINPUTS, OP_INSPECTNUMOUTPUTS, OP_INSPECTOUTPUTASSET, OP_INSPECTOUTPUTNONCE, OP_INSPECTOUTPUTSCRIPTPUBKEY, OP_INSPECTOUTPUTVALUE, OP_INSPECTVERSION, OP_LE32TOLE64, OP_LE64TOSCRIPTNUM, OP_LESSTHAN64, OP_LESSTHANOREQUAL64, OP_MUL64, OP_NEG64, OP_NOT, OP_INVERT, OP_OR, OP_PUSHCURRENTINPUTINDEX, OP_SCRIPTNUMTOLE64, OP_SHA256FINALIZE, OP_SHA256INITIALIZE, OP_SHA256UPDATE, OP_SIZE, OP_SUB64, OP_SWAP, OP_TWEAKVERIFY, OP_TOALTSTACK, OP_TXWEIGHT, OP_VERIFY, OP_XOR, taproot_construct, SIGHASH_DEFAULT, SIGHASH_ALL, SIGHASH_NONE, SIGHASH_SINGLE, SIGHASH_ANYONECANPAY
|
|
|
|
import os
|
|
|
|
VALID_SIGHASHES_ECDSA = [
|
|
SIGHASH_ALL,
|
|
SIGHASH_NONE,
|
|
SIGHASH_SINGLE,
|
|
SIGHASH_ANYONECANPAY + SIGHASH_ALL,
|
|
SIGHASH_ANYONECANPAY + SIGHASH_NONE,
|
|
SIGHASH_ANYONECANPAY + SIGHASH_SINGLE
|
|
]
|
|
|
|
VALID_SIGHASHES_TAPROOT = [SIGHASH_DEFAULT] + VALID_SIGHASHES_ECDSA
|
|
|
|
class TapHashPeginTest(BitcoinTestFramework):
|
|
|
|
def set_test_params(self):
|
|
self.setup_clean_chain = True
|
|
self.num_nodes = 1
|
|
self.extra_args = [
|
|
["-initialfreecoins=2100000000000000",
|
|
"-anyonecanspendaremine=1",
|
|
"-blindedaddresses=1",
|
|
"-validatepegin=0",
|
|
"-con_parent_chain_signblockscript=51",
|
|
"-parentscriptprefix=75",
|
|
"-parent_bech32_hrp=ert",
|
|
"-minrelaytxfee=0",
|
|
"-maxtxfee=100.0",
|
|
]]
|
|
|
|
def add_options(self, parser):
|
|
self.add_wallet_options(parser)
|
|
|
|
def skip_test_if_missing_module(self):
|
|
self.skip_if_no_wallet()
|
|
self.skip_if_no_bdb()
|
|
|
|
def setup_network(self, split=False):
|
|
self.setup_nodes()
|
|
|
|
|
|
def get_utxo(self, fund_tx, idx):
|
|
spent = None
|
|
# Coin selection
|
|
for utxo in self.nodes[0].listunspent():
|
|
if utxo["txid"] == ser_uint256(fund_tx.vin[idx].prevout.hash)[::-1].hex() and utxo["vout"] == fund_tx.vin[idx].prevout.n:
|
|
spent = utxo
|
|
|
|
assert spent is not None
|
|
assert len(fund_tx.vin) == 2
|
|
return spent
|
|
|
|
def create_taproot_utxo(self, scripts = None, blind = False):
|
|
# modify the transaction to add one output that should spend previous taproot
|
|
# Create a taproot prevout
|
|
addr = self.nodes[0].getnewaddress()
|
|
|
|
sec = generate_privkey()
|
|
pub = compute_xonly_pubkey(sec)[0]
|
|
tap = taproot_construct(pub, scripts)
|
|
spk = tap.scriptPubKey
|
|
|
|
# No need to test blinding in this unit test
|
|
unconf_addr = self.nodes[0].getaddressinfo(addr)['unconfidential']
|
|
|
|
raw_tx = self.nodes[0].createrawtransaction([], [{unconf_addr: 1.2}])
|
|
# edit spk directly, no way to get new address.
|
|
# would need to implement bech32m in python
|
|
tx = tx_from_hex(raw_tx)
|
|
tx.vout[0].scriptPubKey = spk
|
|
tx.vout[0].nValue = CTxOutValue(12*10**7)
|
|
raw_hex = tx.serialize().hex()
|
|
|
|
fund_tx = self.nodes[0].fundrawtransaction(raw_hex, False, )["hex"]
|
|
fund_tx = tx_from_hex(fund_tx)
|
|
|
|
# Createrawtransaction might rearrage txouts
|
|
prev_vout = None
|
|
for i, out in enumerate(fund_tx.vout):
|
|
if spk == out.scriptPubKey:
|
|
prev_vout = i
|
|
|
|
tx = self.nodes[0].blindrawtransaction(fund_tx.serialize().hex())
|
|
signed_raw_tx = self.nodes[0].signrawtransactionwithwallet(tx)
|
|
self.nodes[0].sendrawtransaction(signed_raw_tx['hex'])
|
|
tx = tx_from_hex(signed_raw_tx['hex'])
|
|
tx.rehash()
|
|
self.generate(self.nodes[0], 1)
|
|
last_blk = self.nodes[0].getblock(self.nodes[0].getbestblockhash())
|
|
assert tx.hash in last_blk['tx']
|
|
|
|
return tx, prev_vout, spk, sec, pub, tap
|
|
|
|
def tapscript_satisfy_test(self, script, inputs = None, add_issuance = False,
|
|
add_pegin = False, fail=None, add_prevout=False, add_asset=False,
|
|
add_value=False, add_spk = False, seq = 0, add_out_spk = None,
|
|
add_out_asset = None, add_out_value = None, add_out_nonce = None,
|
|
ver = 2, locktime = 0, add_num_outputs=False, add_weight=False, blind=False):
|
|
if inputs is None:
|
|
inputs = []
|
|
# Create a taproot utxo
|
|
scripts = [("s0", script)]
|
|
prev_tx, prev_vout, spk, sec, pub, tap = self.create_taproot_utxo(scripts)
|
|
|
|
if add_pegin:
|
|
fund_info = self.nodes[0].getpeginaddress()
|
|
peg_id = self.nodes[0].sendtoaddress(fund_info["mainchain_address"], 1)
|
|
raw_peg_tx = self.nodes[0].gettransaction(peg_id)["hex"]
|
|
peg_txid = self.nodes[0].sendrawtransaction(raw_peg_tx)
|
|
self.generate(self.nodes[0], 101)
|
|
peg_prf = self.nodes[0].gettxoutproof([peg_txid])
|
|
claim_script = fund_info["claim_script"]
|
|
|
|
raw_claim = self.nodes[0].createrawpegin(raw_peg_tx, peg_prf, claim_script)
|
|
tx = tx_from_hex(raw_claim['hex'])
|
|
else:
|
|
tx = CTransaction()
|
|
|
|
tx.version = ver
|
|
tx.nLockTime = locktime
|
|
# Spend the pegin and taproot tx together
|
|
in_total = prev_tx.vout[prev_vout].nValue.getAmount()
|
|
fees = 1000
|
|
tap_in_pos = 0
|
|
|
|
if blind:
|
|
# Add an unrelated output
|
|
key = ECKey()
|
|
key.generate()
|
|
tx.vout.append(CTxOut(nValue = CTxOutValue(10000), scriptPubKey = spk, nNonce=CTxOutNonce(key.get_pubkey().get_bytes())))
|
|
|
|
tx_hex = self.nodes[0].fundrawtransaction(tx.serialize().hex())
|
|
tx = tx_from_hex(tx_hex['hex'])
|
|
|
|
tx.vin.append(CTxIn(COutPoint(prev_tx.sha256, prev_vout), nSequence=seq))
|
|
tx.vout.append(CTxOut(nValue = CTxOutValue(in_total - fees), scriptPubKey = spk)) # send back to self
|
|
tx.vout.append(CTxOut(CTxOutValue(fees)))
|
|
|
|
if add_issuance:
|
|
blind_addr = self.nodes[0].getnewaddress()
|
|
issue_addr = self.nodes[0].validateaddress(blind_addr)['unconfidential']
|
|
# Issuances only require one fee output and that output must the last
|
|
# one. However the way, the current code is structured, it is not possible
|
|
# to this in a super clean without special casing.
|
|
if add_pegin:
|
|
tx.vout.pop()
|
|
tx.vout.pop()
|
|
tx.vout.insert(0, CTxOut(nValue = CTxOutValue(in_total), scriptPubKey = spk)) # send back to self)
|
|
issued_tx = self.nodes[0].rawissueasset(tx.serialize().hex(), [{"asset_amount":2, "asset_address":issue_addr, "blind":False}])[0]["hex"]
|
|
tx = tx_from_hex(issued_tx)
|
|
# Sign inputs
|
|
if add_pegin:
|
|
signed = self.nodes[0].signrawtransactionwithwallet(tx.serialize().hex())
|
|
tx = tx_from_hex(signed['hex'])
|
|
tap_in_pos += 1
|
|
else:
|
|
# Need to create empty witness when not deserializing from rpc
|
|
tx.wit.vtxinwit.append(CTxInWitness())
|
|
|
|
if blind:
|
|
tx.vin[0], tx.vin[1] = tx.vin[1], tx.vin[0]
|
|
utxo = self.get_utxo(tx, 1)
|
|
zero_str = "0"*64
|
|
blinded_raw = self.nodes[0].rawblindrawtransaction(tx.serialize().hex(), [zero_str, utxo["amountblinder"]], [1.2, utxo['amount']], [utxo['asset'], utxo['asset']], [zero_str, utxo['assetblinder']])
|
|
tx = tx_from_hex(blinded_raw)
|
|
signed_raw_tx = self.nodes[0].signrawtransactionwithwallet(tx.serialize().hex())
|
|
tx = tx_from_hex(signed_raw_tx['hex'])
|
|
|
|
|
|
suffix_annex = []
|
|
control_block = bytes([tap.leaves["s0"].version + tap.negflag]) + tap.internal_pubkey + tap.leaves["s0"].merklebranch
|
|
# Add the prevout to the top of inputs. The witness script will check for equality.
|
|
if add_prevout:
|
|
inputs = [prev_vout.to_bytes(4, 'little'), ser_uint256(prev_tx.sha256)]
|
|
if add_asset:
|
|
assert blind # only used with blinding in testing
|
|
utxo = self.nodes[0].gettxout(ser_uint256(tx.vin[1].prevout.hash)[::-1].hex(), tx.vin[1].prevout.n)
|
|
if "assetcommitment" in utxo:
|
|
asset = bytes.fromhex(utxo["assetcommitment"])
|
|
else:
|
|
asset = b"\x01" + bytes.fromhex(utxo["asset"])[::-1]
|
|
inputs = [asset[0:1], asset[1:33]]
|
|
if add_value:
|
|
utxo = self.nodes[0].gettxout(ser_uint256(tx.vin[1].prevout.hash)[::-1].hex(), tx.vin[1].prevout.n)
|
|
if "valuecommitment" in utxo:
|
|
value = bytes.fromhex(utxo["valuecommitment"])
|
|
inputs = [value[0:1], value[1:33]]
|
|
else:
|
|
value = b"\x01" + int(satoshi_round(utxo["value"])*COIN).to_bytes(8, 'little')
|
|
inputs = [value[0:1], value[1:9]]
|
|
if add_spk:
|
|
ver = CScriptOp.decode_op_n(int.from_bytes(spk[0:1], 'little'))
|
|
inputs = [CScriptNum.encode(CScriptNum(ver))[1:], spk[2:len(spk)]] # always segwit
|
|
|
|
# Add witness for outputs
|
|
if add_out_asset is not None:
|
|
asset = tx.vout[add_out_asset].nAsset.vchCommitment
|
|
inputs = [asset[0:1], asset[1:33]]
|
|
if add_out_value is not None:
|
|
value = tx.vout[add_out_value].nValue.vchCommitment
|
|
if len(value) == 9:
|
|
inputs = [value[0:1], value[1:9][::-1]]
|
|
else:
|
|
inputs = [value[0:1], value[1:33]]
|
|
if add_out_nonce is not None:
|
|
nonce = tx.vout[add_out_nonce].nNonce.vchCommitment
|
|
if len(nonce) == 1:
|
|
inputs = [b'']
|
|
else:
|
|
inputs = [nonce]
|
|
if add_out_spk is not None:
|
|
out_spk = tx.vout[add_out_spk].scriptPubKey
|
|
if len(out_spk) == 0:
|
|
# Python upstream encoding CScriptNum interesting behaviour where it also encodes the length
|
|
# This assumes the implicit wallet behaviour of using segwit outputs.
|
|
# This is useful while sending scripts, but not while using CScriptNums in constructing scripts
|
|
inputs = [CScriptNum.encode(CScriptNum(-1))[1:], sha256(out_spk)]
|
|
else:
|
|
ver = CScriptOp.decode_op_n(int.from_bytes(out_spk[0:1], 'little'))
|
|
inputs = [CScriptNum.encode(CScriptNum(ver))[1:], out_spk[2:len(out_spk)]] # always segwit
|
|
if add_num_outputs:
|
|
num_outs = len(tx.vout)
|
|
inputs = [CScriptNum.encode(CScriptNum(num_outs))[1:]]
|
|
if add_weight:
|
|
# Add a dummy input and check the overall weight
|
|
inputs = [int(5).to_bytes(8, 'little')]
|
|
wit = inputs + [bytes(tap.leaves["s0"].script), control_block] + suffix_annex
|
|
tx.wit.vtxinwit[tap_in_pos].scriptWitness.stack = wit
|
|
|
|
exp_weight = self.nodes[0].decoderawtransaction(tx.serialize().hex())["weight"]
|
|
inputs = [exp_weight.to_bytes(8, 'little')]
|
|
wit = inputs + [bytes(tap.leaves["s0"].script), control_block] + suffix_annex
|
|
tx.wit.vtxinwit[tap_in_pos].scriptWitness.stack = wit
|
|
|
|
if fail:
|
|
assert_raises_rpc_error(-26, fail, self.nodes[0].sendrawtransaction, tx.serialize().hex())
|
|
return
|
|
|
|
|
|
self.nodes[0].sendrawtransaction(hexstring = tx.serialize().hex())
|
|
self.generate(self.nodes[0], 1)
|
|
last_blk = self.nodes[0].getblock(self.nodes[0].getbestblockhash())
|
|
tx.rehash()
|
|
assert tx.hash in last_blk['tx']
|
|
|
|
|
|
def run_test(self):
|
|
self.generate(self.nodes[0], 101)
|
|
self.wait_until(lambda: self.nodes[0].getblockcount() == 101, timeout=5)
|
|
# ELEMENTS: spend the initialfreecoins to node0, to fix bad-txns-inputs-missingorspent error when creating the first taproot utxo
|
|
self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 50)
|
|
self.generate(self.nodes[0], 1)
|
|
|
|
# Test whether the above test framework is working
|
|
self.log.info("Test simple op_1")
|
|
self.tapscript_satisfy_test(CScript([OP_1]))
|
|
|
|
# Test streaming sha256
|
|
# All preimages up to len 80 can be filled in a single standard witness element
|
|
# Testing only random even len for speedup
|
|
self.log.info("Testing streaming SHA256")
|
|
for k in range(0, 81, 2):
|
|
preimage = os.urandom(k)
|
|
hash = sha256(preimage)
|
|
self.tapscript_satisfy_test(CScript([OP_SHA256INITIALIZE, OP_SWAP, OP_SHA256FINALIZE, hash, OP_EQUAL]), inputs = [bytes(0), preimage])
|
|
|
|
# All preimages greater than 80 require Cating the elements
|
|
# But this only works till 520 bytes, streaming opcodes can
|
|
# be when dealing with more than 520 bytes
|
|
# Also adds a test when the sha256 buffer is full
|
|
for pre_len in [64*i for i in range(2, 10)] + [100, 520, 521, 800, 1000, 10000]:
|
|
preimage = os.urandom(pre_len)
|
|
hash = sha256(preimage)
|
|
# Split into chunks of 80
|
|
wit = [preimage[i:min(i + 80, len(preimage))] for i in range(0, len(preimage), 80)]
|
|
script = CScript([OP_SHA256INITIALIZE] + [OP_SWAP, OP_SHA256UPDATE]*(len(wit)-2) + [OP_SWAP, OP_SHA256FINALIZE, hash, OP_EQUAL])
|
|
self.tapscript_satisfy_test(script, wit[::-1])
|
|
|
|
# Test introspection opcodes
|
|
# 1a. No Pegins/issuances
|
|
self.log.info("Introspection tests: outpoint flag")
|
|
self.tapscript_satisfy_test(CScript([OP_0, OP_INSPECTINPUTOUTPOINT, b'\x00', OP_EQUALVERIFY, OP_DROP, OP_DROP, OP_1]))
|
|
# 1b. Add a pegin (Test pegin input must be 0x40)
|
|
self.tapscript_satisfy_test(CScript([OP_0, OP_INSPECTINPUTOUTPOINT, b'\x40', OP_EQUALVERIFY, OP_DROP, OP_DROP, OP_1]), add_pegin=True)
|
|
# 1c. Add pegin (The non-pegin input must be push 0x00)
|
|
self.tapscript_satisfy_test(CScript([OP_1, OP_INSPECTINPUTOUTPOINT, b'\x00', OP_EQUALVERIFY, OP_DROP, OP_DROP, OP_1]), add_pegin=True)
|
|
# 1d. Add an issuance
|
|
self.tapscript_satisfy_test(CScript([OP_0, OP_INSPECTINPUTOUTPOINT, b'\x80', OP_EQUALVERIFY, OP_DROP, OP_DROP, OP_1]), add_issuance=True)
|
|
# 1e. Both issuance and pegins together
|
|
self.tapscript_satisfy_test(CScript([OP_0, OP_INSPECTINPUTOUTPOINT, b'\xc0', OP_EQUALVERIFY, OP_DROP, OP_DROP, OP_1]), add_pegin = True, add_issuance=True)
|
|
# 1f. Failure test case
|
|
self.tapscript_satisfy_test(CScript([OP_0, OP_INSPECTINPUTOUTPOINT, b'\x00', OP_EQUALVERIFY, OP_DROP, OP_DROP, OP_1]), add_pegin = True, add_issuance=True, fail="Script failed an OP_EQUALVERIFY operation")
|
|
|
|
# Test opcode for inspecting prev tx
|
|
self.log.info("Introspection tests: inputs")
|
|
self.tapscript_satisfy_test(CScript([OP_0, OP_INSPECTINPUTOUTPOINT, b'\x00', OP_EQUALVERIFY, OP_TOALTSTACK, OP_EQUALVERIFY, OP_FROMALTSTACK, OP_EQUAL]), add_prevout=True)
|
|
|
|
# Test taproot asset with blinding.
|
|
self.tapscript_satisfy_test(CScript([OP_1]), blind=True)
|
|
|
|
# 2 Test asset(explicit and conf)
|
|
self.tapscript_satisfy_test(CScript([OP_0, OP_INSPECTINPUTASSET, 1, OP_EQUALVERIFY, BITCOIN_ASSET_BYTES, OP_EQUAL]))
|
|
for _ in range(5): # run multiple times for wallet to select conf utxos
|
|
self.tapscript_satisfy_test(CScript([OP_1, OP_INSPECTINPUTASSET, OP_TOALTSTACK, OP_EQUALVERIFY, OP_FROMALTSTACK, OP_EQUAL]), blind=True, add_asset=True)
|
|
|
|
# 3 Test asset(explicit and conf)
|
|
amt = 12*10**7
|
|
self.tapscript_satisfy_test(CScript([OP_0, OP_INSPECTINPUTVALUE, 1, OP_EQUALVERIFY, amt.to_bytes(8, 'little'), OP_EQUAL]))
|
|
for _ in range(5): # run multiple times for wallet to select conf utxos
|
|
self.tapscript_satisfy_test(CScript([OP_1, OP_INSPECTINPUTVALUE, OP_TOALTSTACK, OP_EQUALVERIFY, OP_FROMALTSTACK, OP_EQUAL]), blind=True, add_value=True)
|
|
|
|
# 4 Test scriptpubkey
|
|
self.tapscript_satisfy_test(CScript([OP_0, OP_INSPECTINPUTSCRIPTPUBKEY, OP_TOALTSTACK, OP_EQUALVERIFY, OP_FROMALTSTACK, OP_EQUAL]), add_spk = True)
|
|
|
|
# 5 Test nSequence
|
|
one, two = 1, 2
|
|
self.tapscript_satisfy_test(CScript([OP_0, OP_INSPECTINPUTSEQUENCE, one.to_bytes(4, 'little'), OP_EQUAL]), seq = 1)
|
|
self.tapscript_satisfy_test(CScript([OP_0, OP_INSPECTINPUTSEQUENCE, two.to_bytes(4, 'little'), OP_EQUAL]), seq = 1, fail="Script evaluated without error but finished with a false/empty top stack element")
|
|
|
|
# 6 Test issuances
|
|
# No issaunce
|
|
self.tapscript_satisfy_test(CScript([OP_0, OP_INSPECTINPUTISSUANCE, OP_FALSE, OP_EQUAL]))
|
|
# Issuance with 2 units
|
|
asset_issue_amount = 2*COIN
|
|
self.tapscript_satisfy_test(CScript([
|
|
OP_0, OP_INSPECTINPUTISSUANCE,
|
|
# Check blinding nonce is zero
|
|
bytes(32), OP_EQUALVERIFY,
|
|
# Check the entropy is 32 bytes
|
|
OP_SIZE, 32, OP_EQUALVERIFY, OP_DROP,
|
|
# Check the explicit issue amount
|
|
1, OP_EQUALVERIFY, asset_issue_amount.to_bytes(8, 'little'), OP_EQUALVERIFY,
|
|
# Last inflation keys is null, check it as explicit `0` LE 8
|
|
1, OP_EQUALVERIFY, int(0).to_bytes(8, 'little'), OP_EQUAL
|
|
]), add_issuance=True)
|
|
|
|
# Input index out of bounds
|
|
self.tapscript_satisfy_test(CScript([120, OP_1, OP_INSPECTINPUTASSET, OP_FALSE, OP_EQUAL]), fail="Introspection index out of bounds")
|
|
self.tapscript_satisfy_test(CScript([-1, OP_1, OP_INSPECTINPUTVALUE, OP_FALSE, OP_EQUAL]), fail="Introspection index out of bounds")
|
|
|
|
# Test current input
|
|
self.log.info("Introspection tests: current input index")
|
|
self.tapscript_satisfy_test(CScript([OP_PUSHCURRENTINPUTINDEX, OP_0, OP_EQUAL]))
|
|
self.tapscript_satisfy_test(CScript([OP_PUSHCURRENTINPUTINDEX, OP_1, OP_EQUAL]), fail="Script evaluated without error but finished with a false/empty top stack element")
|
|
|
|
# Test Outputs
|
|
self.log.info("Introspection tests: outputs")
|
|
for blind in [True, False]:
|
|
for out_pos in [0, 1]:
|
|
self.tapscript_satisfy_test(CScript([out_pos, OP_INSPECTOUTPUTASSET, OP_TOALTSTACK, OP_EQUALVERIFY, OP_FROMALTSTACK, OP_EQUAL]), blind=blind, add_out_asset=out_pos)
|
|
self.tapscript_satisfy_test(CScript([out_pos, OP_INSPECTOUTPUTVALUE, OP_TOALTSTACK, OP_EQUALVERIFY, OP_FROMALTSTACK, OP_EQUAL]), blind=blind, add_out_value=out_pos)
|
|
self.tapscript_satisfy_test(CScript([out_pos, OP_INSPECTOUTPUTNONCE, OP_EQUAL]), blind=blind, add_out_nonce=out_pos)
|
|
self.tapscript_satisfy_test(CScript([out_pos, OP_INSPECTOUTPUTSCRIPTPUBKEY, OP_TOALTSTACK, OP_EQUALVERIFY, OP_FROMALTSTACK, OP_EQUALVERIFY, OP_1]), blind=blind, add_out_spk=out_pos)
|
|
|
|
# Test that output index out of bounds fail
|
|
self.tapscript_satisfy_test(CScript([120, OP_INSPECTOUTPUTASSET, OP_FALSE, OP_EQUAL]), fail="Introspection index out of bounds")
|
|
self.tapscript_satisfy_test(CScript([-1, OP_INSPECTOUTPUTVALUE, OP_FALSE, OP_EQUAL]), fail="Introspection index out of bounds")
|
|
|
|
# Finally, check the tx introspection
|
|
self.log.info("Introspection tests: tx")
|
|
# Test version equality
|
|
self.tapscript_satisfy_test(CScript([OP_INSPECTVERSION, int(2).to_bytes(4, 'little'), OP_EQUAL]), ver = 2)
|
|
self.tapscript_satisfy_test(CScript([OP_INSPECTVERSION, int(5).to_bytes(4, 'little'), OP_EQUAL]), ver = 2, fail="Script evaluated without error but finished with a false/empty top stack element")
|
|
|
|
# Test nlocktime
|
|
self.tapscript_satisfy_test(CScript([OP_INSPECTLOCKTIME, int(7).to_bytes(4, 'little'), OP_EQUAL]), locktime = 7)
|
|
self.tapscript_satisfy_test(CScript([OP_INSPECTLOCKTIME, int(7).to_bytes(4, 'little'), OP_EQUAL]), locktime = 10, fail="Script evaluated without error but finished with a false/empty top stack element")
|
|
|
|
# Test num_inputs
|
|
self.tapscript_satisfy_test(CScript([OP_INSPECTNUMINPUTS, OP_1, OP_EQUAL]))
|
|
self.tapscript_satisfy_test(CScript([OP_INSPECTNUMINPUTS, OP_2, OP_EQUAL]), fail="Script evaluated without error but finished with a false/empty top stack element")
|
|
|
|
# Test num_outputs
|
|
self.tapscript_satisfy_test(CScript([OP_INSPECTNUMOUTPUTS, OP_EQUAL]), add_num_outputs= True)
|
|
|
|
# Test tx weight
|
|
self.tapscript_satisfy_test(CScript([OP_TXWEIGHT, OP_EQUAL]), add_weight= True)
|
|
|
|
# short handle to convert int to 8 byte LE
|
|
def le8(x, signed=True):
|
|
return int(x).to_bytes(8, 'little', signed=signed)
|
|
|
|
def check_add(a, b, c, fail=None):
|
|
self.tapscript_satisfy_test(CScript([OP_ADD64, OP_VERIFY, le8(c), OP_EQUAL]), inputs = [le8(a), le8(b)], fail=fail)
|
|
|
|
def check_sub(a, b, c, fail=None):
|
|
self.tapscript_satisfy_test(CScript([OP_SUB64, OP_VERIFY, le8(c), OP_EQUAL]), inputs = [le8(a), le8(b)], fail=fail)
|
|
|
|
def check_mul(a, b, c, fail=None):
|
|
self.tapscript_satisfy_test(CScript([OP_MUL64, OP_VERIFY, le8(c), OP_EQUAL]), inputs = [le8(a), le8(b)], fail=fail)
|
|
|
|
def check_div(a, b, q, r, fail=None):
|
|
self.tapscript_satisfy_test(CScript([OP_DIV64, OP_VERIFY, le8(q), OP_EQUALVERIFY, le8(r), OP_EQUAL]), inputs = [le8(a), le8(b)], fail=fail)
|
|
|
|
def check_le(a, b, res, fail=None):
|
|
self.tapscript_satisfy_test(CScript([OP_LESSTHAN64, res, OP_EQUAL]), inputs = [le8(a), le8(b)], fail=fail)
|
|
|
|
def check_leq(a, b, res, fail=None):
|
|
self.tapscript_satisfy_test(CScript([OP_LESSTHANOREQUAL64, res, OP_EQUAL]), inputs = [le8(a), le8(b)], fail=fail)
|
|
|
|
def check_ge(a, b, res, fail=None):
|
|
self.tapscript_satisfy_test(CScript([OP_GREATERTHAN64, res, OP_EQUAL]), inputs = [le8(a), le8(b)], fail=fail)
|
|
|
|
def check_geq(a, b, res, fail=None):
|
|
self.tapscript_satisfy_test(CScript([OP_GREATERTHANOREQUAL64, res, OP_EQUAL]), inputs = [le8(a), le8(b)], fail=fail)
|
|
|
|
def check_neg(a, res, fail=None):
|
|
self.tapscript_satisfy_test(CScript([OP_NEG64, OP_VERIFY, le8(res), OP_EQUAL]), inputs = [le8(a)], fail=fail)
|
|
# Arithematic opcodes
|
|
self.log.info("Check Arithmetic opcodes")
|
|
check_add(5, 5, 10)
|
|
check_add(-5, -5, -10)
|
|
check_add(-5, 5, 0)
|
|
check_add(14231, -123213, 14231 - 123213)
|
|
check_add(2**63 - 1, 5, 4, fail="Script failed an OP_VERIFY operation")#overflow
|
|
check_add(5, 2**63 - 1, 4, fail="Script failed an OP_VERIFY operation")#overflow
|
|
check_add(-5, -2**63 + 1, -4, fail="Script failed an OP_VERIFY operation")#overflow
|
|
|
|
# Subtraction
|
|
check_sub(5, 6, -1)
|
|
check_sub(-5, 6, -11)
|
|
check_sub(-5, -5, 0)
|
|
check_sub(14231, -123213, 14231 + 123213)
|
|
check_sub(2**63 - 1, 4, 2**63 - 5)
|
|
check_sub(-5, 2**63 - 1, -4, fail="Script failed an OP_VERIFY operation")#overflow
|
|
check_sub(2**63 - 1, -4, 5, fail="Script failed an OP_VERIFY operation")#overflow
|
|
|
|
# Multiplication
|
|
check_mul(5, 6, 30)
|
|
check_mul(-5, 6, -30)
|
|
check_mul(-5, 0, 0)
|
|
check_mul(-5, -6, 30)
|
|
check_mul(14231, -123213, -14231 * 123213)
|
|
check_mul(2**32, 2**31 - 1, 2**63 - 2**32)
|
|
check_mul(2**32, 2**31, 0, fail="Script failed an OP_VERIFY operation")#overflow
|
|
check_mul(-2**32, 2**31, -2**63)# no overflow
|
|
check_mul(-2**32, -2**32, 0, fail="Script failed an OP_VERIFY operation")#overflow
|
|
|
|
# Division
|
|
check_div(5, 6, 0, 5)
|
|
check_div(4, 2, 2, 0)
|
|
check_div(-5, 6, -1, 1) # r must be in 0<=r<a
|
|
check_div(5, -6, 0, 5) # r must be in 0<=r<a
|
|
check_div(-5, 0, 0, 0, fail="Script failed an OP_VERIFY operation") # only fails if b = 0
|
|
check_div(-2**63, -1, 0, 0, fail="Script failed an OP_VERIFY operation") # failure on -2**63/-1
|
|
check_div(6213123213513, 621, 6213123213513//621, 6213123213513 % 621)
|
|
check_div(2**62, 2**31, 2**31, 0)
|
|
|
|
# Less than test
|
|
# Comparison tests
|
|
# Less than
|
|
check_le(5, 6, 1)
|
|
check_le(5, 5, 0)
|
|
check_le(6, 5, 0)
|
|
|
|
# Less than equal
|
|
check_leq(5, 6, 1)
|
|
check_leq(5, 5, 1)
|
|
check_leq(6, 5, 0)
|
|
|
|
# Greater than
|
|
check_ge(5, 6, 0)
|
|
check_ge(5, 5, 0)
|
|
check_ge(6, 5, 1)
|
|
|
|
# Greater than equal
|
|
check_geq(5, 6, 0)
|
|
check_geq(5, 5, 1)
|
|
check_geq(6, 5, 1)
|
|
|
|
# equal
|
|
check_neg(5, -5)
|
|
check_neg(-5, 5)
|
|
check_neg(5, 4, fail="Script evaluated without error but finished with a false/empty top stack element")
|
|
check_neg(-2**63, 0, fail="Script failed an OP_VERIFY operation")
|
|
check_neg(2**63-1, -2**63 + 1)
|
|
|
|
# Test boolean operations
|
|
for _ in range(5):
|
|
a = randint(-2**63, 2**63 -1)
|
|
b = randint(-2**63, 2**63 -1)
|
|
self.tapscript_satisfy_test(CScript([OP_AND, le8(a & b), OP_EQUAL]), inputs = [le8(a), le8(b)])
|
|
self.tapscript_satisfy_test(CScript([OP_OR, le8(a | b), OP_EQUAL]), inputs = [le8(a), le8(b)])
|
|
self.tapscript_satisfy_test(CScript([OP_INVERT, le8(~a), OP_EQUAL]), inputs = [le8(a)])
|
|
self.tapscript_satisfy_test(CScript([OP_XOR, le8(a ^ b), OP_EQUAL]), inputs = [le8(a), le8(b)])
|
|
|
|
# Finally, test conversion opcodes
|
|
self.log.info("Check conversion opcodes")
|
|
for n in [-2**31 + 1, -1, 0, 1, 2**31 - 1]:
|
|
self.tapscript_satisfy_test(CScript([le8(n), OP_LE64TOSCRIPTNUM, n, OP_EQUAL]))
|
|
self.tapscript_satisfy_test(CScript([n, OP_SCRIPTNUMTOLE64, le8(n), OP_EQUAL]))
|
|
self.tapscript_satisfy_test(CScript([n, OP_DUP, OP_SCRIPTNUMTOLE64, OP_LE64TOSCRIPTNUM, OP_EQUAL]))
|
|
self.tapscript_satisfy_test(CScript([le8(n), OP_DUP, OP_LE64TOSCRIPTNUM, OP_SCRIPTNUMTOLE64, OP_EQUAL]))
|
|
self.tapscript_satisfy_test(CScript([n.to_bytes(4, 'little', signed=True), OP_LE32TOLE64, le8(n), OP_EQUAL]), fail= "Script evaluated without error but finished with a false/empty top stack element" if (n < 0) else None)
|
|
|
|
# Non 8 byte inputs
|
|
self.tapscript_satisfy_test(CScript([OP_ADD64, OP_VERIFY, le8(9), OP_EQUAL]), inputs = [le8(0), int(9).to_bytes(7, 'little')], fail="Arithmetic opcodes expect 8 bytes operands")
|
|
# Overflow inputs
|
|
self.tapscript_satisfy_test(CScript([le8(2**31), OP_LE64TOSCRIPTNUM, 2**31, OP_EQUAL]), fail = "Arithmetic opcode error")
|
|
|
|
|
|
self.log.info("Check Crypto opcodes")
|
|
|
|
res = bytes.fromhex("03a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7")
|
|
scalar = bytes.fromhex("000000000000000000000000000000000000000000000000000000000000000a")
|
|
g = bytes.fromhex("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798")
|
|
self.tapscript_satisfy_test(CScript([OP_ECMULSCALARVERIFY, OP_1]), inputs = [res, g, scalar])
|
|
|
|
res = bytes.fromhex("032c0158d0f6df4881e99e65fbea21f27321d817f79ad39e08eaf4f16f1419bb0c")
|
|
scalar = bytes.fromhex("e0f47c124f228b97bbdc0e4398aac9788869b9fbbc193d5323fdad9570609de6")
|
|
g = bytes.fromhex("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798")
|
|
self.tapscript_satisfy_test(CScript([OP_ECMULSCALARVERIFY, OP_1]), inputs = [res, g, scalar])
|
|
|
|
# test that other random values fail correctly
|
|
res = bytes.fromhex("032c0158d0f6df4881e99e65fbea21f27321d817f79ad39e08eaf4f16f1419bb0c")
|
|
scalar = bytes.fromhex("e0f47c124f228b97bbdc0e4398aac9788869b9fbbc193d5323fdad9570609de6")
|
|
g = bytes.fromhex("0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798")# y coordinate mutated
|
|
self.tapscript_satisfy_test(CScript([OP_ECMULSCALARVERIFY, OP_1]), inputs = [res, g, scalar], fail="Public key is neither compressed or uncompressed")
|
|
|
|
invalid_full_pks_33 = [
|
|
"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81700" # invalid point on curve
|
|
]
|
|
invalid_full_pks_non33 = [
|
|
"0979be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", # 33 byte without 0x02,0x03
|
|
"0462b31419b87d5e095e9d7532f92aea39c39735253a48fbdaff48441d5cf706f9a7a946ce481411e4034143e0acad4e79aecb2a8212ee5cca26a7cc5fe5b45881", # compressed
|
|
"62b31419b87d5e095e9d7532f92aea39c39735253a48fbdaff48441d5cf706f9", # x-only key
|
|
"0262b31419b87d5e095e9d7532f92aea39c39735253a48fbdaff48441d5cf706f9f9f9" # 34 byte key
|
|
]
|
|
invalid_tweaks = [
|
|
"e07c124f228b97bbdc0e4398aac9788869b9fbbc193d5323fdad9570609de6", # non 32 byte hash
|
|
"fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364143" # order + 2
|
|
]
|
|
invalid_xonly_keys = [
|
|
"0379be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", # 33 byte valid key
|
|
"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81700", # invalid point on curve
|
|
"0462b31419b87d5e095e9d7532f92aea39c39735253a48fbdaff48441d5cf706f9a7a946ce481411e4034143e0acad4e79aecb2a8212ee5cca26a7cc5fe5b45881", # compressed
|
|
"62b31419b87d5e095e9d7532f92aea39c39735253a48fbdaff48441d5cf706" # 31 byte x-only key
|
|
]
|
|
|
|
valid_res = bytes.fromhex("032c0158d0f6df4881e99e65fbea21f27321d817f79ad39e08eaf4f16f1419bb0c")
|
|
valid_scalar = bytes.fromhex("e0f47c124f228b97bbdc0e4398aac9788869b9fbbc193d5323fdad9570609de6")
|
|
valid_g = bytes.fromhex("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798")
|
|
valid_xonly_key = bytes.fromhex("79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798")
|
|
|
|
for invalid_tweak in invalid_tweaks:
|
|
self.tapscript_satisfy_test(CScript([OP_ECMULSCALARVERIFY, OP_1]), inputs = [valid_res, valid_g, bytes.fromhex(invalid_tweak)], fail="EC scalar mult verify fail")
|
|
|
|
self.tapscript_satisfy_test(CScript([OP_TWEAKVERIFY, OP_1]), inputs = [valid_res, bytes.fromhex(invalid_tweaks[1]), valid_xonly_key], fail="EC scalar mult verify fail")
|
|
self.tapscript_satisfy_test(CScript([OP_TWEAKVERIFY, OP_1]), inputs = [valid_res, bytes.fromhex(invalid_tweaks[0]), valid_xonly_key], fail="Public key is neither compressed or uncompressed")
|
|
|
|
for invalid_full_pk in invalid_full_pks_33:
|
|
self.tapscript_satisfy_test(CScript([OP_ECMULSCALARVERIFY, OP_1]), inputs = [bytes.fromhex(invalid_full_pk), valid_g, valid_scalar], fail="EC scalar mult verify fail")
|
|
self.tapscript_satisfy_test(CScript([OP_ECMULSCALARVERIFY, OP_1]), inputs = [valid_res, bytes.fromhex(invalid_full_pk), valid_scalar], fail="EC scalar mult verify fail")
|
|
|
|
for invalid_full_pk in invalid_full_pks_non33:
|
|
self.tapscript_satisfy_test(CScript([OP_ECMULSCALARVERIFY, OP_1]), inputs = [bytes.fromhex(invalid_full_pk), valid_g, valid_scalar], fail="Public key is neither compressed or uncompressed")
|
|
self.tapscript_satisfy_test(CScript([OP_ECMULSCALARVERIFY, OP_1]), inputs = [valid_res, bytes.fromhex(invalid_full_pk), valid_scalar], fail="Public key is neither compressed or uncompressed")
|
|
|
|
self.tapscript_satisfy_test(CScript([OP_TWEAKVERIFY, OP_1]), inputs = [valid_res, valid_scalar, bytes.fromhex(invalid_xonly_keys[0])], fail="Public key is neither compressed or uncompressed")
|
|
self.tapscript_satisfy_test(CScript([OP_TWEAKVERIFY, OP_1]), inputs = [valid_res, valid_scalar, bytes.fromhex(invalid_xonly_keys[1])], fail="EC scalar mult verify fail")
|
|
self.tapscript_satisfy_test(CScript([OP_TWEAKVERIFY, OP_1]), inputs = [valid_res, valid_scalar, bytes.fromhex(invalid_xonly_keys[2])], fail="Public key is neither compressed or uncompressed")
|
|
self.tapscript_satisfy_test(CScript([OP_TWEAKVERIFY, OP_1]), inputs = [valid_res, valid_scalar, bytes.fromhex(invalid_xonly_keys[3])], fail="Public key is neither compressed or uncompressed")
|
|
|
|
# Checksigfromstack tests
|
|
# 32 byte msg
|
|
def csfs_test(msg, mutute_sig = False, fail=None):
|
|
sec = generate_privkey()
|
|
msg = bytes.fromhex(msg)
|
|
pub = compute_xonly_pubkey(sec)[0]
|
|
sig = sign_schnorr(sec, msg)
|
|
if mutute_sig:
|
|
new_sig = sig[:63]
|
|
new_sig += (sig[63] ^ 0xff).to_bytes(1, 'little') # flip the last bit
|
|
sig = new_sig
|
|
self.tapscript_satisfy_test(CScript([msg, pub, OP_CHECKSIGFROMSTACK]), inputs = [sig], fail=fail)
|
|
|
|
csfs_test("e168c349d0d2499caf3a6d71734c743d517f94f8571fa52c04285b68deec1936")
|
|
csfs_test("Hello World!".encode('utf-8').hex(), fail="Invalid Schnorr signature", mutute_sig=True)
|
|
msg = bytes.fromhex("e168c349d0d2499caf3a6d71734c743d517f94f8571fa52c04285b68deec1936")
|
|
pub = bytes.fromhex("3f67e97da0df6931189cfb0072447da22707897bd5de04936a277ed7e00b35b3")
|
|
self.tapscript_satisfy_test(CScript([OP_0, msg, pub, OP_CHECKSIGFROMSTACK, OP_NOT]), inputs = [])
|
|
short_sig = bytes.fromhex("0102")
|
|
self.tapscript_satisfy_test(CScript([short_sig, msg, pub, OP_CHECKSIGFROMSTACK, OP_NOT]), inputs = [], fail="Invalid Schnorr signature size")
|
|
|
|
short_pub = bytes.fromhex("67e97da0df6931189cfb0072447da22707897bd5de04936a277ed7e00b35b3")
|
|
long_pub = bytes.fromhex("67e97da0df6931189cfb0072447da22707897bd5de04936a277ed7e00b35b30908")
|
|
sig = bytes.fromhex("3f67e97da0df6931189cfb0072447da22707897bd5de04936a277ed7e00b35b33f67e97da0df6931189cfb0072447da22707897bd5de04936a277ed7e00b35b3")
|
|
self.tapscript_satisfy_test(CScript([msg, short_pub, OP_CHECKSIGFROMSTACK]), inputs = [sig], fail="Public key version reserved for soft-fork upgrades")
|
|
self.tapscript_satisfy_test(CScript([msg, long_pub, OP_CHECKSIGFROMSTACK]), inputs = [sig], fail="Public key version reserved for soft-fork upgrades")
|
|
|
|
if __name__ == '__main__':
|
|
TapHashPeginTest(__file__).main()
|