Skip to content

Commit bfda78a

Browse files
instagibbsdarosior
authored andcommitted
qa: functional test coverage of OP_TEMPLATEHASH
This leverages the extensive feature_taproot.py test framework to generate coverage for the numerous scenarii and mutations exercised there. Additionally, a separate feature_templatehash.py functional test is introduced for end-to-end testing of the commit-to-next-transaction use case, which does not fit nicely into the feature_taproot.py framework (assumes input independence).
1 parent b0e4a71 commit bfda78a

File tree

4 files changed

+403
-20
lines changed

4 files changed

+403
-20
lines changed

test/functional/feature_taproot.py

Lines changed: 143 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@
7070
OP_NOTIF,
7171
OP_PUSHDATA1,
7272
OP_RETURN,
73+
OP_SIZE,
7374
OP_SWAP,
75+
OP_TEMPLATEHASH,
7476
OP_VERIFY,
7577
SIGHASH_DEFAULT,
7678
SIGHASH_ALL,
@@ -80,6 +82,7 @@
8082
SegwitV0SignatureMsg,
8183
TaggedHash,
8284
TaprootSignatureMsg,
85+
TemplateMsg,
8386
is_op_success,
8487
taproot_construct,
8588
)
@@ -257,6 +260,18 @@ def default_sighash(ctx):
257260
else:
258261
return hash256(msg)
259262

263+
def default_templatehash_message(ctx):
264+
"""Default expression for "template_hash_msg"."""
265+
tx = get(ctx, "tx")
266+
idx = get(ctx, "idx")
267+
annex = get(ctx, "annex")
268+
return TemplateMsg(tx, idx, annex=annex)
269+
270+
def default_templatehash(ctx):
271+
"""Default expression for "template_hash": the tagged hash of the digest."""
272+
msg = get(ctx, "template_hash_msg")
273+
return TaggedHash("TemplateHash", msg)
274+
260275
def default_tweak(ctx):
261276
"""Default expression for "tweak": None if a leaf is specified, tap[0] otherwise."""
262277
if get(ctx, "leaf") is None:
@@ -384,6 +399,10 @@ def default_scriptsig(ctx):
384399
"sigmsg": default_sigmsg,
385400
# The sighash value (32 bytes)
386401
"sighash": default_sighash,
402+
# The template msg value (preimage of template_hash)
403+
"template_hash_msg": default_templatehash_message,
404+
# The template_hash value (32 bytes)
405+
"template_hash": default_templatehash,
387406
# The information about the chosen script path spend (TaprootLeafInfo object).
388407
"tapleaf": default_tapleaf,
389408
# The script to push, and include in the sighash, for a taproot script path spend.
@@ -615,6 +634,7 @@ def byte_popper(expr):
615634
ERR_EVAL_FALSE = {"err_msg": "Script evaluated without error but finished with a false/empty top stack element"}
616635
ERR_WITNESS_PROGRAM_WITNESS_EMPTY = {"err_msg": "Witness program was passed an empty witness"}
617636
ERR_CHECKSIGVERIFY = {"err_msg": "Script failed an OP_CHECKSIGVERIFY operation"}
637+
SCRIPT_ERR_EQUALVERIFY = {"err_msg": "Script failed an OP_EQUALVERIFY operation"}
618638

