Files
bips/bip-0460/test-vectors.py
T

1507 lines
67 KiB
Python

#!/usr/bin/env python3
"""
Test vector generator for the CISA Taproot key path BIP (witness v2).
Generates two files:
wallet-test-vectors.json scriptPubKey/address derivation, sighash
computation, and witness construction
consensus-test-vectors.json transaction level valid/invalid cases
WARNING: All keys and nonces in this file are deterministic and publicly
known. They exist only to make the vectors reproducible. Never use this
code or these values in production.
"""
import hashlib
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent / "secp256k1lab/src"))
sys.path.insert(0, str(Path(__file__).parent))
from secp256k1lab.secp256k1 import G, GE, Scalar
from secp256k1lab.bip340 import schnorr_sign, schnorr_verify
from secp256k1lab.util import tagged_hash
import halfagg
import fullagg
# Marker bytes
MARKER_HALFAGG = 0xBC
MARKER_FULLAGG = 0xBD
# Sighash epoch for aggregated witness v2 signature messages
SIGHASH_EPOCH = 0x01
SIGHASH_DEFAULT = 0x00
SIGHASH_ALL = 0x01
SIGHASH_NONE = 0x02
SIGHASH_SINGLE = 0x03
SIGHASH_ANYONECANPAY = 0x80
AUX_ZERO = bytes(32)
# ---------------------------------------------------------------------------
# Serialization helpers
# ---------------------------------------------------------------------------
def sha256(b):
return hashlib.sha256(b).digest()
def ser_compact_size(n):
if n < 253:
return bytes([n])
if n < 0x10000:
return b"\xfd" + n.to_bytes(2, "little")
if n < 0x100000000:
return b"\xfe" + n.to_bytes(4, "little")
return b"\xff" + n.to_bytes(8, "little")
class TxIn:
def __init__(self, txid, vout, sequence=0xFFFFFFFF, script_sig=b""):
self.txid = txid # 32 bytes, internal byte order
self.vout = vout
self.sequence = sequence
self.script_sig = script_sig
def outpoint(self):
return self.txid + self.vout.to_bytes(4, "little")
class TxOut:
def __init__(self, amount, script_pubkey):
self.amount = amount
self.script_pubkey = script_pubkey
def serialize(self):
return self.amount.to_bytes(8, "little") + ser_compact_size(
len(self.script_pubkey)
) + self.script_pubkey
class Tx:
def __init__(self, vin, vout, version=2, locktime=0):
self.version = version
self.locktime = locktime
self.vin = vin
self.vout = vout
self.witnesses = [[] for _ in vin]
def serialize_unsigned(self):
out = self.version.to_bytes(4, "little")
out += ser_compact_size(len(self.vin))
for txin in self.vin:
out += (txin.outpoint() + ser_compact_size(len(txin.script_sig))
+ txin.script_sig + txin.sequence.to_bytes(4, "little"))
out += ser_compact_size(len(self.vout))
for txout in self.vout:
out += txout.serialize()
out += self.locktime.to_bytes(4, "little")
return out
def serialize_signed(self):
out = self.version.to_bytes(4, "little")
out += b"\x00\x01"
out += ser_compact_size(len(self.vin))
for txin in self.vin:
out += (txin.outpoint() + ser_compact_size(len(txin.script_sig))
+ txin.script_sig + txin.sequence.to_bytes(4, "little"))
out += ser_compact_size(len(self.vout))
for txout in self.vout:
out += txout.serialize()
for witness in self.witnesses:
out += ser_compact_size(len(witness))
for element in witness:
out += ser_compact_size(len(element)) + element
out += self.locktime.to_bytes(4, "little")
return out
# ---------------------------------------------------------------------------
# Taproot style key derivation
# ---------------------------------------------------------------------------
def tweak_keypair(seckey32):
d0 = Scalar.from_bytes_checked(seckey32)
P = d0 * G
d = d0 if P.has_even_y() else -d0
internal_pubkey = P.to_bytes_xonly()
t = Scalar.from_bytes_checked(tagged_hash("TapTweak", internal_pubkey))
Q = (d + t) * G
tweaked_seckey = (d + t).to_bytes()
return internal_pubkey, t.to_bytes(), tweaked_seckey, Q.to_bytes_xonly()
def v2_script_pubkey(output_key32):
return bytes([0x52, 0x20]) + output_key32
# ---------------------------------------------------------------------------
# Bech32m (adapted from the BIP 350 reference implementation)
# ---------------------------------------------------------------------------
CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
BECH32M_CONST = 0x2BC830A3
def bech32_polymod(values):
generator = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3]
chk = 1
for value in values:
top = chk >> 25
chk = (chk & 0x1FFFFFF) << 5 ^ value
for i in range(5):
chk ^= generator[i] if ((top >> i) & 1) else 0
return chk
def bech32_hrp_expand(hrp):
return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]
def bech32m_create_checksum(hrp, data):
values = bech32_hrp_expand(hrp) + data
polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ BECH32M_CONST
return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
def convertbits(data, frombits, tobits, pad=True):
acc = 0
bits = 0
ret = []
maxv = (1 << tobits) - 1
for value in data:
acc = (acc << frombits) | value
bits += frombits
while bits >= tobits:
bits -= tobits
ret.append((acc >> bits) & maxv)
if pad and bits:
ret.append((acc << (tobits - bits)) & maxv)
return ret
def v2_address(output_key32, hrp="bc"):
data = [2] + convertbits(output_key32, 8, 5)
combined = data + bech32m_create_checksum(hrp, data)
return hrp + "1" + "".join([CHARSET[d] for d in combined])
# ---------------------------------------------------------------------------
# Witness v2 common signature message
# ---------------------------------------------------------------------------
def sigmsg_common(tx, spent_utxos, input_index, hash_type, ext_flag, annex=None):
"""Compute SigMsg(hash_type, ext_flag) as defined in BIP 341."""
assert len(spent_utxos) == len(tx.vin)
anyonecanpay = bool(hash_type & SIGHASH_ANYONECANPAY)
base_type = hash_type & 3
msg = bytes([hash_type])
msg += tx.version.to_bytes(4, "little")
msg += tx.locktime.to_bytes(4, "little")
if not anyonecanpay:
msg += sha256(b"".join(txin.outpoint() for txin in tx.vin))
msg += sha256(b"".join(u.amount.to_bytes(8, "little") for u in spent_utxos))
msg += sha256(
b"".join(
ser_compact_size(len(u.script_pubkey)) + u.script_pubkey
for u in spent_utxos
)
)
msg += sha256(
b"".join(txin.sequence.to_bytes(4, "little") for txin in tx.vin)
)
if base_type not in (SIGHASH_NONE, SIGHASH_SINGLE):
msg += sha256(b"".join(txout.serialize() for txout in tx.vout))
spend_type = 2 * ext_flag + (1 if annex is not None else 0)
msg += bytes([spend_type])
if anyonecanpay:
txin = tx.vin[input_index]
utxo = spent_utxos[input_index]
msg += txin.outpoint()
msg += utxo.amount.to_bytes(8, "little")
msg += ser_compact_size(len(utxo.script_pubkey)) + utxo.script_pubkey
msg += txin.sequence.to_bytes(4, "little")
else:
msg += input_index.to_bytes(4, "little")
if annex is not None:
msg += sha256(ser_compact_size(len(annex)) + annex)
if base_type == SIGHASH_SINGLE:
msg += sha256(tx.vout[input_index].serialize())
return msg
def sigmsg_v2(tx, spent_utxos, input_index, hash_type, agg_mode, annex=None):
"""Compute hash_TapSighash(0x01 || agg_mode || SigMsg(hash_type, 0))."""
msg = sigmsg_common(tx, spent_utxos, input_index, hash_type, 0, annex)
return tagged_hash("TapSighash", bytes([SIGHASH_EPOCH, agg_mode]) + msg)
def sigmsg_tapscript(tx, spent_utxos, input_index, hash_type, tapleaf_hash,
annex=None):
"""Compute the unchanged BIP 342 tapscript signature message,
hash_TapSighash(0x00 || SigMsg(hash_type, 1) || ext)."""
msg = sigmsg_common(tx, spent_utxos, input_index, hash_type, 1, annex)
ext = tapleaf_hash + b"\x00" + b"\xff\xff\xff\xff"
return tagged_hash("TapSighash", bytes([0x00]) + msg + ext)
def sigmsg_v1(tx, spent_utxos, input_index, hash_type, annex=None):
"""Compute the unchanged BIP 341 key path signature message,
hash_TapSighash(0x00 || SigMsg(hash_type, 0))."""
msg = sigmsg_common(tx, spent_utxos, input_index, hash_type, 0, annex)
return tagged_hash("TapSighash", bytes([0x00]) + msg)
# ---------------------------------------------------------------------------
# Witness element construction
# ---------------------------------------------------------------------------
def marker_element(marker, hash_type=SIGHASH_DEFAULT, sig=b""):
element = sig
if hash_type != SIGHASH_DEFAULT:
element += bytes([hash_type])
return element + bytes([marker])
def optout_element(sig, hash_type=SIGHASH_DEFAULT):
if hash_type == SIGHASH_DEFAULT:
return sig
return sig + bytes([hash_type])
def halfagg_member(nonce_share, hash_type=SIGHASH_DEFAULT):
if hash_type == SIGHASH_DEFAULT:
return nonce_share
return nonce_share + bytes([hash_type])
def fullagg_member(hash_type=SIGHASH_DEFAULT):
if hash_type == SIGHASH_DEFAULT:
return b""
return bytes([hash_type])
def halfagg_final(aggsig, hash_type=SIGHASH_DEFAULT):
return marker_element(MARKER_HALFAGG, hash_type, aggsig[-64:])
# ---------------------------------------------------------------------------
# Deterministic test data
# ---------------------------------------------------------------------------
def test_seckey(i):
return tagged_hash("CISA/test/key", bytes([i]))
def test_prevout_txid(i):
return sha256(b"CISA vector prevout " + bytes([i]))
def test_fullagg_secnonce(i):
# Test-only deterministic nonces. Nonces must be fresh uniform
# randomness in any real signing session.
r1 = Scalar.from_bytes_wrapping(tagged_hash("CISA/test/nonce", bytes([i, 0])))
r2 = Scalar.from_bytes_wrapping(tagged_hash("CISA/test/nonce", bytes([i, 1])))
return (r1, r2), (r1 * G, r2 * G)
def test_garbage(label, length=32):
out = b""
i = 0
while len(out) < length:
out += tagged_hash("CISA/test/garbage", label + bytes([i]))
i += 1
return out[:length]
def test_offcurve_x(label):
# Deterministically find 32 bytes that are not a valid x coordinate.
i = 0
while True:
cand = tagged_hash("CISA/test/offcurve", label + bytes([i]))
try:
GE.from_bytes_xonly(cand)
except ValueError:
return cand
i += 1
# ---------------------------------------------------------------------------
# Signing helpers
# ---------------------------------------------------------------------------
def sign_optout(tx, utxos, idx, tweaked_seckey, hash_type):
m = sigmsg_v1(tx, utxos, idx, hash_type)
sig = schnorr_sign(m, tweaked_seckey, AUX_ZERO)
return m, sig
def sign_halfagg_group(tx, utxos, members):
"""members: list of (input_index, tweaked_seckey, hash_type).
Returns (msgs, plain_sigs, aggsig)."""
triples = []
msgs = []
for idx, sk, ht in members:
m = sigmsg_v2(tx, utxos, idx, ht, MARKER_HALFAGG)
sig = schnorr_sign(m, sk, AUX_ZERO)
pk = utxos[idx].script_pubkey[2:]
triples.append((pk, m, sig))
msgs.append(m)
aggsig = halfagg.Aggregate(triples)
assert halfagg.VerifyAggregate(aggsig, [(pk, m) for pk, m, _ in triples])
return msgs, [t[2] for t in triples], aggsig
def sign_fullagg_group(tx, utxos, members, msg_mode=MARKER_FULLAGG,
nonce_offset=0, annexes=None):
"""members: list of (input_index, tweaked_seckey, hash_type).
msg_mode overrides the marker committed in the signature messages,
with None selecting the plain BIP 341 message, used by negative
vectors exercising the mode commitment.
nonce_offset varies the deterministic test nonces between sessions.
annexes optionally maps input indices to their annex.
Returns (msgs, secnonces, pubnonces, sig64)."""
pks = []
msgs = []
secnonces = []
pubnonces = []
for i, (idx, sk, ht) in enumerate(members):
annex = annexes.get(idx) if annexes else None
if msg_mode is None:
m = sigmsg_v1(tx, utxos, idx, ht, annex=annex)
else:
m = sigmsg_v2(tx, utxos, idx, ht, msg_mode, annex=annex)
msgs.append(m)
pks.append(GE.from_bytes_xonly(utxos[idx].script_pubkey[2:]))
secnonce, pubnonce = test_fullagg_secnonce(i + nonce_offset)
secnonces.append(secnonce)
pubnonces.append(pubnonce)
aggnonce = fullagg.NonceAgg(pubnonces)
psigs = []
for i, (idx, sk, ht) in enumerate(members):
d = Scalar.from_bytes_checked(sk)
psig = fullagg.Sign(secnonces[i], d, msgs[i], aggnonce, pks, msgs, pubnonces)
psigs.append(psig)
R, s = fullagg.SigAgg(aggnonce, pks, msgs, pubnonces, psigs)
assert fullagg.Verify(pks, msgs, (R, s))
sig64 = R.to_bytes_xonly() + s.to_bytes()
return msgs, secnonces, pubnonces, sig64
# ---------------------------------------------------------------------------
# Vector construction
# ---------------------------------------------------------------------------
def hexlify(b):
return b.hex()
def make_wallet_vectors():
vectors = {"scriptPubKey": [], "keyPathSpending": []}
# scriptPubKey and address derivation
for i in range(2):
seckey = test_seckey(i)
internal_pubkey, tweak, tweaked_seckey, output_key = tweak_keypair(seckey)
spk = v2_script_pubkey(output_key)
vectors["scriptPubKey"].append(
{
"given": {"internalPubkey": hexlify(internal_pubkey)},
"intermediary": {
"tweak": hexlify(tweak),
"tweakedPubkey": hexlify(output_key),
},
"expected": {
"scriptPubKey": hexlify(spk),
"address": v2_address(output_key),
},
}
)
# Case 1: opted-out input plus a two-member half-aggregation group
keys = [tweak_keypair(test_seckey(i)) for i in range(3)]
utxos = [
TxOut(100_000_000 + i * 1_000_000, v2_script_pubkey(k[3]))
for i, k in enumerate(keys)
]
tx = Tx(
vin=[TxIn(test_prevout_txid(i), 0) for i in range(3)],
vout=[TxOut(299_000_000, v2_script_pubkey(keys[0][3]))],
)
m0, sig0 = sign_optout(tx, utxos, 0, keys[0][2], SIGHASH_ALL)
msgs, sigs, aggsig = sign_halfagg_group(
tx, utxos, [(1, keys[1][2], SIGHASH_DEFAULT), (2, keys[2][2], SIGHASH_DEFAULT)]
)
tx.witnesses[0] = [optout_element(sig0, SIGHASH_ALL)]
tx.witnesses[1] = [halfagg_member(sigs[0][:32])]
tx.witnesses[2] = [halfagg_final(aggsig)]
case1 = {
"description": "Opted-out input with SIGHASH_ALL alongside a "
"two-member half-aggregation group with SIGHASH_DEFAULT",
"given": {
"rawUnsignedTx": hexlify(tx.serialize_unsigned()),
"utxosSpent": [
{"scriptPubKey": hexlify(u.script_pubkey), "amountSats": u.amount}
for u in utxos
],
},
"inputSpending": [],
"auxiliary": {"aggregateSignature": hexlify(aggsig)},
"expected": {"rawSignedTx": hexlify(tx.serialize_signed())},
}
input_data = [
(0, None, SIGHASH_ALL, m0),
(1, MARKER_HALFAGG, SIGHASH_DEFAULT, msgs[0]),
(2, MARKER_HALFAGG, SIGHASH_DEFAULT, msgs[1]),
]
for idx, marker, ht, m in input_data:
case1["inputSpending"].append(
{
"given": {
"txinIndex": idx,
"internalPrivkey": hexlify(test_seckey(idx)),
"aggMode": None if marker is None else f"0x{marker:02x}",
"hashType": ht,
},
"intermediary": {
"internalPubkey": hexlify(keys[idx][0]),
"tweak": hexlify(keys[idx][1]),
"tweakedPrivkey": hexlify(keys[idx][2]),
"sigHash": hexlify(m),
},
"expected": {"witness": [hexlify(e) for e in tx.witnesses[idx]]},
}
)
vectors["keyPathSpending"].append(case1)
# Case 2: two-member full-aggregation group
keys = [tweak_keypair(test_seckey(i + 3)) for i in range(2)]
utxos = [
TxOut(50_000_000, v2_script_pubkey(keys[0][3])),
TxOut(70_000_000, v2_script_pubkey(keys[1][3])),
]
tx = Tx(
vin=[TxIn(test_prevout_txid(i + 3), 1) for i in range(2)],
vout=[TxOut(119_000_000, v2_script_pubkey(keys[0][3]))],
)
msgs, secnonces, pubnonces, sig64 = sign_fullagg_group(
tx, utxos, [(0, keys[0][2], SIGHASH_ALL), (1, keys[1][2], SIGHASH_ALL)]
)
tx.witnesses[0] = [fullagg_member(SIGHASH_ALL)]
tx.witnesses[1] = [marker_element(MARKER_FULLAGG, SIGHASH_ALL, sig64)]
case2 = {
"description": "Two-member full-aggregation group with SIGHASH_ALL "
"(using deterministic nonces)",
"given": {
"rawUnsignedTx": hexlify(tx.serialize_unsigned()),
"utxosSpent": [
{"scriptPubKey": hexlify(u.script_pubkey), "amountSats": u.amount}
for u in utxos
],
},
"inputSpending": [],
"auxiliary": {
"secnonces": [
hexlify(r1.to_bytes() + r2.to_bytes()) for r1, r2 in secnonces
],
"pubnonces": [
hexlify(R1.to_bytes_compressed() + R2.to_bytes_compressed())
for R1, R2 in pubnonces
],
"aggregateSignature": hexlify(sig64),
},
"expected": {"rawSignedTx": hexlify(tx.serialize_signed())},
}
for idx in range(2):
case2["inputSpending"].append(
{
"given": {
"txinIndex": idx,
"internalPrivkey": hexlify(test_seckey(idx + 3)),
"aggMode": f"0x{MARKER_FULLAGG:02x}",
"hashType": SIGHASH_ALL,
},
"intermediary": {
"internalPubkey": hexlify(keys[idx][0]),
"tweak": hexlify(keys[idx][1]),
"tweakedPrivkey": hexlify(keys[idx][2]),
"sigHash": hexlify(msgs[idx]),
},
"expected": {"witness": [hexlify(e) for e in tx.witnesses[idx]]},
}
)
vectors["keyPathSpending"].append(case2)
# Case 3: script path spend of a witness v2 output (BIP 341/342 rules
# unchanged). Same leaf and keys as the consensus scriptpath-valid
# case, so the two files cross-check.
script_seckey = test_seckey(20)
script_pk = (Scalar.from_bytes_checked(script_seckey) * G).to_bytes_xonly()
script = bytes([0x20]) + script_pk + bytes([0xAC]) # <pk> OP_CHECKSIG
tapleaf_hash = tagged_hash(
"TapLeaf", bytes([0xC0]) + ser_compact_size(len(script)) + script
)
internal_seckey = test_seckey(21)
internal_x = (Scalar.from_bytes_checked(internal_seckey) * G).to_bytes_xonly()
t = Scalar.from_bytes_checked(tagged_hash("TapTweak", internal_x + tapleaf_hash))
Q = GE.from_bytes_xonly(internal_x) + t * G
control = bytes([0xC0 | (0 if Q.has_even_y() else 1)]) + internal_x
spk = v2_script_pubkey(Q.to_bytes_xonly())
utxos = [TxOut(100_000_000, spk)]
dest_key = tweak_keypair(test_seckey(0))[3]
tx = Tx(
vin=[TxIn(test_prevout_txid(33), 0)],
vout=[TxOut(99_000_000, v2_script_pubkey(dest_key))],
)
m_script = sigmsg_tapscript(tx, utxos, 0, SIGHASH_DEFAULT, tapleaf_hash)
sig_script = schnorr_sign(m_script, script_seckey, AUX_ZERO)
assert schnorr_verify(m_script, script_pk, sig_script)
tx.witnesses[0] = [sig_script, script, control]
vectors["scriptPathSpending"] = [
{
"description": "Script path spend of a witness v2 output with a "
"single CHECKSIG leaf, following BIP 341/342 unchanged",
"given": {
"rawUnsignedTx": hexlify(tx.serialize_unsigned()),
"utxosSpent": [
{"scriptPubKey": hexlify(u.script_pubkey),
"amountSats": u.amount}
for u in utxos
],
"internalPubkey": hexlify(internal_x),
"script": hexlify(script),
"leafVersion": "0xc0",
"hashType": SIGHASH_DEFAULT,
},
"intermediary": {
"leafHash": hexlify(tapleaf_hash),
"tweak": hexlify(t.to_bytes()),
"tweakedPubkey": hexlify(Q.to_bytes_xonly()),
"controlBlock": hexlify(control),
"sigHash": hexlify(m_script),
},
"expected": {
"scriptPubKey": hexlify(spk),
"address": v2_address(Q.to_bytes_xonly()),
"witness": [hexlify(e) for e in tx.witnesses[0]],
"rawSignedTx": hexlify(tx.serialize_signed()),
},
}
]
return vectors
def make_consensus_vectors():
cases = []
def add_case(case_id, description, tx, utxos, valid, failure_reason=None):
case = {
"id": case_id,
"description": description,
"prevouts": [
{"scriptPubKey": hexlify(u.script_pubkey), "amountSats": u.amount}
for u in utxos
],
"tx": hexlify(tx.serialize_signed()),
"valid": valid,
}
if failure_reason is not None:
case["failureReason"] = failure_reason
cases.append(case)
def fresh_setup(num_inputs, key_offset=0):
keys = [tweak_keypair(test_seckey(i + key_offset)) for i in range(num_inputs)]
utxos = [
TxOut(100_000_000, v2_script_pubkey(k[3])) for k in keys
]
tx = Tx(
vin=[TxIn(test_prevout_txid(i + key_offset), 0) for i in range(num_inputs)],
vout=[TxOut(num_inputs * 100_000_000 - 1_000_000,
v2_script_pubkey(keys[0][3]))],
)
return keys, utxos, tx
# 1: valid two-member half-aggregation group with explicit sighash
# forms
keys, utxos, tx = fresh_setup(2)
_, sigs1, aggsig = sign_halfagg_group(
tx, utxos, [(0, keys[0][2], SIGHASH_ALL), (1, keys[1][2], SIGHASH_ALL)]
)
tx.witnesses[0] = [halfagg_member(sigs1[0][:32], SIGHASH_ALL)]
tx.witnesses[1] = [halfagg_final(aggsig, SIGHASH_ALL)]
add_case("halfagg-valid", "Two-member half-aggregation group, SIGHASH_ALL",
tx, utxos, True)
# 2: valid two-member full-aggregation group
keys, utxos, tx = fresh_setup(2, key_offset=3)
_, _, _, sig64 = sign_fullagg_group(
tx, utxos, [(0, keys[0][2], SIGHASH_ALL), (1, keys[1][2], SIGHASH_ALL)]
)
tx.witnesses[0] = [fullagg_member(SIGHASH_ALL)]
tx.witnesses[1] = [marker_element(MARKER_FULLAGG, SIGHASH_ALL, sig64)]
add_case("fullagg-valid", "Two-member full-aggregation group, SIGHASH_ALL",
tx, utxos, True)
fullagg_base = (keys, utxos, tx, sig64)
# 3: valid mix of opted-out input and half-aggregation group
keys, utxos, tx = fresh_setup(3, key_offset=6)
_, sig0 = sign_optout(tx, utxos, 0, keys[0][2], SIGHASH_DEFAULT)
_, sigs3, aggsig3 = sign_halfagg_group(
tx, utxos, [(1, keys[1][2], SIGHASH_DEFAULT), (2, keys[2][2], SIGHASH_DEFAULT)]
)
tx.witnesses[0] = [optout_element(sig0)]
tx.witnesses[1] = [halfagg_member(sigs3[0][:32])]
tx.witnesses[2] = [halfagg_final(aggsig3)]
add_case("mixed-valid",
"Opted-out input alongside a two-member half-aggregation group",
tx, utxos, True)
# 4: invalid, undefined sighash value 0xbb. The signature covers the
# message that hashes 0xbb, so only the sighash validation fails.
keys, utxos, tx = fresh_setup(3, key_offset=6)
_, sig0_4 = sign_optout(tx, utxos, 0, keys[0][2], 0xBB)
tx.witnesses[0] = [sig0_4 + bytes([0xBB])]
tx.witnesses[1] = [halfagg_member(sigs3[0][:32])]
tx.witnesses[2] = [halfagg_final(aggsig3)]
add_case("optout-sighash-undefined", "65-byte opted-out signature ending in "
"the undefined sighash value 0xbb", tx, utxos, False,
"undefined sighash type")
# 5: invalid, explicit 0x00 sighash byte on a group member
keys, utxos, tx = fresh_setup(3, key_offset=6)
tx.witnesses[0] = [optout_element(sig0)]
tx.witnesses[1] = [sigs3[0][:32] + bytes([0x00])]
tx.witnesses[2] = [halfagg_final(aggsig3)]
add_case("member-explicit-default", "Half-aggregation member encodes "
"SIGHASH_DEFAULT explicitly as a 0x00 sighash byte",
tx, utxos, False, "explicit 0x00 sighash byte")
# 6: invalid, aggregation group without a final input
keys, utxos, tx = fresh_setup(2)
tx.witnesses[0] = [halfagg_member(sigs1[0][:32])]
tx.witnesses[1] = [halfagg_member(sigs1[1][:32])]
add_case("group-no-final", "Half-aggregation members without an input "
"carrying the marker and the s value", tx, utxos, False,
"aggregation group has no final input")
# 7: invalid, member after the group final
keys, utxos, tx = fresh_setup(3, key_offset=9)
_, sigs7, aggsig7 = sign_halfagg_group(
tx, utxos, [(0, keys[0][2], SIGHASH_DEFAULT), (1, keys[1][2], SIGHASH_DEFAULT)]
)
tx.witnesses[0] = [halfagg_member(sigs7[0][:32])]
tx.witnesses[1] = [halfagg_final(aggsig7)]
tx.witnesses[2] = [halfagg_member(sigs7[1][:32])]
add_case("member-after-final", "Half-aggregation member appears after "
"the group's final input", tx, utxos, False,
"member after group final")
# 8: invalid, 2-byte element. No defined form has two bytes.
keys, utxos, tx = fresh_setup(2)
_, sig1_8 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
tx.witnesses[0] = [bytes([SIGHASH_ALL, MARKER_HALFAGG])]
tx.witnesses[1] = [optout_element(sig1_8)]
add_case("element-length-undefined", "2-byte witness element ending in "
"a marker byte, matching no defined structure", tx, utxos,
False, "marker and length match no defined structure")
# 9: invalid, cross-mode fold (mode commitment)
# Opted-out signatures over the plain BIP 341 message are packaged
# as a half-aggregation group. The group structure is well formed
# but aggregate verification fails because the group messages commit
# to the aggregation mode.
keys, utxos, tx = fresh_setup(2, key_offset=12)
triples = []
for idx in range(2):
m_optout = sigmsg_v1(tx, utxos, idx, SIGHASH_DEFAULT)
sig = schnorr_sign(m_optout, keys[idx][2], AUX_ZERO)
pk = utxos[idx].script_pubkey[2:]
triples.append((pk, m_optout, sig))
aggsig9 = halfagg.Aggregate(triples)
consensus_msgs = [
(utxos[i].script_pubkey[2:],
sigmsg_v2(tx, utxos, i, SIGHASH_DEFAULT, MARKER_HALFAGG))
for i in range(2)
]
assert not halfagg.VerifyAggregate(aggsig9, consensus_msgs)
tx.witnesses[0] = [halfagg_member(triples[0][2][:32])]
tx.witnesses[1] = [halfagg_final(aggsig9)]
add_case("cross-mode-fold", "Opted-out signatures folded into a "
"half-aggregation group by a third party", tx, utxos, False,
"aggregate signature invalid, messages commit to the "
"aggregation mode")
# 10: invalid, empty witness
keys, utxos, tx = fresh_setup(2)
_, sig1 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
tx.witnesses[0] = []
tx.witnesses[1] = [optout_element(sig1)]
add_case("empty-witness", "Witness v2 input with an empty witness",
tx, utxos, False, "empty witness")
# 11: invalid, explicit 0x00 sighash byte on an opted-out signature.
# Complements case 5, which covers the marked form.
keys, utxos, tx = fresh_setup(2)
_, sig0 = sign_optout(tx, utxos, 0, keys[0][2], SIGHASH_DEFAULT)
_, sig1 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
tx.witnesses[0] = [sig0 + bytes([0x00])]
tx.witnesses[1] = [optout_element(sig1)]
add_case("optout-explicit-default", "Opted-out input encodes "
"SIGHASH_DEFAULT explicitly as a 0x00 sighash byte",
tx, utxos, False, "explicit 0x00 sighash byte")
# 12: valid, opted-out 64-byte signature ending in the marker byte
# 0xbc, ground via the auxiliary randomness
keys, utxos, tx = fresh_setup(2)
m0 = sigmsg_v1(tx, utxos, 0, SIGHASH_DEFAULT)
aux_i = 0
while True:
aux = tagged_hash("CISA/test/aux", aux_i.to_bytes(2, "little"))
sig0 = schnorr_sign(m0, keys[0][2], aux)
if sig0[63] == MARKER_HALFAGG:
break
aux_i += 1
_, sig1 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
tx.witnesses[0] = [optout_element(sig0)]
tx.witnesses[1] = [optout_element(sig1)]
add_case("optout-sig-ends-in-marker", "Opted-out 64-byte signature "
"whose last byte equals the half-aggregation marker 0xbc",
tx, utxos, True)
# 13: valid, half-aggregation group member carrying a signed annex
keys, utxos, tx = fresh_setup(2, key_offset=15)
annex = bytes([0x50]) + b"CISA annex test"
m0 = sigmsg_v2(tx, utxos, 0, SIGHASH_DEFAULT, MARKER_HALFAGG, annex=annex)
m1 = sigmsg_v2(tx, utxos, 1, SIGHASH_DEFAULT, MARKER_HALFAGG)
pk0 = utxos[0].script_pubkey[2:]
pk1 = utxos[1].script_pubkey[2:]
sig0 = schnorr_sign(m0, keys[0][2], AUX_ZERO)
sig1 = schnorr_sign(m1, keys[1][2], AUX_ZERO)
aggsig_annex = halfagg.Aggregate([(pk0, m0, sig0), (pk1, m1, sig1)])
assert halfagg.VerifyAggregate(aggsig_annex, [(pk0, m0), (pk1, m1)])
tx.witnesses[0] = [halfagg_member(sig0[:32]), annex]
tx.witnesses[1] = [halfagg_final(aggsig_annex)]
add_case("annex-committed-valid", "Half-aggregation group whose first "
"member carries an annex that is committed in its signature "
"message", tx, utxos, True)
# 14: invalid, annex stripped from the transaction of case 13
keys, utxos, tx = fresh_setup(2, key_offset=15)
m0_stripped = sigmsg_v2(tx, utxos, 0, SIGHASH_DEFAULT, MARKER_HALFAGG)
assert not halfagg.VerifyAggregate(
aggsig_annex, [(pk0, m0_stripped), (pk1, m1)]
)
tx.witnesses[0] = [halfagg_member(sig0[:32])]
tx.witnesses[1] = [halfagg_final(aggsig_annex)]
add_case("annex-stripped", "Annex removed by a third party from the "
"transaction of annex-committed-valid", tx, utxos, False,
"signature message committed to the annex")
# 15: valid, witness v2 script path spend (BIP 341/342 rules unchanged)
script_seckey = test_seckey(20)
script_pk = (Scalar.from_bytes_checked(script_seckey) * G).to_bytes_xonly()
script = bytes([0x20]) + script_pk + bytes([0xAC]) # <pk> OP_CHECKSIG
tapleaf_hash = tagged_hash(
"TapLeaf", bytes([0xC0]) + ser_compact_size(len(script)) + script
)
internal_seckey = test_seckey(21)
d0 = Scalar.from_bytes_checked(internal_seckey)
P_int = d0 * G
internal_x = P_int.to_bytes_xonly()
t = Scalar.from_bytes_checked(tagged_hash("TapTweak", internal_x + tapleaf_hash))
Q = GE.from_bytes_xonly(internal_x) + t * G
control = bytes([0xC0 | (0 if Q.has_even_y() else 1)]) + internal_x
keys, utxos, tx = fresh_setup(2, key_offset=18)
utxos[0] = TxOut(100_000_000, v2_script_pubkey(Q.to_bytes_xonly()))
m_script = sigmsg_tapscript(tx, utxos, 0, SIGHASH_DEFAULT, tapleaf_hash)
sig_script = schnorr_sign(m_script, script_seckey, AUX_ZERO)
assert schnorr_verify(m_script, script_pk, sig_script)
_, sig1 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
tx.witnesses[0] = [sig_script, script, control]
tx.witnesses[1] = [optout_element(sig1)]
add_case("scriptpath-valid", "Witness v2 script path spend of a single "
"CHECKSIG leaf under BIP 341/342 rules, alongside an opted-out "
"key path input", tx, utxos, True)
# 16: invalid, cross-mode messages in a full-aggregation group
# The signers run a full-aggregation session over plain BIP 341
# messages and the result is placed in a 0xbd witness. Unlike case 9
# there is no public fold operation for full-agg, this checks that
# verifiers derive the messages with the 0xbd marker on this path too.
keys, utxos, tx = fresh_setup(2, key_offset=22)
_, _, _, sig64_16 = sign_fullagg_group(
tx, utxos,
[(0, keys[0][2], SIGHASH_DEFAULT), (1, keys[1][2], SIGHASH_DEFAULT)],
msg_mode=None,
)
pks16 = [GE.from_bytes_xonly(u.script_pubkey[2:]) for u in utxos]
consensus_msgs16 = [
sigmsg_v2(tx, utxos, i, SIGHASH_DEFAULT, MARKER_FULLAGG) for i in range(2)
]
R16 = GE.from_bytes_xonly(sig64_16[:32])
s16 = Scalar.from_bytes_checked(sig64_16[32:])
assert not fullagg.Verify(pks16, consensus_msgs16, (R16, s16))
tx.witnesses[0] = [fullagg_member()]
tx.witnesses[1] = [marker_element(MARKER_FULLAGG, sig=sig64_16)]
add_case("cross-mode-fullagg", "Full-aggregation signature created over "
"plain BIP 341 messages, placed in a full-aggregation group",
tx, utxos, False,
"aggregate signature invalid, messages commit to the "
"aggregation mode")
# 17: invalid, 97-byte 0xbd element. Marker 0xbd admits lengths 65
# and 66 only, so parsing fails in pass 1 regardless of the content.
# fresh_setup(2, key_offset=3) recreates the transaction from case 2
# deterministically, so the aggregate signature from that case
# corresponds to this tx.
_, _, _, sig64 = fullagg_base
keys, utxos, tx = fresh_setup(2, key_offset=3)
tx.witnesses[0] = [fullagg_member(SIGHASH_ALL)]
tx.witnesses[1] = [marker_element(MARKER_FULLAGG, sig=sig64 + bytes(32))]
add_case("fullagg-size-mismatch", "Full-aggregation final input carries "
"96 bytes before the marker (97-byte 0xbd element, matches no "
"defined witness structure)", tx, utxos, False,
"marker and length match no defined structure")
# 18: valid, mixed transaction mirroring Example 4 of the BIP: two
# opted-out inputs with explicit sighash types, a half-aggregation
# group and a full-aggregation group with interleaved indices. Also
# covers a valid SIGHASH_SINGLE member with a corresponding output.
keys, utxos, tx = fresh_setup(7, key_offset=24)
tx.vout = [
TxOut(350_000_000, v2_script_pubkey(keys[0][3])),
TxOut(349_000_000, v2_script_pubkey(keys[1][3])),
]
_, sig0 = sign_optout(tx, utxos, 0, keys[0][2], SIGHASH_DEFAULT)
_, sig5 = sign_optout(tx, utxos, 5, keys[5][2],
SIGHASH_NONE | SIGHASH_ANYONECANPAY)
_, sigs18, aggsig18 = sign_halfagg_group(
tx, utxos,
[(1, keys[1][2], SIGHASH_SINGLE), (4, keys[4][2], SIGHASH_DEFAULT)],
)
_, _, _, sig64_18 = sign_fullagg_group(
tx, utxos,
[(2, keys[2][2], SIGHASH_DEFAULT), (3, keys[3][2], SIGHASH_DEFAULT),
(6, keys[6][2], SIGHASH_DEFAULT)],
)
tx.witnesses[0] = [optout_element(sig0)]
tx.witnesses[1] = [halfagg_member(sigs18[0][:32], SIGHASH_SINGLE)]
tx.witnesses[2] = [fullagg_member()]
tx.witnesses[3] = [fullagg_member()]
tx.witnesses[4] = [halfagg_final(aggsig18)]
tx.witnesses[5] = [optout_element(sig5,
SIGHASH_NONE | SIGHASH_ANYONECANPAY)]
tx.witnesses[6] = [marker_element(MARKER_FULLAGG, sig=sig64_18)]
add_case("two-groups-valid", "Mixed transaction mirroring Example 4 of "
"the BIP: two opted-out inputs, a half-aggregation group with "
"a SIGHASH_SINGLE member, and a full-aggregation group, with "
"interleaved input indices", tx, utxos, True)
# 19: invalid, SIGHASH_SINGLE without a corresponding output. Input 1
# signals SIGHASH_SINGLE but the transaction has only one output, so
# computing SigMsg fails per the BIP 341 failure conditions, which are
# incorporated by reference. The witness is structurally well formed
# and carries a syntactically valid signature (created over the
# SIGHASH_ALL flavored message), so the only reason for rejection is
# the missing corresponding output.
keys, utxos, tx = fresh_setup(2, key_offset=31)
_, sig0 = sign_optout(tx, utxos, 0, keys[0][2], SIGHASH_DEFAULT)
m_all = sigmsg_v1(tx, utxos, 1, SIGHASH_ALL)
sig1_19 = schnorr_sign(m_all, keys[1][2], AUX_ZERO)
tx.witnesses[0] = [optout_element(sig0)]
tx.witnesses[1] = [optout_element(sig1_19, SIGHASH_SINGLE)]
add_case("sighash-single-no-output", "Opted-out input uses "
"SIGHASH_SINGLE at input index 1 of a single-output "
"transaction", tx, utxos, False,
"SIGHASH_SINGLE input without corresponding output")
# 20: invalid, two final inputs in one half-aggregation group. Each
# element carries a valid single-member final, so the failure is
# purely structural. Distinct from case 7, where the element after
# the final is a member.
keys, utxos, tx = fresh_setup(2, key_offset=34)
_, _, agg_a = sign_halfagg_group(tx, utxos, [(0, keys[0][2], SIGHASH_DEFAULT)])
_, _, agg_b = sign_halfagg_group(tx, utxos, [(1, keys[1][2], SIGHASH_DEFAULT)])
tx.witnesses[0] = [halfagg_final(agg_a)]
tx.witnesses[1] = [halfagg_final(agg_b)]
add_case("halfagg-two-finals", "Two inputs each carrying a 65-byte "
"half-aggregation final element", tx, utxos, False,
"more than one final input in a group")
# 21: invalid, two final inputs in one full-aggregation group
keys, utxos, tx = fresh_setup(2, key_offset=36)
_, _, _, sig_a = sign_fullagg_group(
tx, utxos, [(0, keys[0][2], SIGHASH_DEFAULT)], nonce_offset=10)
_, _, _, sig_b = sign_fullagg_group(
tx, utxos, [(1, keys[1][2], SIGHASH_DEFAULT)], nonce_offset=11)
tx.witnesses[0] = [marker_element(MARKER_FULLAGG, sig=sig_a)]
tx.witnesses[1] = [marker_element(MARKER_FULLAGG, sig=sig_b)]
add_case("fullagg-two-finals", "Two inputs each carrying a 65-byte "
"full-aggregation final element", tx, utxos, False,
"more than one final input in a group")
# 22: invalid, full-aggregation members without a final input
# (full-agg analog of case 6)
keys, utxos, tx = fresh_setup(2, key_offset=38)
tx.witnesses[0] = [fullagg_member()]
tx.witnesses[1] = [fullagg_member()]
add_case("fullagg-no-final", "Full-aggregation members without an input "
"carrying the marker and the aggregate signature", tx, utxos,
False, "aggregation group has no final input")
# 23: invalid, full-agg member after the final
# (full-agg analog of case 7)
keys, utxos, tx = fresh_setup(3, key_offset=40)
_, _, _, sig64_23 = sign_fullagg_group(
tx, utxos,
[(0, keys[0][2], SIGHASH_DEFAULT), (1, keys[1][2], SIGHASH_DEFAULT)],
nonce_offset=12)
tx.witnesses[0] = [fullagg_member()]
tx.witnesses[1] = [marker_element(MARKER_FULLAGG, sig=sig64_23)]
tx.witnesses[2] = [fullagg_member()]
add_case("fullagg-member-after-final", "Full-aggregation member appears "
"after the group's final input", tx, utxos, False,
"member after group final")
# 24: valid, single-member half-aggregation group. The 65-byte
# element matches an explicit sighash opted-out signature in length
# and is distinguished by its 0xbc last byte.
keys, utxos, tx = fresh_setup(1, key_offset=43)
_, _, aggsig24 = sign_halfagg_group(
tx, utxos, [(0, keys[0][2], SIGHASH_DEFAULT)])
tx.witnesses[0] = [halfagg_final(aggsig24)]
add_case("halfagg-single-member", "Half-aggregation group with a single "
"member carrying a 64-byte aggregate in a 65-byte element",
tx, utxos, True)
# 25: valid, single-member full-aggregation group
keys, utxos, tx = fresh_setup(1, key_offset=44)
_, _, _, sig64_25 = sign_fullagg_group(
tx, utxos, [(0, keys[0][2], SIGHASH_DEFAULT)], nonce_offset=13)
tx.witnesses[0] = [marker_element(MARKER_FULLAGG, sig=sig64_25)]
add_case("fullagg-single-member", "Full-aggregation group with a single "
"member", tx, utxos, True)
# 26: invalid, single witness element starting with the annex prefix.
# An annex requires at least two witness elements, so this parses as
# a key path element that matches no defined structure.
keys, utxos, tx = fresh_setup(2, key_offset=45)
_, sig1_26 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
tx.witnesses[0] = [bytes([0x50]) + b"CISA annex test"]
tx.witnesses[1] = [optout_element(sig1_26)]
add_case("annex-lookalike", "Single witness element starting with the "
"annex prefix 0x50", tx, utxos, False,
"matches no defined structure, a single element is never "
"an annex")
# 27: valid, annex on the final input of a group. The annex must be
# stripped before the final element is interpreted. Complements case
# 13, which has the annex on a member input.
keys, utxos, tx = fresh_setup(2, key_offset=47)
annex27 = bytes([0x50]) + b"CISA annex test"
m0_27 = sigmsg_v2(tx, utxos, 0, SIGHASH_DEFAULT, MARKER_HALFAGG)
m1_27 = sigmsg_v2(tx, utxos, 1, SIGHASH_DEFAULT, MARKER_HALFAGG,
annex=annex27)
pk0 = utxos[0].script_pubkey[2:]
pk1 = utxos[1].script_pubkey[2:]
sig0_27 = schnorr_sign(m0_27, keys[0][2], AUX_ZERO)
sig1_27 = schnorr_sign(m1_27, keys[1][2], AUX_ZERO)
agg27 = halfagg.Aggregate([(pk0, m0_27, sig0_27), (pk1, m1_27, sig1_27)])
assert halfagg.VerifyAggregate(agg27, [(pk0, m0_27), (pk1, m1_27)])
tx.witnesses[0] = [halfagg_member(sig0_27[:32])]
tx.witnesses[1] = [halfagg_final(agg27), annex27]
add_case("annex-on-final", "Half-aggregation group whose final input "
"carries an annex that is committed in its signature message",
tx, utxos, True)
# 28: invalid, undefined sighash value. Cases 5 and 11 cover the
# explicit 0x00 encoding rule, this covers a value outside the
# defined set. The signature covers the message that hashes 0x04,
# so only the sighash validation fails.
keys, utxos, tx = fresh_setup(2, key_offset=49)
m0_28 = sigmsg_v1(tx, utxos, 0, 0x04)
sig0_28 = schnorr_sign(m0_28, keys[0][2], AUX_ZERO)
_, sig1_28 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
tx.witnesses[0] = [sig0_28 + bytes([0x04])]
tx.witnesses[1] = [optout_element(sig1_28)]
add_case("sighash-value-undefined", "Opted-out input with the undefined "
"sighash value 0x04", tx, utxos, False,
"undefined sighash type")
# 29: invalid, 33-byte element ending in 0xbc. It parses as a
# half-aggregation member whose sighash byte is the marker value,
# which is not a valid sighash type.
keys, utxos, tx = fresh_setup(2, key_offset=51)
_, sig0_29 = sign_optout(tx, utxos, 0, keys[0][2], SIGHASH_DEFAULT)
_, _, agg29 = sign_halfagg_group(
tx, utxos, [(1, keys[1][2], SIGHASH_DEFAULT)])
tx.witnesses[0] = [optout_element(sig0_29)]
tx.witnesses[1] = [agg29[:32] + bytes([MARKER_HALFAGG])]
add_case("member-sighash-is-marker", "33-byte element ending in the "
"marker 0xbc, a half-aggregation member with an invalid "
"sighash byte", tx, utxos, False, "undefined sighash type")
# 30: valid, witness v2 output whose witness program is not 32 bytes.
# Such outputs remain unencumbered by the BIP, so the witness content
# is unconstrained, even an undefined marker byte.
keys, utxos, tx = fresh_setup(2, key_offset=53)
program33 = test_garbage(b"33-byte program", 33)
utxos[0] = TxOut(100_000_000, bytes([0x52, 0x21]) + program33)
_, sig1_30 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
tx.witnesses[0] = [bytes([0xBE])]
tx.witnesses[1] = [optout_element(sig1_30)]
add_case("program-not-32-bytes", "Witness v2 output with a 33-byte "
"witness program spent with an arbitrary witness", tx, utxos,
True)
# 31: invalid, opted-out witness program is not a valid x coordinate.
# BIP 340 verification fails at lift_x.
keys, utxos, tx = fresh_setup(2, key_offset=55)
utxos[0] = TxOut(100_000_000,
bytes([0x52, 0x20]) + test_offcurve_x(b"optout"))
_, sig1_31 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
tx.witnesses[0] = [optout_element(test_garbage(b"offcurve sig", 64))]
tx.witnesses[1] = [optout_element(sig1_31)]
add_case("program-offcurve-optout", "Opted-out input whose witness "
"program is not a valid x-only public key", tx, utxos, False,
"witness program is not a valid x-only public key")
# 32: invalid, group member witness program is not a valid x
# coordinate. VerifyAggregate fails at lift_x of the first pubkey, so
# the aggregate content is irrelevant.
keys, utxos, tx = fresh_setup(2, key_offset=57)
utxos[0] = TxOut(100_000_000,
bytes([0x52, 0x20]) + test_offcurve_x(b"group"))
m1_32 = sigmsg_v2(tx, utxos, 1, SIGHASH_DEFAULT, MARKER_HALFAGG)
sig1_32 = schnorr_sign(m1_32, keys[1][2], AUX_ZERO)
agg32 = test_garbage(b"offcurve r0", 32) + sig1_32
assert not halfagg.VerifyAggregate(agg32, [
(utxos[0].script_pubkey[2:],
sigmsg_v2(tx, utxos, 0, SIGHASH_DEFAULT, MARKER_HALFAGG)),
(utxos[1].script_pubkey[2:], m1_32),
])
tx.witnesses[0] = [halfagg_member(agg32[:32])]
tx.witnesses[1] = [halfagg_final(agg32)]
add_case("program-offcurve-in-group", "Half-aggregation group member "
"whose witness program is not a valid x-only public key",
tx, utxos, False,
"witness program is not a valid x-only public key")
# 33: valid, script path spend with an unknown leaf version (0xc2).
# Unknown leaf versions succeed unconditionally under BIP 341,
# preserving the upgrade hook in witness v2.
keys, utxos, tx = fresh_setup(2, key_offset=59)
internal_seckey33 = test_seckey(200)
internal_x33 = (Scalar.from_bytes_checked(internal_seckey33) * G).to_bytes_xonly()
script33 = bytes([0x51])
leaf33 = tagged_hash(
"TapLeaf", bytes([0xC2]) + ser_compact_size(len(script33)) + script33)
t33 = Scalar.from_bytes_checked(tagged_hash("TapTweak", internal_x33 + leaf33))
Q33 = GE.from_bytes_xonly(internal_x33) + t33 * G
control33 = bytes([0xC2 | (0 if Q33.has_even_y() else 1)]) + internal_x33
utxos[0] = TxOut(100_000_000, v2_script_pubkey(Q33.to_bytes_xonly()))
_, sig1_33 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
tx.witnesses[0] = [script33, control33]
tx.witnesses[1] = [optout_element(sig1_33)]
add_case("scriptpath-unknown-leaf-version", "Witness v2 script path "
"spend with unknown leaf version 0xc2, which succeeds "
"unconditionally per BIP 341", tx, utxos, True)
# 34: valid, script path leaf containing an OP_SUCCESS opcode. The
# script byte 0xbc is OP_SUCCESS188 in tapscript and has no marker
# meaning inside a script.
keys, utxos, tx = fresh_setup(2, key_offset=61)
internal_seckey34 = test_seckey(201)
internal_x34 = (Scalar.from_bytes_checked(internal_seckey34) * G).to_bytes_xonly()
script34 = bytes([MARKER_HALFAGG])
leaf34 = tagged_hash(
"TapLeaf", bytes([0xC0]) + ser_compact_size(len(script34)) + script34)
t34 = Scalar.from_bytes_checked(tagged_hash("TapTweak", internal_x34 + leaf34))
Q34 = GE.from_bytes_xonly(internal_x34) + t34 * G
control34 = bytes([0xC0 | (0 if Q34.has_even_y() else 1)]) + internal_x34
utxos[0] = TxOut(100_000_000, v2_script_pubkey(Q34.to_bytes_xonly()))
_, sig1_34 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
tx.witnesses[0] = [script34, control34]
tx.witnesses[1] = [optout_element(sig1_34)]
add_case("scriptpath-op-success", "Witness v2 script path spend whose "
"leaf script is OP_SUCCESS188 (byte 0xbc), which succeeds "
"unconditionally per BIP 342", tx, utxos, True)
# 35: valid, mixed witness v1 and v2 transaction mirroring Example 3
# of the BIP. Input 0 spends a taproot output with an unchanged
# BIP 341 key path signature (epoch 0x00, no marker).
keys, utxos, tx = fresh_setup(3, key_offset=63)
utxos[0] = TxOut(100_000_000, bytes([0x51, 0x20]) + keys[0][3])
m0_35 = sigmsg_v1(tx, utxos, 0, SIGHASH_DEFAULT)
sig0_35 = schnorr_sign(m0_35, keys[0][2], AUX_ZERO)
_, sigs35, agg35 = sign_halfagg_group(
tx, utxos,
[(1, keys[1][2], SIGHASH_DEFAULT), (2, keys[2][2], SIGHASH_DEFAULT)])
tx.witnesses[0] = [sig0_35]
tx.witnesses[1] = [halfagg_member(sigs35[0][:32])]
tx.witnesses[2] = [halfagg_final(agg35)]
add_case("v1-v2-mixed", "Witness v1 key path input alongside a "
"two-member witness v2 half-aggregation group, mirroring "
"Example 3 of the BIP", tx, utxos, True)
# 36: invalid, explicit sighash byte stripped by a third party. Input
# 0 signed with explicit SIGHASH_ALL, the witness is malleated to the
# 32-byte form, so the verifier derives the SIGHASH_DEFAULT flavored
# message. Complements case 14, which strips the annex.
keys, utxos, tx = fresh_setup(2, key_offset=66)
msgs36, sigs36, agg36 = sign_halfagg_group(
tx, utxos,
[(0, keys[0][2], SIGHASH_ALL), (1, keys[1][2], SIGHASH_DEFAULT)])
m0_default = sigmsg_v2(tx, utxos, 0, SIGHASH_DEFAULT, MARKER_HALFAGG)
assert not halfagg.VerifyAggregate(agg36, [
(utxos[0].script_pubkey[2:], m0_default),
(utxos[1].script_pubkey[2:], msgs36[1]),
])
tx.witnesses[0] = [halfagg_member(sigs36[0][:32])]
tx.witnesses[1] = [halfagg_final(agg36)]
add_case("sighash-byte-stripped", "Explicit SIGHASH_ALL byte removed "
"from a group member by a third party", tx, utxos, False,
"signature message committed to the sighash type")
# 37: invalid, single empty witness element. It parses as a
# full-aggregation member and the group has no final input. Distinct
# from case 10, where the witness stack itself is empty.
keys, utxos, tx = fresh_setup(2, key_offset=68)
_, sig1_37 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
tx.witnesses[0] = [b""]
tx.witnesses[1] = [optout_element(sig1_37)]
add_case("empty-element-no-final", "Witness v2 input whose only witness "
"element is empty, a full-aggregation member without a final "
"input", tx, utxos, False,
"aggregation group has no final input")
# 38: invalid, SIGHASH_SINGLE group member without a corresponding
# output. Complements case 19, which covers the opted-out path. The
# witness is structurally well formed (66-byte final element) and
# the signature bytes are created over the SIGHASH_ALL flavored
# message, so the only reason for rejection is the missing output.
keys, utxos, tx = fresh_setup(2, key_offset=70)
pk0 = utxos[0].script_pubkey[2:]
pk1 = utxos[1].script_pubkey[2:]
m0_38 = sigmsg_v2(tx, utxos, 0, SIGHASH_DEFAULT, MARKER_HALFAGG)
m1_38 = sigmsg_v2(tx, utxos, 1, SIGHASH_ALL, MARKER_HALFAGG)
sig0_38 = schnorr_sign(m0_38, keys[0][2], AUX_ZERO)
sig1_38 = schnorr_sign(m1_38, keys[1][2], AUX_ZERO)
agg38 = halfagg.Aggregate([(pk0, m0_38, sig0_38), (pk1, m1_38, sig1_38)])
tx.witnesses[0] = [halfagg_member(sig0_38[:32])]
tx.witnesses[1] = [halfagg_final(agg38, SIGHASH_SINGLE)]
add_case("sighash-single-no-output-group", "Half-aggregation group "
"member uses SIGHASH_SINGLE at input index 1 of a "
"single-output transaction", tx, utxos, False,
"SIGHASH_SINGLE input without corresponding output")
# 39: invalid, two witness elements where the last is not an annex.
# With two elements and no annex prefix this is a script path spend,
# and the 1-byte last element is not a valid control block.
keys, utxos, tx = fresh_setup(2, key_offset=72)
m0_39 = sigmsg_v1(tx, utxos, 0, SIGHASH_DEFAULT)
sig0_39 = schnorr_sign(m0_39, keys[0][2], AUX_ZERO)
_, sig1_39 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
tx.witnesses[0] = [sig0_39, bytes([0x60])]
tx.witnesses[1] = [optout_element(sig1_39)]
add_case("two-elements-no-annex", "Keypath-looking element followed by "
"a second element that is not an annex, making this a script "
"path spend with an invalid control block", tx, utxos, False,
"invalid control block size")
# 40: invalid, half-aggregation flavored signature placed as a bare
# 64-byte opted-out element
keys, utxos, tx = fresh_setup(2, key_offset=74)
m0_agg = sigmsg_v2(tx, utxos, 0, SIGHASH_DEFAULT, MARKER_HALFAGG)
sig0_40 = schnorr_sign(m0_agg, keys[0][2], AUX_ZERO)
m0_plain = sigmsg_v1(tx, utxos, 0, SIGHASH_DEFAULT)
assert not schnorr_verify(m0_plain, utxos[0].script_pubkey[2:], sig0_40)
_, sig1_40 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
tx.witnesses[0] = [optout_element(sig0_40)]
tx.witnesses[1] = [optout_element(sig1_40)]
add_case("agg-mode-sig-as-optout", "Signature created over the "
"half-aggregation flavored message used as a bare opted-out "
"signature", tx, utxos, False,
"signature invalid, message commits to the aggregation mode")
# 41: valid, 8-member half-aggregation group, exercising the nonce
# share assembly across many inputs
keys, utxos, tx = fresh_setup(8, key_offset=76)
members41 = [(i, keys[i][2], SIGHASH_DEFAULT) for i in range(8)]
_, sigs41, agg41 = sign_halfagg_group(tx, utxos, members41)
for i in range(7):
tx.witnesses[i] = [halfagg_member(sigs41[i][:32])]
tx.witnesses[7] = [halfagg_final(agg41)]
add_case("halfagg-large-group", "8-member half-aggregation group",
tx, utxos, True)
# 42: valid, script path spend whose first stack element is a 1-byte
# 0xbc marker lookalike. With three witness elements this is a script
# path spend, so marker interpretation does not apply. The leaf
# script drops the element and pushes true.
keys, utxos, tx = fresh_setup(2, key_offset=84)
internal_seckey42 = test_seckey(202)
internal_x42 = (Scalar.from_bytes_checked(internal_seckey42) * G).to_bytes_xonly()
script42 = bytes([0x75, 0x51]) # OP_DROP OP_1
leaf42 = tagged_hash(
"TapLeaf", bytes([0xC0]) + ser_compact_size(len(script42)) + script42)
t42 = Scalar.from_bytes_checked(tagged_hash("TapTweak", internal_x42 + leaf42))
Q42 = GE.from_bytes_xonly(internal_x42) + t42 * G
control42 = bytes([0xC0 | (0 if Q42.has_even_y() else 1)]) + internal_x42
utxos[0] = TxOut(100_000_000, v2_script_pubkey(Q42.to_bytes_xonly()))
_, sig1_42 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
tx.witnesses[0] = [bytes([MARKER_HALFAGG]), script42, control42]
tx.witnesses[1] = [optout_element(sig1_42)]
add_case("scriptpath-marker-lookalike", "Script path spend whose first "
"stack element is a 1-byte 0xbc marker lookalike, which has "
"no marker meaning outside key path spends", tx, utxos, True)
# 43: valid, both group members spend outputs with the same witness
# program (address reuse). The messages differ by input index and
# neither scheme deduplicates keys.
keys, utxos, tx = fresh_setup(4, key_offset=86)
utxos[1] = TxOut(100_000_000, v2_script_pubkey(keys[0][3]))
utxos[3] = TxOut(100_000_000, v2_script_pubkey(keys[2][3]))
_, sigs43, agg43 = sign_halfagg_group(
tx, utxos,
[(0, keys[0][2], SIGHASH_DEFAULT), (1, keys[0][2], SIGHASH_DEFAULT)])
_, _, _, sig64_43 = sign_fullagg_group(
tx, utxos,
[(2, keys[2][2], SIGHASH_DEFAULT), (3, keys[2][2], SIGHASH_DEFAULT)],
nonce_offset=20)
tx.witnesses[0] = [halfagg_member(sigs43[0][:32])]
tx.witnesses[1] = [halfagg_final(agg43)]
tx.witnesses[2] = [fullagg_member()]
tx.witnesses[3] = [marker_element(MARKER_FULLAGG, sig=sig64_43)]
add_case("duplicate-key-groups", "Half-aggregation and full-aggregation "
"groups whose two members each spend outputs with the same "
"witness program (address reuse)", tx, utxos, True)
# 44: invalid, ANYONECANPAY signature for the same output key created
# as if spending the v1 scriptPubKey, replayed on the witness v2
# input. Fails because the message commits to the spent scriptPubKey.
keys, utxos, tx = fresh_setup(2, key_offset=90)
utxos_v1 = list(utxos)
utxos_v1[0] = TxOut(utxos[0].amount, bytes([0x51, 0x20]) + keys[0][3])
ht44 = SIGHASH_ALL | SIGHASH_ANYONECANPAY
m0_v1spk = sigmsg_v1(tx, utxos_v1, 0, ht44)
sig0_44 = schnorr_sign(m0_v1spk, keys[0][2], AUX_ZERO)
m0_v2spk = sigmsg_v1(tx, utxos, 0, ht44)
assert not schnorr_verify(m0_v2spk, keys[0][3], sig0_44)
_, sig1_44 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
tx.witnesses[0] = [optout_element(sig0_44, ht44)]
tx.witnesses[1] = [optout_element(sig1_44)]
add_case("v1-replay", "BIP 341 key path signature created for a "
"witness v1 output with the same output key, replayed on a "
"witness v2 input", tx, utxos, False,
"signature message commits to the spent scriptPubKey")
# 45: invalid, undefined sighash value on a 1-byte full-aggregation
# member, complementing case 28 for the member forms. The aggregate
# covers the matching messages, so only the sighash validation fails.
keys, utxos, tx = fresh_setup(3, key_offset=92)
_, sig0_45 = sign_optout(tx, utxos, 0, keys[0][2], SIGHASH_DEFAULT)
_, _, _, sig64_45 = sign_fullagg_group(
tx, utxos, [(1, keys[1][2], 0x04), (2, keys[2][2], SIGHASH_DEFAULT)],
nonce_offset=21)
tx.witnesses[0] = [optout_element(sig0_45)]
tx.witnesses[1] = [fullagg_member(0x04)]
tx.witnesses[2] = [marker_element(MARKER_FULLAGG, sig=sig64_45)]
add_case("member-sighash-undefined", "Full-aggregation member with the "
"undefined sighash value 0x04", tx, utxos, False,
"undefined sighash type")
# 46: invalid, 97-byte 0xbc element carrying the whole aggregate
# signature on the final input, the encoding of an earlier revision
# of this BIP. Half-agg analog of case 17.
keys, utxos, tx = fresh_setup(2, key_offset=95)
_, sigs46, agg46 = sign_halfagg_group(
tx, utxos,
[(0, keys[0][2], SIGHASH_DEFAULT), (1, keys[1][2], SIGHASH_DEFAULT)])
tx.witnesses[0] = [halfagg_member(sigs46[0][:32])]
tx.witnesses[1] = [marker_element(MARKER_HALFAGG, sig=agg46)]
add_case("halfagg-final-old-format", "97-byte 0xbc element carrying the "
"whole aggregate signature, the final input encoding of an "
"earlier revision", tx, utxos, False,
"marker and length match no defined structure")
# 47: valid, half-aggregation member with SIGHASH_ALL|ANYONECANPAY,
# covering the per-input commitment branch of SigMsg in an
# aggregation flavored message
keys, utxos, tx = fresh_setup(2, key_offset=97)
ht47 = SIGHASH_ALL | SIGHASH_ANYONECANPAY
_, sigs47, agg47 = sign_halfagg_group(
tx, utxos, [(0, keys[0][2], ht47), (1, keys[1][2], SIGHASH_DEFAULT)]
)
tx.witnesses[0] = [halfagg_member(sigs47[0][:32], ht47)]
tx.witnesses[1] = [halfagg_final(agg47)]
add_case("halfagg-acp-member", "Half-aggregation group with a "
"SIGHASH_ALL|SIGHASH_ANYONECANPAY member", tx, utxos, True)
# 48: invalid, marker of the full-aggregation final from case 25
# flipped to 0xbc by a third party, which parses as a half-aggregation
# final of the correct size. Fails because the message commits to the
# marker. fresh_setup(1, key_offset=44) recreates the case 25 tx.
keys, utxos, tx = fresh_setup(1, key_offset=44)
m0_48 = sigmsg_v2(tx, utxos, 0, SIGHASH_DEFAULT, MARKER_HALFAGG)
assert not halfagg.VerifyAggregate(
sig64_25, [(utxos[0].script_pubkey[2:], m0_48)])
tx.witnesses[0] = [marker_element(MARKER_HALFAGG, sig=sig64_25)]
add_case("marker-swapped-fullagg", "Full-aggregation final of a "
"single-member group mutated to carry the half-aggregation "
"marker", tx, utxos, False,
"aggregate signature invalid, messages commit to the "
"aggregation mode")
# 49: valid, annex on an empty full-aggregation member element.
# Complements cases 13 and 27 for the empty element form.
keys, utxos, tx = fresh_setup(2, key_offset=99)
annex49 = bytes([0x50]) + b"CISA annex test"
_, _, _, sig64_49 = sign_fullagg_group(
tx, utxos,
[(0, keys[0][2], SIGHASH_DEFAULT), (1, keys[1][2], SIGHASH_DEFAULT)],
nonce_offset=22, annexes={0: annex49})
tx.witnesses[0] = [fullagg_member(), annex49]
tx.witnesses[1] = [marker_element(MARKER_FULLAGG, sig=sig64_49)]
add_case("annex-on-empty-member", "Full-aggregation member carrying an "
"annex on an empty witness element, committed in its signature "
"message", tx, utxos, True)
# 50: invalid, nonce shares swapped between the two inputs by a third
# party. The randomizers of the aggregate commit to the shares in
# input order, so verification fails.
keys, utxos, tx = fresh_setup(2, key_offset=101)
_, sigs50, agg50 = sign_halfagg_group(
tx, utxos,
[(0, keys[0][2], SIGHASH_DEFAULT), (1, keys[1][2], SIGHASH_DEFAULT)])
swapped = sigs50[1][:32] + sigs50[0][:32] + agg50[-32:]
msgs50 = [(utxos[i].script_pubkey[2:],
sigmsg_v2(tx, utxos, i, SIGHASH_DEFAULT, MARKER_HALFAGG))
for i in range(2)]
assert not halfagg.VerifyAggregate(swapped, msgs50)
tx.witnesses[0] = [halfagg_member(sigs50[1][:32])]
tx.witnesses[1] = [halfagg_final(swapped)]
add_case("nonce-shares-swapped", "Nonce shares of the two "
"half-aggregation inputs swapped by a third party", tx, utxos,
False, "aggregate signature invalid, the randomizers commit "
"to the shares in input order")
# 51: invalid, 66-byte element ending in a valid sighash byte.
# 66-byte elements must end in a marker, there is no 66-byte
# opted-out form.
keys, utxos, tx = fresh_setup(2, key_offset=103)
_, sig0_51 = sign_optout(tx, utxos, 0, keys[0][2], SIGHASH_ALL)
_, sig1_51 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
tx.witnesses[0] = [sig0_51 + bytes([SIGHASH_ALL, SIGHASH_ALL])]
tx.witnesses[1] = [optout_element(sig1_51)]
add_case("optout-too-long", "66-byte element ending in a valid sighash "
"byte, one byte above the opted-out maximum", tx, utxos, False,
"marker and length match no defined structure")
# 52: invalid, 1-byte element containing only the marker value. It
# parses as a full-aggregation member with an invalid sighash byte.
# In an earlier revision this was a valid placeholder form.
keys, utxos, tx = fresh_setup(2, key_offset=105)
_, sig1_52 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
tx.witnesses[0] = [bytes([MARKER_HALFAGG])]
tx.witnesses[1] = [optout_element(sig1_52)]
add_case("member-marker-only", "1-byte element containing only the "
"marker value 0xbc, a full-aggregation member with an invalid "
"sighash byte", tx, utxos, False, "undefined sighash type")
# 53: invalid, marker of the half-aggregation final from case 24
# flipped to 0xbd by a third party, the mirror of case 48.
# fresh_setup(1, key_offset=43) recreates the case 24 tx.
keys, utxos, tx = fresh_setup(1, key_offset=43)
m0_53 = sigmsg_v2(tx, utxos, 0, SIGHASH_DEFAULT, MARKER_FULLAGG)
R53 = GE.from_bytes_xonly(aggsig24[:32])
s53 = Scalar.from_bytes_checked(aggsig24[32:])
assert not fullagg.Verify(
[GE.from_bytes_xonly(utxos[0].script_pubkey[2:])], [m0_53],
(R53, s53))
tx.witnesses[0] = [marker_element(MARKER_FULLAGG, sig=aggsig24)]
add_case("marker-swapped-halfagg", "Half-aggregation final of a "
"single-member group mutated to carry the full-aggregation "
"marker", tx, utxos, False,
"aggregate signature invalid, messages commit to the "
"aggregation mode")
# 54: valid, witness v2 program wrapped in P2SH, the P2SH analog of
# case 30. Such outputs remain unencumbered by the BIP, so the
# witness content is unconstrained.
keys, utxos, tx = fresh_setup(2, key_offset=107)
redeem54 = bytes([0x52, 0x20]) + test_garbage(b"p2sh program")
h160 = hashlib.new("ripemd160", sha256(redeem54)).digest()
utxos[0] = TxOut(100_000_000, bytes([0xA9, 0x14]) + h160 + bytes([0x87]))
tx.vin[0].script_sig = bytes([len(redeem54)]) + redeem54
_, sig1_54 = sign_optout(tx, utxos, 1, keys[1][2], SIGHASH_DEFAULT)
tx.witnesses[0] = [bytes([0xBE])]
tx.witnesses[1] = [optout_element(sig1_54)]
add_case("program-p2sh-wrapped", "P2SH-wrapped witness v2 program "
"spent with an arbitrary witness", tx, utxos, True)
# 55: valid, full-aggregation group using SIGHASH_NONE,
# SIGHASH_SINGLE and the three ANYONECANPAY variants. Every input
# has an output at its index, as SIGHASH_SINGLE requires.
keys, utxos, tx = fresh_setup(5, key_offset=109)
tx.vout = [TxOut(99_000_000, v2_script_pubkey(k[3])) for k in keys]
ht55 = [SIGHASH_NONE, SIGHASH_SINGLE,
SIGHASH_ALL | SIGHASH_ANYONECANPAY,
SIGHASH_NONE | SIGHASH_ANYONECANPAY,
SIGHASH_SINGLE | SIGHASH_ANYONECANPAY]
_, _, _, sig64_55 = sign_fullagg_group(
tx, utxos, [(i, keys[i][2], ht) for i, ht in enumerate(ht55)],
nonce_offset=23)
for i in range(4):
tx.witnesses[i] = [fullagg_member(ht55[i])]
tx.witnesses[4] = [marker_element(MARKER_FULLAGG, ht55[4], sig64_55)]
add_case("fullagg-sighash-types", "Full-aggregation group using "
"SIGHASH_NONE, SIGHASH_SINGLE and the three ANYONECANPAY "
"variants", tx, utxos, True)
# 56: valid, half-aggregation group using SIGHASH_NONE,
# SIGHASH_NONE|ANYONECANPAY and SIGHASH_SINGLE|ANYONECANPAY. Every
# input has an output at its index, as SIGHASH_SINGLE requires.
keys, utxos, tx = fresh_setup(3, key_offset=114)
tx.vout = [TxOut(99_000_000, v2_script_pubkey(k[3])) for k in keys]
ht56 = [SIGHASH_NONE, SIGHASH_NONE | SIGHASH_ANYONECANPAY,
SIGHASH_SINGLE | SIGHASH_ANYONECANPAY]
_, sigs56, agg56 = sign_halfagg_group(
tx, utxos, [(i, keys[i][2], ht) for i, ht in enumerate(ht56)])
tx.witnesses[0] = [halfagg_member(sigs56[0][:32], ht56[0])]
tx.witnesses[1] = [halfagg_member(sigs56[1][:32], ht56[1])]
tx.witnesses[2] = [halfagg_final(agg56, ht56[2])]
add_case("halfagg-sighash-types", "Half-aggregation group using "
"SIGHASH_NONE, SIGHASH_NONE|ANYONECANPAY and "
"SIGHASH_SINGLE|ANYONECANPAY", tx, utxos, True)
return {"testCases": cases}
def main():
out_dir = Path(__file__).parent
wallet = make_wallet_vectors()
consensus = make_consensus_vectors()
with open(out_dir / "wallet-test-vectors.json", "w") as f:
json.dump(wallet, f, indent=2)
f.write("\n")
with open(out_dir / "consensus-test-vectors.json", "w") as f:
json.dump(consensus, f, indent=2)
f.write("\n")
print(f"wrote {out_dir / 'wallet-test-vectors.json'}")
print(f"wrote {out_dir / 'consensus-test-vectors.json'}")
if __name__ == "__main__":
main()