|
70 | 70 | OP_NOTIF, |
71 | 71 | OP_PUSHDATA1, |
72 | 72 | OP_RETURN, |
| 73 | + OP_SIZE, |
73 | 74 | OP_SWAP, |
| 75 | + OP_TEMPLATEHASH, |
74 | 76 | OP_VERIFY, |
75 | 77 | SIGHASH_DEFAULT, |
76 | 78 | SIGHASH_ALL, |
|
80 | 82 | SegwitV0SignatureMsg, |
81 | 83 | TaggedHash, |
82 | 84 | TaprootSignatureMsg, |
| 85 | + TemplateMsg, |
83 | 86 | is_op_success, |
84 | 87 | taproot_construct, |
85 | 88 | ) |
@@ -257,6 +260,18 @@ def default_sighash(ctx): |
257 | 260 | else: |
258 | 261 | return hash256(msg) |
259 | 262 |
|
| 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 | + |
260 | 275 | def default_tweak(ctx): |
261 | 276 | """Default expression for "tweak": None if a leaf is specified, tap[0] otherwise.""" |
262 | 277 | if get(ctx, "leaf") is None: |
@@ -384,6 +399,10 @@ def default_scriptsig(ctx): |
384 | 399 | "sigmsg": default_sigmsg, |
385 | 400 | # The sighash value (32 bytes) |
386 | 401 | "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, |
387 | 406 | # The information about the chosen script path spend (TaprootLeafInfo object). |
388 | 407 | "tapleaf": default_tapleaf, |
389 | 408 | # The script to push, and include in the sighash, for a taproot script path spend. |
@@ -615,6 +634,7 @@ def byte_popper(expr): |
615 | 634 | ERR_EVAL_FALSE = {"err_msg": "Script evaluated without error but finished with a false/empty top stack element"} |
616 | 635 | ERR_WITNESS_PROGRAM_WITNESS_EMPTY = {"err_msg": "Witness program was passed an empty witness"} |
617 | 636 | ERR_CHECKSIGVERIFY = {"err_msg": "Script failed an OP_CHECKSIGVERIFY operation"} |
| 637 | +SCRIPT_ERR_EQUALVERIFY = {"err_msg": "Script failed an OP_EQUALVERIFY operation"} |
618 | 638 |
|
619 | 639 | VALID_SIGHASHES_ECDSA = [ |
620 | 640 | SIGHASH_ALL, |
@@ -645,7 +665,7 @@ def byte_popper(expr): |
645 | 665 | # === Actual test cases === |
646 | 666 |
|
647 | 667 |
|
648 | | -def spenders_taproot_active(): |
| 668 | +def spenders_taproot_active(template_active): |
649 | 669 | """Return a list of Spenders for testing post-Taproot activation behavior.""" |
650 | 670 |
|
651 | 671 | secs = [generate_privkey() for _ in range(8)] |
@@ -1141,7 +1161,7 @@ def predict_sigops_ratio(n, dummy_size): |
1141 | 1161 | hashtype = lambda _: random.choice(VALID_SIGHASHES_TAPROOT) |
1142 | 1162 | for opval in range(76, 0x100): |
1143 | 1163 | 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): |
1145 | 1165 | continue |
1146 | 1166 | scripts = [ |
1147 | 1167 | ("bare_success", CScript([opcode])), |
@@ -1172,7 +1192,7 @@ def predict_sigops_ratio(n, dummy_size): |
1172 | 1192 | # Non-OP_SUCCESSx (verify that those aren't accidentally treated as OP_SUCCESSx) |
1173 | 1193 | for opval in range(0, 0x100): |
1174 | 1194 | opcode = CScriptOp(opval) |
1175 | | - if is_op_success(opcode, is_temphash_active=True): |
| 1195 | + if is_op_success(opcode, is_temphash_active=template_active): |
1176 | 1196 | continue |
1177 | 1197 | scripts = [ |
1178 | 1198 | ("normal", CScript([OP_RETURN, opcode] + [OP_NOP] * 75)), |
@@ -1200,13 +1220,92 @@ def predict_sigops_ratio(n, dummy_size): |
1200 | 1220 | 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) |
1201 | 1221 | 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) |
1202 | 1222 |
|
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. |
1204 | 1224 | for p2sh in [False, True]: |
1205 | 1225 | for witv0 in [False, True]: |
1206 | 1226 | for hashtype in VALID_SIGHASHES_ECDSA + [random.randrange(0x04, 0x80), random.randrange(0x84, 0x100)]: |
1207 | 1227 | standard = hashtype in VALID_SIGHASHES_ECDSA and (p2sh or witv0) |
1208 | 1228 | 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) |
1209 | 1229 |
|
| 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 | + |
1210 | 1309 | return spenders |
1211 | 1310 |
|
1212 | 1311 |
|
@@ -1271,11 +1370,14 @@ def sample_spenders(): |
1271 | 1370 | # Consensus validation flags to use in dumps for all other tests. |
1272 | 1371 | TAPROOT_FLAGS = "P2SH,DERSIG,CHECKLOCKTIMEVERIFY,CHECKSEQUENCEVERIFY,WITNESS,NULLDUMMY,TAPROOT" |
1273 | 1372 |
|
1274 | | -def dump_json_test(tx, input_utxos, idx, success, failure): |
| 1373 | +def dump_json_test(tx, input_utxos, idx, success, failure, template_active): |
1275 | 1374 | spender = input_utxos[idx].spender |
1276 | 1375 | # Determine flags to dump |
1277 | 1376 | flags = LEGACY_FLAGS if spender.comment.startswith("legacy/") or spender.comment.startswith("inactive/") else TAPROOT_FLAGS |
1278 | 1377 |
|
| 1378 | + if template_active and flags == TAPROOT_FLAGS: |
| 1379 | + flags += ",TEMPLATEHASH" |
| 1380 | + |
1279 | 1381 | fields = [ |
1280 | 1382 | ("tx", tx.serialize().hex()), |
1281 | 1383 | ("prevouts", [x.output.serialize().hex() for x in input_utxos]), |
@@ -1320,6 +1422,7 @@ def skip_test_if_missing_module(self): |
1320 | 1422 | def set_test_params(self): |
1321 | 1423 | self.num_nodes = 1 |
1322 | 1424 | self.setup_clean_chain = True |
| 1425 | + self.extra_args = [[f"-vbparams=templatehash:0:{2**63 - 1}"]] # test activation of templatehash |
1323 | 1426 |
|
1324 | 1427 | def block_submit(self, node, txs, msg, err_msg, cb_pubkey=None, fees=0, sigops_weight=0, witness=False, accept=False): |
1325 | 1428 |
|
@@ -1353,7 +1456,7 @@ def init_blockinfo(self, node): |
1353 | 1456 | self.lastblockheight = block['height'] |
1354 | 1457 | self.lastblocktime = block['time'] |
1355 | 1458 |
|
1356 | | - def test_spenders(self, node, spenders, input_counts): |
| 1459 | + def test_spenders(self, node, spenders, input_counts, template_active): |
1357 | 1460 | """Run randomized tests with a number of "spenders". |
1358 | 1461 |
|
1359 | 1462 | Steps: |
@@ -1519,7 +1622,7 @@ def test_spenders(self, node, spenders, input_counts): |
1519 | 1622 | fail = fn(tx, i, [utxo.output for utxo in input_utxos], False) |
1520 | 1623 | input_data.append((fail, success)) |
1521 | 1624 | 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) |
1523 | 1626 |
|
1524 | 1627 | # Sign each input incorrectly once on each complete signing pass, except the very last. |
1525 | 1628 | for fail_input in list(range(len(input_utxos))) + [None]: |
@@ -1792,19 +1895,39 @@ def run_test(self): |
1792 | 1895 |
|
1793 | 1896 | self.log.info("Post-activation tests...") |
1794 | 1897 |
|
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) |
1808 | 1931 |
|
1809 | 1932 |
|
1810 | 1933 | if __name__ == '__main__': |
|
0 commit comments