619639
VALID_SIGHASHES_ECDSA = [
620640
SIGHASH_ALL,
@@ -645,7 +665,7 @@ def byte_popper(expr):
645665
# === Actual test cases ===
646666

647667

648-
def spenders_taproot_active():
668+
def spenders_taproot_active(template_active):
649669
"""Return a list of Spenders for testing post-Taproot activation behavior."""
650670

651671
secs = [generate_privkey() for _ in range(8)]
@@ -1141,7 +1161,7 @@ def predict_sigops_ratio(n, dummy_size):
11411161
hashtype = lambda _: random.choice(VALID_SIGHASHES_TAPROOT)
11421162
for opval in range(76, 0x100):
11431163
opcode = CScriptOp(opval)
1144-
if not is_op_success(opcode, is_temphash_active=True):
1164+
if not is_op_success(opcode, is_temphash_active=template_active):
11451165
continue
11461166
scripts = [
11471167
("bare_success", CScript([opcode])),
@@ -1172,7 +1192,7 @@ def predict_sigops_ratio(n, dummy_size):
11721192
# Non-OP_SUCCESSx (verify that those aren't accidentally treated as OP_SUCCESSx)
11731193
for opval in range(0, 0x100):
11741194
opcode = CScriptOp(opval)
1175-
if is_op_success(opcode, is_temphash_active=True):
1195+
if is_op_success(opcode, is_temphash_active=template_active):
11761196
continue
11771197
scripts = [
11781198
("normal", CScript([OP_RETURN, opcode] + [OP_NOP] * 75)),
@@ -1200,13 +1220,92 @@ def predict_sigops_ratio(n, dummy_size):
12001220
add_spender(spenders, "legacy/pk-wrongkey", hashtype=hashtype, p2sh=p2sh, witv0=witv0, standard=standard, script=key_to_p2pk_script(pubkey1), **SINGLE_SIG, key=eckey1, failure={"key": eckey2}, sigops_weight=4-3*witv0, **ERR_EVAL_FALSE)
12011221
add_spender(spenders, "legacy/pkh-sighashflip", hashtype=hashtype, p2sh=p2sh, witv0=witv0, standard=standard, pkh=pubkey1, key=eckey1, **SIGHASH_BITFLIP, sigops_weight=4-3*witv0, **ERR_EVAL_FALSE)
12021222

1203-
# Verify that OP_CHECKSIGADD wasn't accidentally added to pre-taproot validation logic.
1223+
# Verify that OP_CHECKSIGADD, OP_TEMPLATEHASH weren't accidentally added to pre-taproot validation logic.
12041224
for p2sh in [False, True]:
12051225
for witv0 in [False, True]:
12061226
for hashtype in VALID_SIGHASHES_ECDSA + [random.randrange(0x04, 0x80), random.randrange(0x84, 0x100)]:
12071227
standard = hashtype in VALID_SIGHASHES_ECDSA and (p2sh or witv0)
12081228
add_spender(spenders, "compat/nocsa", hashtype=hashtype, p2sh=p2sh, witv0=witv0, standard=standard, script=CScript([OP_IF, OP_11, pubkey1, OP_CHECKSIGADD, OP_12, OP_EQUAL, OP_ELSE, pubkey1, OP_CHECKSIG, OP_ENDIF]), key=eckey1, sigops_weight=4-3*witv0, inputs=[getter("sign"), b''], failure={"inputs": [getter("sign"), b'\x01']}, **ERR_BAD_OPCODE)
12091229

1230+
# Should still fail if not in executed branch
1231+
add_spender(spenders, "compat/noth", p2sh=p2sh, witv0=witv0, standard=p2sh or witv0, script=CScript([OP_IF, OP_TEMPLATEHASH, OP_ENDIF, OP_1]), inputs=[b''], failure={"inputs": [b'\x01']}, **ERR_BAD_OPCODE)
1232+
1233+
return spenders
1234+
1235+
def generate_template_spenders_consensus():
1236+
"""Spenders for testing that post-active TEMPLATEHASH usage is enforced"""
1237+
1238+
spenders = []
1239+
1240+
sec = generate_privkey()
1241+
pub, _ = compute_xonly_pubkey(sec)
1242+
scripts = [
1243+
("basic", CScript([OP_TEMPLATEHASH])),
1244+
("emptystack", CScript([OP_TEMPLATEHASH, OP_DROP])),
1245+
("2stack", CScript([OP_TEMPLATEHASH, OP_1])),
1246+
("equality", CScript([OP_TEMPLATEHASH, OP_EQUAL])),
1247+
("32bytes", CScript([OP_TEMPLATEHASH, OP_SIZE, 0x20, OP_EQUALVERIFY])),
1248+
("wrongbytes", CScript([OP_TEMPLATEHASH, OP_SIZE, 0x21, OP_EQUALVERIFY])),
1249+
("doublegood", CScript([OP_TEMPLATEHASH, OP_TEMPLATEHASH, OP_EQUAL])),
1250+
]
1251+
tap = taproot_construct(pub, scripts)
1252+
1253+
add_spender(spenders, "template/basic", tap=tap, leaf="basic", failure={"leaf": "emptystack"}, **ERR_CLEANSTACK)
1254+
add_spender(spenders, "template/2stack", tap=tap, leaf="basic", failure={"leaf": "2stack"}, **ERR_CLEANSTACK)
1255+
add_spender(spenders, "template/32bytes", tap=tap, leaf="32bytes", failure={"leaf": "wrongbytes"}, **SCRIPT_ERR_EQUALVERIFY)
1256+
add_spender(spenders, "template/doublegood", tap=tap, leaf="doublegood", failure={"inputs": [random.randbytes(1)]}, **ERR_CLEANSTACK)
1257+
1258+
TEMPLATEHASH_BITFLIP = {"failure": {"template_hash": bitflipper(default_templatehash)}}
1259+
TEMPLATEHASH_POP_BYTE = {"failure": {"template_hash": byte_popper(default_templatehash)}}
1260+
TEMPLATEHASH_ADD_ZERO = {"failure": {"template_hash": zero_appender(default_templatehash)}}
1261+
1262+
# Test various 31/32/33-byte pushes with mutations
1263+
for i, mutator in enumerate([TEMPLATEHASH_BITFLIP, TEMPLATEHASH_POP_BYTE, TEMPLATEHASH_ADD_ZERO]):
1264+
add_spender(spenders, f"template/equality_{i}", tap=tap, leaf="equality", inputs=[getter("template_hash")], **mutator, **ERR_EVAL_FALSE)
1265+
1266+
# Test random other lengths
1267+
for i in range(256):
1268+
if i == 32:
1269+
continue
1270+
wrongsize_template_hash = random.randbytes(i)
1271+
add_spender(spenders, f"template/equality_rand_{i}", tap=tap, leaf="equality", inputs=[getter("template_hash")], failure={"inputs": [wrongsize_template_hash]}, **ERR_EVAL_FALSE)
1272+
1273+
# Test annex commitment
1274+
for i in range(32):
1275+
# No annex commitment
1276+
add_spender(spenders, f"template/equality_annex_none_{i}", tap=tap, leaf="equality", inputs=[getter("template_hash")], failure={"template_hash": override(default_templatehash, annex=bytes([ANNEX_TAG]) + random.randbytes(i))}, **ERR_EVAL_FALSE)
1277+
1278+
# Annex committed, compared to none
1279+
add_spender(spenders, f"template/equality_annex_{i}", tap=tap, leaf="equality", standard=False, annex=bytes([ANNEX_TAG]) + random.randbytes(i), inputs=[getter("template_hash")], failure={"template_hash": override(default_templatehash, annex=None)}, **ERR_EVAL_FALSE)
1280+
1281+
# Both have annex, no collision allowed
1282+
if i > 0:
1283+
annex = bytes([ANNEX_TAG]) + random.randbytes(i)
1284+
wrong_annex = None
1285+
while wrong_annex is None or wrong_annex == annex:
1286+
wrong_annex = bytes([ANNEX_TAG]) + random.randbytes(i)
1287+
add_spender(spenders, f"template/equality_annex_mismatch_{i}", tap=tap, leaf="equality", annex=annex, standard=False, inputs=[getter("template_hash")], failure={"template_hash": override(default_sighash, annex=wrong_annex)}, **ERR_EVAL_FALSE)
1288+
1289+
return spenders
1290+
1291+
def generate_template_spenders_nonstandard():
1292+
"""Spenders for testing that pre-active TEMPLATEHASH usage is discouraged"""
1293+
1294+
spenders = []
1295+
1296+
sec = generate_privkey()
1297+
pub, _ = compute_xonly_pubkey(sec)
1298+
scripts = [
1299+
("basic", CScript([OP_TEMPLATEHASH])),
1300+
("emptystack", CScript([OP_TEMPLATEHASH, OP_DROP])),
1301+
]
1302+
tap = taproot_construct(pub, scripts)
1303+
1304+
# Valid but non-standard until activation
1305+
add_spender(spenders, "discouraged_template/basic", tap=tap, leaf="basic", standard=False)
1306+
# This will fail after activation
1307+
add_spender(spenders, "discouraged_template/emptystack", tap=tap, leaf="emptystack", standard=False)
1308+
12101309
return spenders
12111310

12121311

@@ -1271,11 +1370,14 @@ def sample_spenders():
12711370
# Consensus validation flags to use in dumps for all other tests.
12721371
TAPROOT_FLAGS = "P2SH,DERSIG,CHECKLOCKTIMEVERIFY,CHECKSEQUENCEVERIFY,WITNESS,NULLDUMMY,TAPROOT"
12731372

1274-
def dump_json_test(tx, input_utxos, idx, success, failure):
1373+
def dump_json_test(tx, input_utxos, idx, success, failure, template_active):
12751374
spender = input_utxos[idx].spender
12761375
# Determine flags to dump
12771376
flags = LEGACY_FLAGS if spender.comment.startswith("legacy/") or spender.comment.startswith("inactive/") else TAPROOT_FLAGS
12781377

1378+
if template_active and flags == TAPROOT_FLAGS:
1379+
flags += ",TEMPLATEHASH"
1380+
12791381
fields = [
12801382
("tx", tx.serialize().hex()),
12811383
("prevouts", [x.output.serialize().hex() for x in input_utxos]),
@@ -1320,6 +1422,7 @@ def skip_test_if_missing_module(self):
13201422
def set_test_params(self):
13211423
self.num_nodes = 1
13221424
self.setup_clean_chain = True
1425+
self.extra_args = [[f"-vbparams=templatehash:0:{2**63 - 1}"]] # test activation of templatehash
13231426

13241427
def block_submit(self, node, txs, msg, err_msg, cb_pubkey=None, fees=0, sigops_weight=0, witness=False, accept=False):
13251428

@@ -1353,7 +1456,7 @@ def init_blockinfo(self, node):
13531456
self.lastblockheight = block['height']
13541457
self.lastblocktime = block['time']
13551458

1356-
def test_spenders(self, node, spenders, input_counts):
1459+
def test_spenders(self, node, spenders, input_counts, template_active):
13571460
"""Run randomized tests with a number of "spenders".
13581461
13591462
Steps:
@@ -1519,7 +1622,7 @@ def test_spenders(self, node, spenders, input_counts):
15191622
fail = fn(tx, i, [utxo.output for utxo in input_utxos], False)
15201623
input_data.append((fail, success))
15211624
if self.options.dump_tests:
1522-
dump_json_test(tx, input_utxos, i, success, fail)
1625+
dump_json_test(tx, input_utxos, i, success, fail, template_active)
15231626

15241627
# Sign each input incorrectly once on each complete signing pass, except the very last.
15251628
for fail_input in list(range(len(input_utxos))) + [None]:
@@ -1792,19 +1895,39 @@ def run_test(self):
17921895

17931896
self.log.info("Post-activation tests...")
17941897

1795-
# New sub-tests not checking standardness can be added to consensus_spenders
1796-
# to allow for increased coverage across input types.
1797-
# See sample_spenders for a minimal example
1798-
consensus_spenders = sample_spenders()
1799-
consensus_spenders += spenders_taproot_active()
1800-
self.test_spenders(self.nodes[0], consensus_spenders, input_counts=[1, 2, 2, 2, 2, 3])
1801-
1802-
# Run each test twice; once in isolation, and once combined with others. Testing in isolation
1803-
# means that the standardness is verified in every test (as combined transactions are only standard
1804-
# when all their inputs are standard).
1805-
nonstd_spenders = spenders_taproot_nonstandard()
1806-
self.test_spenders(self.nodes[0], nonstd_spenders, input_counts=[1])
1807-
self.test_spenders(self.nodes[0], nonstd_spenders, input_counts=[2, 3])
1898+
# Run all non-TEMPLATEHASH tests pre and post-activation to avoid regressions
1899+
for template_active in [False, True]:
1900+
discouragement_spenders = spenders_taproot_nonstandard()
1901+
consensus_spenders = spenders_taproot_active(template_active=template_active)
1902+
1903+
if not template_active:
1904+
self.log.info("TEMPLATEHASH Pre-activation tests...")
1905+
assert_equal(self.nodes[0].getdeploymentinfo()["deployments"]["templatehash"]["bip9"]["status"], "defined")
1906+
1907+
# Discouragement tests to ensure non-inclusion in mempool before activation
1908+
discouragement_spenders += generate_template_spenders_nonstandard()
1909+
1910+
else:
1911+
self.log.info("Activating TEMPLATEHASH softfork")
1912+
# Blocks being created until now have not signalled
1913+
self.generate(self.nodes[0], 432)
1914+
assert_equal(self.nodes[0].getdeploymentinfo()["deployments"]["templatehash"]["bip9"]["status"], "active")
1915+
1916+
self.log.info("TEMPLATEHASH Post-activation tests...")
1917+
# Test coverage for committed hash in outputs is not included here but in
1918+
# feature_templatehash.py
1919+
consensus_spenders += generate_template_spenders_consensus()
1920+
1921+
# New sub-tests not checking standardness can be added to consensus_spenders
1922+
# to allow for increased coverage across input types.
1923+
# See sample_spenders for a minimal example
1924+
self.test_spenders(self.nodes[0], consensus_spenders, input_counts=[1, 2, 2, 2, 2, 3], template_active=template_active)
1925+
1926+
# Run each test twice; once in isolation, and once combined with others. Testing in isolation
1927+
# means that the standardness is verified in every test (as combined transactions are only standard
1928+
# when all their inputs are standard).
1929+
self.test_spenders(self.nodes[0], discouragement_spenders, input_counts=[1], template_active=template_active)
1930+
self.test_spenders(self.nodes[0], discouragement_spenders, input_counts=[2, 3], template_active=template_active)
18081931

18091932

18101933
if __name__ == '__main__':

0 commit comments

Comments
 (0)