# (c) Copyright 2026 by Coinkite Inc. This file is covered by license found in COPYING-CC. # # construct Proof of Reserves transaction according to BIP-322 # import struct, hashlib from ckcc_protocol.protocol import MAX_TXN_LEN from psbt import BasicPSBT, BasicPSBTInput, BasicPSBTOutput from io import BytesIO from helpers import hash160, str_to_path, taptweak from bip32 import BIP32Node, PublicKey from constants import simulator_fixed_tprv, AF_P2WSH, AF_P2WSH_P2SH, AF_P2SH from ctransaction import CTransaction, COutPoint, CTxIn, CTxOut, uint256_from_str from sighash import legacy_sighash, segwit_v0_sighash, taproot_sighash, SIGHASH_DEFAULT, SIGHASH_ALL from pysecp256k1 import ec_pubkey_parse, ecdsa_signature_parse_der, ecdsa_verify from pysecp256k1.extrakeys import xonly_pubkey_parse from pysecp256k1.schnorrsig import schnorrsig_verify def bip322_msg_hash(msg): tag_hash = hashlib.sha256(b'BIP0322-signed-message').digest() return hashlib.sha256(tag_hash + tag_hash + msg).digest() def ecdsa_verify_sig(pubkey, sig, digest): if not sig or sig[-1] != SIGHASH_ALL: return False try: parsed = ecdsa_signature_parse_der(sig[:-1]) return bool(ecdsa_verify(parsed, ec_pubkey_parse(pubkey), digest)) except Exception: return False def bip322_verify(psbt_bytes): """Verify BIP-322 PSBT signatures without a full script interpreter. Enforces the BIP-322 transaction shape, SIGHASH_ALL for ECDSA, SIGHASH_DEFAULT/SIGHASH_ALL for taproot, and direct signature checks for p2pkh, p2wpkh, p2sh-p2wpkh, sh, wsh, and p2tr key-path. It intentionally omits consensus-level script evaluation rules such as CLEANSTACK, MINIMALIF, NULLFAIL beyond empty CHECKMULTISIG dummy, CODESEPARATOR/FindAndDelete handling, and NOP-upgrade checks; unsupported scripts raise AssertionError. """ psbt = BasicPSBT().parse(psbt_bytes) assert psbt.bip322_msg is not None msg = psbt.bip322_msg tx = CTransaction() if psbt.txn: tx.deserialize(BytesIO(psbt.txn)) else: tx.nVersion = psbt.txn_version tx.nLockTime = psbt.fallback_locktime or 0 for inp in psbt.inputs: tx.vin.append(CTxIn(COutPoint(uint256_from_str(inp.previous_txid), inp.prevout_idx), nSequence=inp.sequence if inp.sequence is not None else 0xffffffff)) for out in psbt.outputs: tx.vout.append(CTxOut(out.amount, out.script)) inp0 = psbt.inputs[0] to_spend = None if inp0.utxo: to_spend = CTransaction() to_spend.deserialize(BytesIO(inp0.utxo)) assert len(to_spend.vout) == 1 assert to_spend.vout[0].nValue == 0 script_pubkey = to_spend.vout[0].scriptPubKey else: assert inp0.witness_utxo witness_utxo = CTxOut() witness_utxo.deserialize(BytesIO(inp0.witness_utxo)) assert witness_utxo.nValue == 0 script_pubkey = witness_utxo.scriptPubKey expected_to_spend = CTransaction() expected_to_spend.nVersion = 0 expected_to_spend.nLockTime = 0 expected_to_spend.vin = [CTxIn(COutPoint(hash=0, n=0xffffffff), scriptSig=b'\x00\x20' + bip322_msg_hash(msg), nSequence=0)] expected_to_spend.vout = [CTxOut(0, script_pubkey)] expected_to_spend.calc_sha256() if to_spend: assert to_spend.serialize_without_witness() == expected_to_spend.serialize_without_witness() to_spend = expected_to_spend assert tx.nVersion in (0, 2) assert len(tx.vin) >= 1 assert tx.vin[0].prevout.hash == to_spend.sha256 assert tx.vin[0].prevout.n == 0 assert not (len(tx.vin) == 1 and (tx.vin[0].nSequence != 0 or tx.nLockTime != 0)) assert len(tx.vout) == 1 assert tx.vout[0].nValue == 0 assert tx.vout[0].scriptPubKey == b'\x6a' prevouts = [] for idx, txin in enumerate(tx.vin): if idx == 0: prevouts.append((0, script_pubkey)) else: assert idx < len(psbt.inputs) if psbt.inputs[idx].witness_utxo: prev = CTxOut() prev.deserialize(BytesIO(psbt.inputs[idx].witness_utxo)) else: prev_tx = CTransaction() prev_tx.deserialize(BytesIO(psbt.inputs[idx].utxo)) prev = prev_tx.vout[txin.prevout.n] prevouts.append((prev.nValue, prev.scriptPubKey)) for idx, txin in enumerate(tx.vin): amount, spk = prevouts[idx] inp = psbt.inputs[idx] if len(spk) == 25 and spk[:3] == b'\x76\xa9\x14' and spk[-2:] == b'\x88\xac': assert len(inp.part_sigs) == 1 pub, sig = next(iter(inp.part_sigs.items())) assert hash160(pub) == spk[3:23] assert ecdsa_verify_sig(pub, sig, legacy_sighash(tx, idx, spk)) continue if len(spk) == 22 and spk[:2] == b'\x00\x14': assert len(inp.part_sigs) == 1 pub, sig = next(iter(inp.part_sigs.items())) assert hash160(pub) == spk[2:22] script_code = b'\x76\xa9\x14' + spk[2:22] + b'\x88\xac' assert ecdsa_verify_sig(pub, sig, segwit_v0_sighash(tx, idx, script_code, amount)) continue if len(spk) == 34 and spk[:2] == b'\x00\x20': assert inp.witness_script assert hashlib.sha256(inp.witness_script).digest() == spk[2:34] assert inp.part_sigs sighash = segwit_v0_sighash(tx, idx, inp.witness_script, amount) for pub, sig in inp.part_sigs.items(): assert ecdsa_verify_sig(pub, sig, sighash) continue if len(spk) == 34 and spk[:2] == b'\x51\x20': assert inp.taproot_key_sig if len(inp.taproot_key_sig) == 64: sighash = SIGHASH_DEFAULT sig = inp.taproot_key_sig else: assert len(inp.taproot_key_sig) == 65 sighash = inp.taproot_key_sig[-1] sig = inp.taproot_key_sig[:-1] digest = taproot_sighash(tx, idx, prevouts, sighash) assert schnorrsig_verify(sig, digest, xonly_pubkey_parse(spk[2:34])) continue if len(spk) == 23 and spk[:2] == b'\xa9\x14' and spk[-1:] == b'\x87': assert inp.redeem_script assert hash160(inp.redeem_script) == spk[2:22] if len(inp.redeem_script) == 22 and inp.redeem_script[:2] == b'\x00\x14': assert len(inp.part_sigs) == 1 pub, sig = next(iter(inp.part_sigs.items())) assert hash160(pub) == inp.redeem_script[2:22] script_code = b'\x76\xa9\x14' + inp.redeem_script[2:22] + b'\x88\xac' assert ecdsa_verify_sig(pub, sig, segwit_v0_sighash(tx, idx, script_code, amount)) continue if len(inp.redeem_script) == 34 and inp.redeem_script[:2] == b'\x00\x20': assert inp.witness_script assert inp.redeem_script == b'\x00\x20' + hashlib.sha256(inp.witness_script).digest() assert inp.part_sigs sighash = segwit_v0_sighash(tx, idx, inp.witness_script, amount) for pub, sig in inp.part_sigs.items(): assert ecdsa_verify_sig(pub, sig, sighash) continue assert inp.part_sigs sighash = legacy_sighash(tx, idx, inp.redeem_script) for pub, sig in inp.part_sigs.items(): assert ecdsa_verify_sig(pub, sig, sighash) continue assert False, "unsupported script" def bip322_txn(inputs, msg=b"POR", addr_fmt="p2wpkh", input_amount=1E8, to_sign_lock_time=0, sighash=None, psbt_hacker=None, witness_utxo=[], to_sign_nVersion=0, psbt_v2=False, master_xpub=None): msg_challenge = None num_ins = len(inputs) psbt = BasicPSBT() psbt.bip322_msg = msg to_sign = CTransaction() to_sign.nLockTime = to_sign_lock_time # must be set to 2 if BIP-68 is used (relative tx level lock) to_sign.nVersion = to_sign_nVersion master_xpub = master_xpub or simulator_fixed_tprv # we have a key; use it to provide "plausible" value inputs mk = BIP32Node.from_wallet_key(master_xpub) mfp = mk.fingerprint() psbt.inputs = [BasicPSBTInput(idx=i) for i in range(num_ins)] psbt.outputs = [] for i, inp in enumerate(inputs): sp = f"0/{i}" af = addr_fmt ia = input_amount pubkey = None # public key try: if inp[0] is not None: af = inp[0] if inp[1] is not None: sp = inp[1] if inp[2] is not None: ia = inp[2] if inp[3] is not None: pubkey = inp[3] except: pass if pubkey: int_path = [0] sec = pubkey else: int_path = str_to_path(sp) sec = mk.subkey_for_path(sp).sec() subkey = PublicKey.parse(sec) assert len(sec) == 33, "expect compressed" if af == "p2tr": tweaked_xonly = taptweak(sec[1:]) psbt.inputs[i].taproot_bip32_paths[sec[1:]] = b"\x00" + mfp + struct.pack(f'<{"I" * len(int_path)}', *int_path) scr = bytes([81, 32]) + tweaked_xonly elif af in ("p2wpkh", "p2sh-p2wpkh", "p2wpkh-p2sh"): psbt.inputs[i].bip32_paths[sec] = mfp + struct.pack(f'<{"I" * len(int_path)}', *int_path) scr = bytes([0x00, 0x14]) + subkey.h160() if af != "p2wpkh": # use classic p2wpkh (from above) as redeem script psbt.inputs[i].redeem_script = scr scr = bytes([0xa9, 0x14]) + hash160(scr) + bytes([0x87]) elif af == "p2pkh": psbt.inputs[i].bip32_paths[sec] = mfp + struct.pack(f'<{"I" * len(int_path)}', *int_path) scr = bytes([0x76, 0xa9, 0x14]) + subkey.h160() + bytes([0x88, 0xac]) else: raise ValueError("unknown addr_fmt %s" % af) if i == 0: # first input always spends to_spend to_spend = CTransaction() to_spend.nVersion = 0 out_point = COutPoint(hash=0, n=0xffffffff) msg_hash = bip322_msg_hash(msg) to_spend.vin = [CTxIn(out_point, scriptSig=b'\x00\x20' + msg_hash)] to_spend.vout = [CTxOut(0, scr)] # always zero val msg_challenge = scr else: # other outputs that we want to prove ownership to_spend = CTransaction() to_spend.nVersion = 0 out_point = COutPoint( uint256_from_str(struct.pack('4Q', 0xdead, 0xbeef, 0, i)), 73 ) to_spend.vin = [CTxIn(out_point, nSequence=0xffffffff)] to_spend.vout.append(CTxOut(int(ia), scr)) if sighash is not None: psbt.inputs[i].sighash = sighash to_spend.calc_sha256() if i in witness_utxo: psbt.inputs[i].witness_utxo = to_spend.vout[-1].serialize() else: psbt.inputs[i].utxo = to_spend.serialize_with_witness() if len(inputs) == 1: # basic msg sign seq = 0 else: if to_sign_lock_time and not i: seq = 0xfffffffd else: seq = 0xffffffff spendable = CTxIn(COutPoint(to_spend.sha256, 0), nSequence=seq) to_sign.vin.append(spendable) # just one zero amount output with script null data OP_RETURN op_ret_o = BasicPSBTOutput(idx=0) op_return_out = CTxOut(0, b'\x6a') to_sign.vout.append(op_return_out) psbt.outputs.append(op_ret_o) psbt.txn = to_sign.serialize_with_witness() # last minute chance to mod PSBT object if psbt_hacker: psbt_hacker(psbt) if psbt_v2: psbt.parsed_txn = CTransaction() psbt.parsed_txn.deserialize(BytesIO(psbt.txn)) psbt.to_v2() rv = BytesIO() psbt.serialize(rv) assert rv.tell() <= MAX_TXN_LEN, 'too fat' return rv.getvalue(), msg_challenge def bip322_ms_txn(num_ins, M, keys, msg=b"POR", inp_af=AF_P2WSH, input_amount=1E8, path_mapper=None, lock_time=0, with_sigs=False, sighash=None, hack_psbt=None, to_sign_nVersion=0, psbt_v2=False): from test_multisig import make_ms_address msg_challenge = None psbt = BasicPSBT() psbt.bip322_msg = msg txn = CTransaction() txn.nVersion = to_sign_nVersion txn.nLockTime = lock_time psbt.inputs = [BasicPSBTInput(idx=i) for i in range(num_ins)] psbt.outputs = [] for i in range(num_ins): # make a fake txn to supply each of the inputs # - each input is 1BTC # addr where the fake money will be stored. addr, scriptPubKey, script, details = make_ms_address( M, keys, idx=i, addr_fmt=inp_af, path_mapper=path_mapper ) if inp_af == AF_P2WSH: psbt.inputs[i].witness_script = script elif inp_af == AF_P2SH: psbt.inputs[i].redeem_script = script else: assert inp_af == AF_P2WSH_P2SH psbt.inputs[i].witness_script = script psbt.inputs[i].redeem_script = b'\0\x20' + hashlib.sha256(script).digest() for pubkey, xfp_path in details: psbt.inputs[i].bip32_paths[pubkey] = b''.join(struct.pack('