Skip to content

Commit ac3037d

Browse files
committed
tests: BIP341 test vector generation
1 parent ca83ffc commit ac3037d

File tree

1 file changed

+250
-7
lines changed

1 file changed

+250
-7
lines changed

test/functional/feature_taproot.py

Lines changed: 250 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222
)
2323
from test_framework.script import (
2424
ANNEX_TAG,
25+
BIP341_sha_amounts,
26+
BIP341_sha_outputs,
27+
BIP341_sha_prevouts,
28+
BIP341_sha_scriptpubkeys,
29+
BIP341_sha_sequences,
2530
CScript,
2631
CScriptNum,
2732
CScriptOp,
@@ -79,6 +84,7 @@
7984
)
8085
from test_framework.script_util import (
8186
key_to_p2pk_script,
87+
key_to_p2pkh_script,
8288
key_to_p2wpkh_script,
8389
keyhash_to_p2pkh_script,
8490
script_to_p2sh_script,
@@ -89,6 +95,7 @@
8995
from test_framework.key import generate_privkey, compute_xonly_pubkey, sign_schnorr, tweak_add_privkey, ECKey
9096
from test_framework.address import (
9197
hash160,
98+
program_to_witness
9299
)
93100
from collections import OrderedDict, namedtuple
94101
from io import BytesIO
@@ -97,6 +104,9 @@
97104
import os
98105
import random
99106

107+
# Whether or not to output generated test vectors, in JSON format.
108+
GEN_TEST_VECTORS = False
109+
100110
# === Framework for building spending transactions. ===
101111
#
102112
# The computation is represented as a "context" dict, whose entries store potentially-unevaluated expressions that
@@ -418,6 +428,7 @@ def flatten(lst):
418428
ret.append(elem)
419429
return ret
420430

431+
421432
def spend(tx, idx, utxos, **kwargs):
422433
"""Sign transaction input idx of tx, provided utxos is the list of outputs being spent.
423434
@@ -1276,6 +1287,14 @@ def block_submit(self, node, txs, msg, err_msg, cb_pubkey=None, fees=0, sigops_w
12761287
else:
12771288
assert node.getbestblockhash() == self.lastblockhash, "Failed to reject: " + msg
12781289

1290+
def init_blockinfo(self, node):
1291+
# Initialize variables used by block_submit().
1292+
self.lastblockhash = node.getbestblockhash()
1293+
self.tip = int(self.lastblockhash, 16)
1294+
block = node.getblock(self.lastblockhash)
1295+
self.lastblockheight = block['height']
1296+
self.lastblocktime = block['time']
1297+
12791298
def test_spenders(self, node, spenders, input_counts):
12801299
"""Run randomized tests with a number of "spenders".
12811300
@@ -1302,12 +1321,7 @@ def test_spenders(self, node, spenders, input_counts):
13021321
host_spks.append(spk)
13031322
host_pubkeys.append(bytes.fromhex(info['pubkey']))
13041323

1305-
# Initialize variables used by block_submit().
1306-
self.lastblockhash = node.getbestblockhash()
1307-
self.tip = int(self.lastblockhash, 16)
1308-
block = node.getblock(self.lastblockhash)
1309-
self.lastblockheight = block['height']
1310-
self.lastblocktime = block['time']
1324+
self.init_blockinfo(node)
13111325

13121326
# Create transactions spending up to 50 of the wallet's inputs, with one output for each spender, and
13131327
# one change output at the end. The transaction is constructed on the Python side to enable
@@ -1481,10 +1495,239 @@ def test_spenders(self, node, spenders, input_counts):
14811495
assert len(mismatching_utxos) == 0
14821496
self.log.info(" - Done")
14831497

1498+
def gen_test_vectors(self):
1499+
"""Run a scenario that corresponds (and optionally produces) to BIP341 test vectors."""
1500+
1501+
self.log.info("Unit test scenario...")
1502+
1503+
# Deterministically mine coins to OP_TRUE in block 1
1504+
assert self.nodes[1].getblockcount() == 0
1505+
coinbase = CTransaction()
1506+
coinbase.nVersion = 1
1507+
coinbase.vin = [CTxIn(COutPoint(0, 0xffffffff), CScript([OP_1, OP_1]), 0xffffffff)]
1508+
coinbase.vout = [CTxOut(5000000000, CScript([OP_1]))]
1509+
coinbase.nLockTime = 0
1510+
coinbase.rehash()
1511+
assert coinbase.hash == "f60c73405d499a956d3162e3483c395526ef78286458a4cb17b125aa92e49b20"
1512+
# Mine it
1513+
block = create_block(hashprev=int(self.nodes[1].getbestblockhash(), 16), coinbase=coinbase)
1514+
block.rehash()
1515+
block.solve()
1516+
self.nodes[1].submitblock(block.serialize().hex())
1517+
assert self.nodes[1].getblockcount() == 1
1518+
self.generate(self.nodes[1], COINBASE_MATURITY)
1519+
1520+
SEED = 317
1521+
VALID_LEAF_VERS = list(range(0xc0, 0x100, 2)) + [0x66, 0x7e, 0x80, 0x84, 0x96, 0x98, 0xba, 0xbc, 0xbe]
1522+
# Generate private keys
1523+
prvs = [hashlib.sha256(SEED.to_bytes(2, 'big') + bytes([i])).digest() for i in range(100)]
1524+
# Generate corresponding public x-only pubkeys
1525+
pubs = [compute_xonly_pubkey(prv)[0] for prv in prvs]
1526+
# Generate taproot objects
1527+
inner_keys = [pubs[i] for i in range(7)]
1528+
1529+
script_lists = [
1530+
None,
1531+
[("0", CScript([pubs[50], OP_CHECKSIG]), 0xc0)],
1532+
[("0", CScript([pubs[51], OP_CHECKSIG]), 0xc0)],
1533+
[("0", CScript([pubs[52], OP_CHECKSIG]), 0xc0), ("1", CScript([b"BIP341"]), VALID_LEAF_VERS[pubs[99][0] % 41])],
1534+
[("0", CScript([pubs[53], OP_CHECKSIG]), 0xc0), ("1", CScript([b"Taproot"]), VALID_LEAF_VERS[pubs[99][1] % 41])],
1535+
[("0", CScript([pubs[54], OP_CHECKSIG]), 0xc0), [("1", CScript([pubs[55], OP_CHECKSIG]), 0xc0), ("2", CScript([pubs[56], OP_CHECKSIG]), 0xc0)]],
1536+
[("0", CScript([pubs[57], OP_CHECKSIG]), 0xc0), [("1", CScript([pubs[58], OP_CHECKSIG]), 0xc0), ("2", CScript([pubs[59], OP_CHECKSIG]), 0xc0)]],
1537+
]
1538+
taps = [taproot_construct(inner_keys[i], script_lists[i]) for i in range(len(inner_keys))]
1539+
1540+
# Require negated taps[0]
1541+
assert taps[0].negflag
1542+
# Require one negated and one non-negated in taps 1 and 2.
1543+
assert taps[1].negflag != taps[2].negflag
1544+
# Require one negated and one non-negated in taps 3 and 4.
1545+
assert taps[3].negflag != taps[4].negflag
1546+
# Require one negated and one non-negated in taps 5 and 6.
1547+
assert taps[5].negflag != taps[6].negflag
1548+
1549+
cblks = [{leaf: get({**DEFAULT_CONTEXT, 'tap': taps[i], 'leaf': leaf}, 'controlblock') for leaf in taps[i].leaves} for i in range(7)]
1550+
# Require one swapped and one unswapped in taps 3 and 4.
1551+
assert (cblks[3]['0'][33:65] < cblks[3]['1'][33:65]) != (cblks[4]['0'][33:65] < cblks[4]['1'][33:65])
1552+
# Require one swapped and one unswapped in taps 5 and 6, both at the top and child level.
1553+
assert (cblks[5]['0'][33:65] < cblks[5]['1'][65:]) != (cblks[6]['0'][33:65] < cblks[6]['1'][65:])
1554+
assert (cblks[5]['1'][33:65] < cblks[5]['2'][33:65]) != (cblks[6]['1'][33:65] < cblks[6]['2'][33:65])
1555+
# Require within taps 5 (and thus also 6) that one level is swapped and the other is not.
1556+
assert (cblks[5]['0'][33:65] < cblks[5]['1'][65:]) != (cblks[5]['1'][33:65] < cblks[5]['2'][33:65])
1557+
1558+
# Compute a deterministic set of scriptPubKeys
1559+
tap_spks = []
1560+
old_spks = []
1561+
spend_info = {}
1562+
# First, taproot scriptPubKeys, for the tap objects constructed above
1563+
for i, tap in enumerate(taps):
1564+
tap_spks.append(tap.scriptPubKey)
1565+
d = {'key': prvs[i], 'tap': tap, 'mode': 'taproot'}
1566+
spend_info[tap.scriptPubKey] = d
1567+
# Then, a number of deterministically generated (keys 0x1,0x2,0x3) with 2x P2PKH, 1x P2WPKH spks.
1568+
for i in range(1, 4):
1569+
prv = ECKey()
1570+
prv.set(i.to_bytes(32, 'big'), True)
1571+
pub = prv.get_pubkey().get_bytes()
1572+
d = {"key": prv}
1573+
d["scriptcode"] = key_to_p2pkh_script(pub)
1574+
d["inputs"] = [getter("sign"), pub]
1575+
if i < 3:
1576+
# P2PKH
1577+
d['spk'] = key_to_p2pkh_script(pub)
1578+
d['mode'] = 'legacy'
1579+
else:
1580+
# P2WPKH
1581+
d['spk'] = key_to_p2wpkh_script(pub)
1582+
d['mode'] = 'witv0'
1583+
old_spks.append(d['spk'])
1584+
spend_info[d['spk']] = d
1585+
1586+
# Construct a deterministic chain of transactions creating UTXOs to the test's spk's (so that they
1587+
# come from distinct txids).
1588+
txn = []
1589+
lasttxid = coinbase.sha256
1590+
amount = 5000000000
1591+
for i, spk in enumerate(old_spks + tap_spks):
1592+
val = 42000000 * (i + 7)
1593+
tx = CTransaction()
1594+
tx.nVersion = 1
1595+
tx.vin = [CTxIn(COutPoint(lasttxid, i & 1), CScript([]), 0xffffffff)]
1596+
tx.vout = [CTxOut(val, spk), CTxOut(amount - val, CScript([OP_1]))]
1597+
if i & 1:
1598+
tx.vout = list(reversed(tx.vout))
1599+
tx.nLockTime = 0
1600+
tx.rehash()
1601+
amount -= val
1602+
lasttxid = tx.sha256
1603+
txn.append(tx)
1604+
spend_info[spk]['prevout'] = COutPoint(tx.sha256, i & 1)
1605+
spend_info[spk]['utxo'] = CTxOut(val, spk)
1606+
# Mine those transactions
1607+
self.init_blockinfo(self.nodes[1])
1608+
self.block_submit(self.nodes[1], txn, "Crediting txn", None, sigops_weight=10, accept=True)
1609+
1610+
# scriptPubKey computation
1611+
tests = {"version": 1}
1612+
spk_tests = tests.setdefault("scriptPubKey", [])
1613+
for i, tap in enumerate(taps):
1614+
test_case = {}
1615+
given = test_case.setdefault("given", {})
1616+
given['internalPubkey'] = tap.internal_pubkey.hex()
1617+
1618+
def pr(node):
1619+
if node is None:
1620+
return None
1621+
elif isinstance(node, tuple):
1622+
return {"id": int(node[0]), "script": node[1].hex(), "leafVersion": node[2]}
1623+
elif len(node) == 1:
1624+
return pr(node[0])
1625+
elif len(node) == 2:
1626+
return [pr(node[0]), pr(node[1])]
1627+
else:
1628+
assert False
1629+
1630+
given['scriptTree'] = pr(script_lists[i])
1631+
intermediary = test_case.setdefault("intermediary", {})
1632+
if len(tap.leaves):
1633+
leafhashes = intermediary.setdefault('leafHashes', [None] * len(tap.leaves))
1634+
for leaf in tap.leaves:
1635+
leafhashes[int(leaf)] = tap.leaves[leaf].leaf_hash.hex()
1636+
intermediary['merkleRoot'] = tap.merkle_root.hex() if tap.merkle_root else None
1637+
intermediary['tweak'] = tap.tweak.hex()
1638+
intermediary['tweakedPubkey'] = tap.output_pubkey.hex()
1639+
expected = test_case.setdefault("expected", {})
1640+
expected['scriptPubKey'] = tap.scriptPubKey.hex()
1641+
expected['bip350Address'] = program_to_witness(1, bytes(tap.output_pubkey), True)
1642+
if len(tap.leaves):
1643+
control_blocks = expected.setdefault("scriptPathControlBlocks", [None] * len(tap.leaves))
1644+
for leaf in tap.leaves:
1645+
ctx = {**DEFAULT_CONTEXT, 'tap': tap, 'leaf': leaf}
1646+
control_blocks[int(leaf)] = get(ctx, "controlblock").hex()
1647+
spk_tests.append(test_case)
1648+
1649+
# Construct a deterministic transaction spending all outputs created above.
1650+
tx = CTransaction()
1651+
tx.nVersion = 2
1652+
tx.vin = []
1653+
inputs = []
1654+
input_spks = [tap_spks[0], tap_spks[1], old_spks[0], tap_spks[2], tap_spks[5], old_spks[2], tap_spks[6], tap_spks[3], tap_spks[4]]
1655+
sequences = [0, 0xffffffff, 0xffffffff, 0xfffffffe, 0xfffffffe, 0, 0, 0xffffffff, 0xffffffff]
1656+
hashtypes = [SIGHASH_SINGLE, SIGHASH_SINGLE|SIGHASH_ANYONECANPAY, SIGHASH_ALL, SIGHASH_ALL, SIGHASH_DEFAULT, SIGHASH_ALL, SIGHASH_NONE, SIGHASH_NONE|SIGHASH_ANYONECANPAY, SIGHASH_ALL|SIGHASH_ANYONECANPAY]
1657+
for i, spk in enumerate(input_spks):
1658+
tx.vin.append(CTxIn(spend_info[spk]['prevout'], CScript(), sequences[i]))
1659+
inputs.append(spend_info[spk]['utxo'])
1660+
tx.vout.append(CTxOut(1000000000, old_spks[1]))
1661+
tx.vout.append(CTxOut(3410000000, pubs[98]))
1662+
tx.nLockTime = 500000000
1663+
precomputed = {
1664+
"hashAmounts": BIP341_sha_amounts(inputs),
1665+
"hashPrevouts": BIP341_sha_prevouts(tx),
1666+
"hashScriptPubkeys": BIP341_sha_scriptpubkeys(inputs),
1667+
"hashSequences": BIP341_sha_sequences(tx),
1668+
"hashOutputs": BIP341_sha_outputs(tx)
1669+
}
1670+
keypath_tests = tests.setdefault("keyPathSpending", [])
1671+
tx_test = {}
1672+
global_given = tx_test.setdefault("given", {})
1673+
global_given['rawUnsignedTx'] = tx.serialize().hex()
1674+
utxos_spent = global_given.setdefault("utxosSpent", [])
1675+
for i in range(len(input_spks)):
1676+
utxos_spent.append({"scriptPubKey": inputs[i].scriptPubKey.hex(), "amountSats": inputs[i].nValue})
1677+
global_intermediary = tx_test.setdefault("intermediary", {})
1678+
for key in sorted(precomputed.keys()):
1679+
global_intermediary[key] = precomputed[key].hex()
1680+
test_list = tx_test.setdefault('inputSpending', [])
1681+
for i in range(len(input_spks)):
1682+
ctx = {
1683+
**DEFAULT_CONTEXT,
1684+
**spend_info[input_spks[i]],
1685+
'tx': tx,
1686+
'utxos': inputs,
1687+
'idx': i,
1688+
'hashtype': hashtypes[i],
1689+
'deterministic': True
1690+
}
1691+
if ctx['mode'] == 'taproot':
1692+
test_case = {}
1693+
given = test_case.setdefault("given", {})
1694+
given['txinIndex'] = i
1695+
given['internalPrivkey'] = get(ctx, 'key').hex()
1696+
if get(ctx, "tap").merkle_root != bytes():
1697+
given['merkleRoot'] = get(ctx, "tap").merkle_root.hex()
1698+
else:
1699+
given['merkleRoot'] = None
1700+
given['hashType'] = get(ctx, "hashtype")
1701+
intermediary = test_case.setdefault("intermediary", {})
1702+
intermediary['internalPubkey'] = get(ctx, "tap").internal_pubkey.hex()
1703+
intermediary['tweak'] = get(ctx, "tap").tweak.hex()
1704+
intermediary['tweakedPrivkey'] = get(ctx, "key_tweaked").hex()
1705+
sigmsg = get(ctx, "sigmsg")
1706+
intermediary['sigMsg'] = sigmsg.hex()
1707+
intermediary['precomputedUsed'] = [key for key in sorted(precomputed.keys()) if sigmsg.count(precomputed[key])]
1708+
intermediary['sigHash'] = get(ctx, "sighash").hex()
1709+
expected = test_case.setdefault("expected", {})
1710+
expected['witness'] = [get(ctx, "sign").hex()]
1711+
test_list.append(test_case)
1712+
tx.wit.vtxinwit.append(CTxInWitness())
1713+
tx.vin[i].scriptSig = CScript(flatten(get(ctx, "scriptsig")))
1714+
tx.wit.vtxinwit[i].scriptWitness.stack = flatten(get(ctx, "witness"))
1715+
aux = tx_test.setdefault("auxiliary", {})
1716+
aux['fullySignedTx'] = tx.serialize().hex()
1717+
keypath_tests.append(tx_test)
1718+
assert_equal(hashlib.sha256(tx.serialize()).hexdigest(), "24bab662cb55a7f3bae29b559f651674c62bcc1cd442d44715c0133939107b38")
1719+
# Mine the spending transaction
1720+
self.block_submit(self.nodes[1], [tx], "Spending txn", None, sigops_weight=10000, accept=True, witness=True)
1721+
1722+
if GEN_TEST_VECTORS:
1723+
print(json.dumps(tests, indent=4, sort_keys=False))
1724+
1725+
14841726
def run_test(self):
1727+
self.gen_test_vectors()
1728+
14851729
# Post-taproot activation tests go first (pre-taproot tests' blocks are invalid post-taproot).
14861730
self.log.info("Post-activation tests...")
1487-
self.generate(self.nodes[1], COINBASE_MATURITY + 1)
14881731
self.test_spenders(self.nodes[1], spenders_taproot_active(), input_counts=[1, 2, 2, 2, 2, 3])
14891732

14901733
# Re-connect nodes in case they have been disconnected

0 commit comments

Comments
 (0)