From 716163deb0d5e3b94f98b03c7a3dd4391f4ecb16 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Thu, 12 Jun 2025 10:44:02 -0400 Subject: [PATCH 01/12] chainparams: introduce a deployment for OP_TEMPLATEHASH The deployment is never active on mainnet or testnet. Activation parameters are intentionally left to be defined eventually and out of scope for this patchset. The deployment is always active on regtest, allowing us to extensively test expected behaviour of the proposed operation using the functional test suite. Co-Authored-By: Greg Sanders --- src/consensus/params.h | 1 + src/deploymentinfo.cpp | 4 ++++ src/kernel/chainparams.cpp | 32 +++++++++++++++++++++++++++++++ src/rpc/blockchain.cpp | 1 + test/functional/mining_basic.py | 2 +- test/functional/rpc_blockchain.py | 13 +++++++++++++ 6 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/consensus/params.h b/src/consensus/params.h index 7f56729455a8..6514e3ebee3d 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -36,6 +36,7 @@ constexpr bool ValidDeployment(BuriedDeployment dep) { return dep <= DEPLOYMENT_ enum DeploymentPos : uint16_t { DEPLOYMENT_TESTDUMMY, + DEPLOYMENT_TEMPLATEHASH, // NOTE: Also add new deployments to VersionBitsDeploymentInfo in deploymentinfo.cpp // Removing an entry may require bumping MinBIP9WarningHeight. MAX_VERSION_BITS_DEPLOYMENTS diff --git a/src/deploymentinfo.cpp b/src/deploymentinfo.cpp index 551982b23ae7..f4ff3d1122f0 100644 --- a/src/deploymentinfo.cpp +++ b/src/deploymentinfo.cpp @@ -13,6 +13,10 @@ const std::array Versi .name = "testdummy", .gbt_optional_rule = true, }, + VBDeploymentInfo{ + .name = "templatehash", + .gbt_optional_rule = true, + } }; std::string DeploymentName(Consensus::BuriedDeployment dep) diff --git a/src/kernel/chainparams.cpp b/src/kernel/chainparams.cpp index cc77a373481b..06650ef1c6bd 100644 --- a/src/kernel/chainparams.cpp +++ b/src/kernel/chainparams.cpp @@ -136,6 +136,12 @@ class CMainParams : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].threshold = 1815; // 90% consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].period = 2016; + // Deployment of OP_TEMPLATEHASH + consensus.vDeployments[Consensus::DEPLOYMENT_TEMPLATEHASH].bit = 3; + consensus.vDeployments[Consensus::DEPLOYMENT_TEMPLATEHASH].nStartTime = Consensus::BIP9Deployment::NEVER_ACTIVE; + consensus.vDeployments[Consensus::DEPLOYMENT_TEMPLATEHASH].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; + consensus.vDeployments[Consensus::DEPLOYMENT_TEMPLATEHASH].min_activation_height = 0; + ApplyDeploymentOptions(opts.dep_opts); consensus.nMinimumChainWork = uint256{"0000000000000000000000000000000000000001128750f82f4c366153a3a030"}; @@ -261,6 +267,12 @@ class CTestNetParams : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].threshold = 1512; // 75% consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].period = 2016; + // Deployment of OP_TEMPLATEHASH + consensus.vDeployments[Consensus::DEPLOYMENT_TEMPLATEHASH].bit = 3; + consensus.vDeployments[Consensus::DEPLOYMENT_TEMPLATEHASH].nStartTime = Consensus::BIP9Deployment::NEVER_ACTIVE; + consensus.vDeployments[Consensus::DEPLOYMENT_TEMPLATEHASH].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; + consensus.vDeployments[Consensus::DEPLOYMENT_TEMPLATEHASH].min_activation_height = 0; + ApplyDeploymentOptions(opts.dep_opts); consensus.nMinimumChainWork = uint256{"0000000000000000000000000000000000000000000017dde1c649f3708d14b6"}; @@ -363,6 +375,12 @@ class CTestNet4Params : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].threshold = 1512; // 75% consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].period = 2016; + // Deployment of OP_TEMPLATEHASH + consensus.vDeployments[Consensus::DEPLOYMENT_TEMPLATEHASH].bit = 3; + consensus.vDeployments[Consensus::DEPLOYMENT_TEMPLATEHASH].nStartTime = Consensus::BIP9Deployment::NEVER_ACTIVE; + consensus.vDeployments[Consensus::DEPLOYMENT_TEMPLATEHASH].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; + consensus.vDeployments[Consensus::DEPLOYMENT_TEMPLATEHASH].min_activation_height = 0; + ApplyDeploymentOptions(opts.dep_opts); consensus.nMinimumChainWork = uint256{"0000000000000000000000000000000000000000000009a0fe15d0177d086304"}; @@ -508,6 +526,12 @@ class SigNetParams : public CChainParams { consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].threshold = 1815; // 90% consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].period = 2016; + // Deployment of OP_TEMPLATEHASH + consensus.vDeployments[Consensus::DEPLOYMENT_TEMPLATEHASH].bit = 3; + consensus.vDeployments[Consensus::DEPLOYMENT_TEMPLATEHASH].nStartTime = Consensus::BIP9Deployment::NEVER_ACTIVE; + consensus.vDeployments[Consensus::DEPLOYMENT_TEMPLATEHASH].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; + consensus.vDeployments[Consensus::DEPLOYMENT_TEMPLATEHASH].min_activation_height = 0; + ApplyDeploymentOptions(options.dep_opts); // message start is defined as the first 4 bytes of the sha256d of the block script @@ -592,6 +616,14 @@ class CRegTestParams : public CChainParams consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].threshold = 108; // 75% consensus.vDeployments[Consensus::DEPLOYMENT_TESTDUMMY].period = 144; // Faster than normal for regtest (144 instead of 2016) + // Deployment of OP_TEMPLATEHASH + consensus.vDeployments[Consensus::DEPLOYMENT_TEMPLATEHASH].bit = 3; + consensus.vDeployments[Consensus::DEPLOYMENT_TEMPLATEHASH].nStartTime = Consensus::BIP9Deployment::ALWAYS_ACTIVE; + consensus.vDeployments[Consensus::DEPLOYMENT_TEMPLATEHASH].nTimeout = Consensus::BIP9Deployment::NO_TIMEOUT; + consensus.vDeployments[Consensus::DEPLOYMENT_TEMPLATEHASH].min_activation_height = 0; // No activation delay + consensus.vDeployments[Consensus::DEPLOYMENT_TEMPLATEHASH].threshold = 108; // 75% + consensus.vDeployments[Consensus::DEPLOYMENT_TEMPLATEHASH].period = 144; + consensus.nMinimumChainWork = uint256{}; consensus.defaultAssumeValid = uint256{}; diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 14412c90fcc9..4cd658d0171a 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1516,6 +1516,7 @@ UniValue DeploymentInfo(const CBlockIndex* blockindex, const ChainstateManager& SoftForkDescPushBack(blockindex, softforks, chainman, Consensus::DEPLOYMENT_CSV); SoftForkDescPushBack(blockindex, softforks, chainman, Consensus::DEPLOYMENT_SEGWIT); SoftForkDescPushBack(blockindex, softforks, chainman, Consensus::DEPLOYMENT_TESTDUMMY); + SoftForkDescPushBack(blockindex, softforks, chainman, Consensus::DEPLOYMENT_TEMPLATEHASH); return softforks; } } // anon namespace diff --git a/test/functional/mining_basic.py b/test/functional/mining_basic.py index 7060a3cba62b..c6979e860937 100755 --- a/test/functional/mining_basic.py +++ b/test/functional/mining_basic.py @@ -449,7 +449,7 @@ def run_test(self): assert_raises_rpc_error(-8, "getblocktemplate must be called with the segwit rule set", node.getblocktemplate, {}) self.log.info("getblocktemplate: result should set the right rules") - assert_equal(['csv', '!segwit', 'taproot'], self.nodes[0].getblocktemplate(NORMAL_GBT_REQUEST_PARAMS)['rules']) + assert_equal(['csv', '!segwit', 'taproot', 'templatehash'], self.nodes[0].getblocktemplate(NORMAL_GBT_REQUEST_PARAMS)['rules']) self.log.info("submitblock: Test block decode failure") assert_raises_rpc_error(-22, "Block decode failed", node.submitblock, block.serialize()[:-15].hex()) diff --git a/test/functional/rpc_blockchain.py b/test/functional/rpc_blockchain.py index d731907fd8ca..17f1b5a1083e 100755 --- a/test/functional/rpc_blockchain.py +++ b/test/functional/rpc_blockchain.py @@ -244,6 +244,19 @@ def check_signalling_deploymentinfo_result(self, gdi_result, height, blockhash, }, 'active': False }, + 'templatehash': { + 'type': 'bip9', + 'height': 0, + 'active': True, + 'bip9': { + 'start_time': -1, + 'timeout': 9223372036854775807, + 'min_activation_height': 0, + 'status': 'active', + 'since': 0, + 'status_next': 'active' + } + }, } }) From 884695fd6fa59821e8fe96db4a7d5b2d728e3b64 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Thu, 12 Jun 2025 11:44:15 -0400 Subject: [PATCH 02/12] script: introduce verify flags for OP_TEMPLATEHASH Script validation of OP_TEMPLATEHASH is enabled past its activation height, which means it is currently never enabled on mainnet/testnet and always is on regtest. Tapscript spends making use of OP_TEMPLATEHASH do not get relayed nor included in blocks until the soft fork is active. We no longer disconnect or ban for loose mempool transaction failures, so we can add the new flag directly to mandatory flag set, which makes disconnect and bans possible in blocks only post-activation. Co-Authored-by: Greg Sanders --- src/policy/policy.h | 6 ++++-- src/script/interpreter.cpp | 2 ++ src/script/interpreter.h | 6 ++++++ src/test/fuzz/script_assets_test_minimizer.cpp | 4 +++- src/validation.cpp | 12 +++++++++++- test/functional/rpc_blockchain.py | 2 +- 6 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/policy/policy.h b/src/policy/policy.h index 13bcf4cef750..05d01da4995a 100644 --- a/src/policy/policy.h +++ b/src/policy/policy.h @@ -107,7 +107,8 @@ static constexpr script_verify_flags MANDATORY_SCRIPT_VERIFY_FLAGS{SCRIPT_VERIFY SCRIPT_VERIFY_CHECKLOCKTIMEVERIFY | SCRIPT_VERIFY_CHECKSEQUENCEVERIFY | SCRIPT_VERIFY_WITNESS | - SCRIPT_VERIFY_TAPROOT}; + SCRIPT_VERIFY_TAPROOT | + SCRIPT_VERIFY_TEMPLATEHASH}; /** * Standard script verification flags that standard transactions will comply @@ -128,7 +129,8 @@ static constexpr script_verify_flags STANDARD_SCRIPT_VERIFY_FLAGS{MANDATORY_SCRI SCRIPT_VERIFY_CONST_SCRIPTCODE | SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_TAPROOT_VERSION | SCRIPT_VERIFY_DISCOURAGE_OP_SUCCESS | - SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_PUBKEYTYPE}; + SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_PUBKEYTYPE | + SCRIPT_VERIFY_DISCOURAGE_TEMPLATEHASH}; /** For convenience, standard but not mandatory verify flags. */ static constexpr script_verify_flags STANDARD_NOT_MANDATORY_VERIFY_FLAGS{STANDARD_SCRIPT_VERIFY_FLAGS & ~MANDATORY_SCRIPT_VERIFY_FLAGS}; diff --git a/src/script/interpreter.cpp b/src/script/interpreter.cpp index 98b16eca6bcd..bc68dcbd49c5 100644 --- a/src/script/interpreter.cpp +++ b/src/script/interpreter.cpp @@ -2200,6 +2200,8 @@ const std::map& ScriptFlagNamesToEnum() FLAG_NAME(DISCOURAGE_UPGRADABLE_PUBKEYTYPE), FLAG_NAME(DISCOURAGE_OP_SUCCESS), FLAG_NAME(DISCOURAGE_UPGRADABLE_TAPROOT_VERSION), + FLAG_NAME(TEMPLATEHASH), + FLAG_NAME(DISCOURAGE_TEMPLATEHASH), }; #undef FLAG_NAME return g_names_to_enum; diff --git a/src/script/interpreter.h b/src/script/interpreter.h index ff63a73f0fc7..71f16a9b48d6 100644 --- a/src/script/interpreter.h +++ b/src/script/interpreter.h @@ -146,6 +146,12 @@ enum class script_verify_flag_name : uint8_t { // Making unknown public key versions (in BIP 342 scripts) non-standard SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_PUBKEYTYPE, + // OP_TEMPLATEHASH validation (BIP 446) + SCRIPT_VERIFY_TEMPLATEHASH, + + // Make OP_TEMPLATEHASH spend non-standard before activation. + SCRIPT_VERIFY_DISCOURAGE_TEMPLATEHASH, + // Constants to point to the highest flag in use. Add new flags above this line. // SCRIPT_VERIFY_END_MARKER diff --git a/src/test/fuzz/script_assets_test_minimizer.cpp b/src/test/fuzz/script_assets_test_minimizer.cpp index fc6f9fb167a1..034cfaff32d2 100644 --- a/src/test/fuzz/script_assets_test_minimizer.cpp +++ b/src/test/fuzz/script_assets_test_minimizer.cpp @@ -98,13 +98,14 @@ const std::map FLAG_NAMES = { {std::string("CHECKSEQUENCEVERIFY"), SCRIPT_VERIFY_CHECKSEQUENCEVERIFY}, {std::string("WITNESS"), SCRIPT_VERIFY_WITNESS}, {std::string("TAPROOT"), SCRIPT_VERIFY_TAPROOT}, + {std::string("TEMPLATE"), SCRIPT_VERIFY_TEMPLATEHASH}, }; std::vector AllFlags() { std::vector ret; - for (unsigned int i = 0; i < 128; ++i) { + for (unsigned int i = 0; i < 256; ++i) { script_verify_flags flag = 0; if (i & 1) flag |= SCRIPT_VERIFY_P2SH; if (i & 2) flag |= SCRIPT_VERIFY_DERSIG; @@ -113,6 +114,7 @@ std::vector AllFlags() if (i & 16) flag |= SCRIPT_VERIFY_CHECKSEQUENCEVERIFY; if (i & 32) flag |= SCRIPT_VERIFY_WITNESS; if (i & 64) flag |= SCRIPT_VERIFY_TAPROOT; + if (i & 128) flag |= SCRIPT_VERIFY_TEMPLATEHASH; // SCRIPT_VERIFY_WITNESS requires SCRIPT_VERIFY_P2SH if (flag & SCRIPT_VERIFY_WITNESS && !(flag & SCRIPT_VERIFY_P2SH)) continue; diff --git a/src/validation.cpp b/src/validation.cpp index 77e91999b6cd..d1e055f2cf34 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1136,7 +1136,12 @@ bool MemPoolAccept::PolicyScriptChecks(const ATMPArgs& args, Workspace& ws) const CTransaction& tx = *ws.m_ptx; TxValidationState& state = ws.m_state; - constexpr script_verify_flags scriptVerifyFlags = STANDARD_SCRIPT_VERIFY_FLAGS; + script_verify_flags scriptVerifyFlags = STANDARD_SCRIPT_VERIFY_FLAGS; + + // Past activation, don't discourage OP_TEMPLATEHASH spends. + if (DeploymentActiveAt(*m_active_chainstate.m_chain.Tip(), m_active_chainstate.m_chainman, Consensus::DEPLOYMENT_TEMPLATEHASH)) { + scriptVerifyFlags &= ~SCRIPT_VERIFY_DISCOURAGE_TEMPLATEHASH; + } // Check input scripts and signatures. // This is done last to help prevent CPU exhaustion denial-of-service attacks. @@ -2284,6 +2289,11 @@ script_verify_flags GetBlockScriptFlags(const CBlockIndex& block_index, const Ch flags |= SCRIPT_VERIFY_NULLDUMMY; } + // Enforce OP_TEMPLATEHASH (BIP446) + if (DeploymentActiveAt(block_index, chainman, Consensus::DEPLOYMENT_TEMPLATEHASH)) { + flags |= SCRIPT_VERIFY_TEMPLATEHASH; + } + return flags; } diff --git a/test/functional/rpc_blockchain.py b/test/functional/rpc_blockchain.py index 17f1b5a1083e..6a018549190e 100755 --- a/test/functional/rpc_blockchain.py +++ b/test/functional/rpc_blockchain.py @@ -216,7 +216,7 @@ def check_signalling_deploymentinfo_result(self, gdi_result, height, blockhash, assert_equal(gdi_result, { "hash": blockhash, "height": height, - "script_flags": ["CHECKLOCKTIMEVERIFY","CHECKSEQUENCEVERIFY","DERSIG","NULLDUMMY","P2SH","TAPROOT","WITNESS"], + "script_flags": ["CHECKLOCKTIMEVERIFY","CHECKSEQUENCEVERIFY","DERSIG","NULLDUMMY","P2SH","TAPROOT","TEMPLATEHASH","WITNESS"], "deployments": { 'bip34': {'type': 'buried', 'active': True, 'height': 2}, 'bip66': {'type': 'buried', 'active': True, 'height': 3}, From b89d43f7abd34086d75fc671a4aea5f8671fe4e0 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Tue, 10 Jun 2025 17:25:39 -0400 Subject: [PATCH 03/12] script: implement OP_TEMPLATEHASH This introduces a new Script operation exclusively available in Tapscript context: OP_TEMPLATEHASH. This operation pushes the hash of the spending transaction on the stack. See BIP446 for details. This operation is introduced as replacing OP_SUCCESS206 (0xce). Co-Authored-By: Greg Sanders --- src/script/interpreter.cpp | 46 ++++++++++++++++++++++++ src/script/interpreter.h | 11 ++++++ src/script/script.cpp | 3 ++ src/script/script.h | 3 ++ src/script/script_error.cpp | 2 ++ src/script/script_error.h | 1 + src/test/script_tests.cpp | 3 +- test/functional/feature_taproot.py | 4 +-- test/functional/test_framework/script.py | 8 ++++- 9 files changed, 77 insertions(+), 4 deletions(-) diff --git a/src/script/interpreter.cpp b/src/script/interpreter.cpp index bc68dcbd49c5..95b87bcaf4c9 100644 --- a/src/script/interpreter.cpp +++ b/src/script/interpreter.cpp @@ -1224,6 +1224,20 @@ bool EvalScript(std::vector >& stack, const CScript& } break; + case OP_TEMPLATEHASH: + { + // OP_TEMPLATEHASH is only available in Tapscript. Note this is the exact same error + // returned in the default case, which would be the one hit by OP_SUCCESS206 before + // the introduction of OP_TEMPLATEHASH. + if (sigversion == SigVersion::BASE || sigversion == SigVersion::WITNESS_V0) { + return set_error(serror, SCRIPT_ERR_BAD_OPCODE); + } + + const uint256 template_hash{checker.GetTemplateHash(execdata)}; + stack.emplace_back(template_hash.begin(), template_hash.end()); + } + break; + default: return set_error(serror, SCRIPT_ERR_BAD_OPCODE); } @@ -1476,6 +1490,7 @@ template PrecomputedTransactionData::PrecomputedTransactionData(const CMutableTr const HashWriter HASHER_TAPSIGHASH{TaggedHash("TapSighash")}; const HashWriter HASHER_TAPLEAF{TaggedHash("TapLeaf")}; const HashWriter HASHER_TAPBRANCH{TaggedHash("TapBranch")}; +const HashWriter HASHER_TEMPLATEHASH{TaggedHash("TemplateHash")}; static bool HandleMissingData(MissingDataBehavior mdb) { @@ -1835,6 +1850,27 @@ bool GenericTransactionSignatureChecker::CheckSequence(const CScriptNum& nSeq return true; } +template +uint256 GenericTransactionSignatureChecker::GetTemplateHash(ScriptExecutionData& execdata) const +{ + assert(txdata && txdata->m_bip341_taproot_ready); + assert(execdata.m_annex_init); + + HashWriter ss{HASHER_TEMPLATEHASH}; + ss << txTo->version; + ss << txTo->nLockTime; + ss << txdata->m_sequences_single_hash; + ss << txdata->m_outputs_single_hash; + const uint8_t annex_present = (execdata.m_annex_present ? 1 : 0); + ss << annex_present; + ss << nIn; + if (execdata.m_annex_present) { + ss << execdata.m_annex_hash; + } + + return ss.GetSHA256(); +} + // explicit instantiation template class GenericTransactionSignatureChecker; template class GenericTransactionSignatureChecker; @@ -1854,6 +1890,16 @@ static bool ExecuteWitnessScript(const std::span& stack_span, con } // New opcodes will be listed here. May use a different sigversion to modify existing opcodes. if (IsOpSuccess(opcode)) { + // Do not return early success on OP_TEMPLATEHASH (OP_SUCCESS206) once it is active. It is + // non-standard until then. + if (opcode == OP_TEMPLATEHASH) { + if (flags & SCRIPT_VERIFY_DISCOURAGE_TEMPLATEHASH) { + return set_error(serror, SCRIPT_ERR_DISCOURAGE_TEMPLATEHASH); + } + if (flags & SCRIPT_VERIFY_TEMPLATEHASH) { + continue; + } + } if (flags & SCRIPT_VERIFY_DISCOURAGE_OP_SUCCESS) { return set_error(serror, SCRIPT_ERR_DISCOURAGE_OP_SUCCESS); } diff --git a/src/script/interpreter.h b/src/script/interpreter.h index 71f16a9b48d6..b8538c9c653e 100644 --- a/src/script/interpreter.h +++ b/src/script/interpreter.h @@ -301,6 +301,11 @@ class BaseSignatureChecker return false; } + virtual uint256 GetTemplateHash(ScriptExecutionData& execdata) const + { + return {}; + } + virtual ~BaseSignatureChecker() = default; }; @@ -338,6 +343,7 @@ class GenericTransactionSignatureChecker : public BaseSignatureChecker bool CheckSchnorrSignature(std::span sig, std::span pubkey, SigVersion sigversion, ScriptExecutionData& execdata, ScriptError* serror = nullptr) const override; bool CheckLockTime(const CScriptNum& nLockTime) const override; bool CheckSequence(const CScriptNum& nSequence) const override; + uint256 GetTemplateHash(ScriptExecutionData& execdata) const override; }; using TransactionSignatureChecker = GenericTransactionSignatureChecker; @@ -369,6 +375,11 @@ class DeferringSignatureChecker : public BaseSignatureChecker { return m_checker.CheckSequence(nSequence); } + + uint256 GetTemplateHash(ScriptExecutionData& execdata) const override + { + return m_checker.GetTemplateHash(execdata); + } }; /** Compute the BIP341 tapleaf hash from leaf version & script. */ diff --git a/src/script/script.cpp b/src/script/script.cpp index 4097d13d2c4a..ca474078f5e3 100644 --- a/src/script/script.cpp +++ b/src/script/script.cpp @@ -150,6 +150,9 @@ std::string GetOpName(opcodetype opcode) // Opcode added by BIP 342 (Tapscript) case OP_CHECKSIGADD : return "OP_CHECKSIGADD"; + // Opcode added by BIP446 (Tapscript) + case OP_TEMPLATEHASH : return "OP_TEMPLATEHASH"; + case OP_INVALIDOPCODE : return "OP_INVALIDOPCODE"; } // no default case, so the compiler can warn about missing cases diff --git a/src/script/script.h b/src/script/script.h index e0112cc386e7..329cfb851ffa 100644 --- a/src/script/script.h +++ b/src/script/script.h @@ -210,6 +210,9 @@ enum opcodetype // Opcode added by BIP 342 (Tapscript) OP_CHECKSIGADD = 0xba, + // Opcode added by BIP xx (Tapscript-only, formerly OP_SUCCESS206) + OP_TEMPLATEHASH = 0xce, + OP_INVALIDOPCODE = 0xff, }; diff --git a/src/script/script_error.cpp b/src/script/script_error.cpp index 7fa127420e65..53f6a09ee93b 100644 --- a/src/script/script_error.cpp +++ b/src/script/script_error.cpp @@ -79,6 +79,8 @@ std::string ScriptErrorString(const ScriptError serror) return "OP_SUCCESSx reserved for soft-fork upgrades"; case SCRIPT_ERR_DISCOURAGE_UPGRADABLE_PUBKEYTYPE: return "Public key version reserved for soft-fork upgrades"; + case SCRIPT_ERR_DISCOURAGE_TEMPLATEHASH: + return "Templatehash is not active"; case SCRIPT_ERR_PUBKEYTYPE: return "Public key is neither compressed or uncompressed"; case SCRIPT_ERR_CLEANSTACK: diff --git a/src/script/script_error.h b/src/script/script_error.h index 60ab3e51bfe6..c1ca6e015683 100644 --- a/src/script/script_error.h +++ b/src/script/script_error.h @@ -60,6 +60,7 @@ typedef enum ScriptError_t SCRIPT_ERR_DISCOURAGE_UPGRADABLE_TAPROOT_VERSION, SCRIPT_ERR_DISCOURAGE_OP_SUCCESS, SCRIPT_ERR_DISCOURAGE_UPGRADABLE_PUBKEYTYPE, + SCRIPT_ERR_DISCOURAGE_TEMPLATEHASH, /* segregated witness */ SCRIPT_ERR_WITNESS_PROGRAM_WRONG_LENGTH, diff --git a/src/test/script_tests.cpp b/src/test/script_tests.cpp index 37ca862a8d6d..0aaaf0a8f615 100644 --- a/src/test/script_tests.cpp +++ b/src/test/script_tests.cpp @@ -93,7 +93,8 @@ static ScriptErrorDesc script_errors[]={ {SCRIPT_ERR_TAPSCRIPT_EMPTY_PUBKEY, "TAPSCRIPT_EMPTY_PUBKEY"}, {SCRIPT_ERR_OP_CODESEPARATOR, "OP_CODESEPARATOR"}, {SCRIPT_ERR_SIG_FINDANDDELETE, "SIG_FINDANDDELETE"}, - {SCRIPT_ERR_SCRIPTNUM, "SCRIPTNUM"} + {SCRIPT_ERR_SCRIPTNUM, "SCRIPTNUM"}, + {SCRIPT_ERR_DISCOURAGE_TEMPLATEHASH, "DISCOURAGE_TEMPLATEHASH"}, }; static std::string FormatScriptFlags(script_verify_flags flags) diff --git a/test/functional/feature_taproot.py b/test/functional/feature_taproot.py index eb3bb86ec3d6..2d9f34032dfa 100755 --- a/test/functional/feature_taproot.py +++ b/test/functional/feature_taproot.py @@ -1172,7 +1172,7 @@ def predict_sigops_ratio(n, dummy_size): hashtype = lambda _: random.choice(VALID_SIGHASHES_TAPROOT) for opval in range(76, 0x100): opcode = CScriptOp(opval) - if not is_op_success(opcode): + if not is_op_success(opcode, is_temphash_active=True): continue scripts = [ ("bare_success", CScript([opcode])), @@ -1203,7 +1203,7 @@ def predict_sigops_ratio(n, dummy_size): # Non-OP_SUCCESSx (verify that those aren't accidentally treated as OP_SUCCESSx) for opval in range(0, 0x100): opcode = CScriptOp(opval) - if is_op_success(opcode): + if is_op_success(opcode, is_temphash_active=True): continue scripts = [ ("normal", CScript([OP_RETURN, opcode] + [OP_NOP] * 75)), diff --git a/test/functional/test_framework/script.py b/test/functional/test_framework/script.py index fb168928c3e8..83f4dd271e5d 100644 --- a/test/functional/test_framework/script.py +++ b/test/functional/test_framework/script.py @@ -256,6 +256,9 @@ def __new__(cls, n): # BIP 342 opcodes (Tapscript) OP_CHECKSIGADD = CScriptOp(0xba) +# BIP xx opcode (Tapscript-only, formerly OP_SUCCESS206) +OP_TEMPLATEHASH = CScriptOp(0xce) + OP_INVALIDOPCODE = CScriptOp(0xff) OPCODE_NAMES.update({ @@ -371,6 +374,7 @@ def __new__(cls, n): OP_NOP9: 'OP_NOP9', OP_NOP10: 'OP_NOP10', OP_CHECKSIGADD: 'OP_CHECKSIGADD', + OP_TEMPLATEHASH: 'OP_TEMPLATEHASH', OP_INVALIDOPCODE: 'OP_INVALIDOPCODE', }) @@ -940,5 +944,7 @@ def taproot_construct(pubkey, scripts=None, treat_internal_as_infinity=False): leaves = dict((name, TaprootLeafInfo(script, version, merklebranch, leaf)) for name, version, script, merklebranch, leaf in ret) return TaprootInfo(CScript([OP_1, tweaked]), pubkey, negated + 0, tweak, leaves, h, tweaked) -def is_op_success(o): +def is_op_success(o, is_temphash_active): + if o == OP_TEMPLATEHASH: + return not is_temphash_active return o == 0x50 or o == 0x62 or o == 0x89 or o == 0x8a or o == 0x8d or o == 0x8e or (o >= 0x7e and o <= 0x81) or (o >= 0x83 and o <= 0x86) or (o >= 0x95 and o <= 0x99) or (o >= 0xbb and o <= 0xfe) From af6912ed68f35f484c59660c49445a01a6335882 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Wed, 11 Jun 2025 15:56:14 -0400 Subject: [PATCH 04/12] qa: unit tests to sanity check OP_TEMPLATEHASH Sanity check the template hash by using it to commit to the transaction that must spend an output. Malleating committed fields must lead to a consensus failure, and changing non-committed fields is fine. We also add the option to generate test vectors from this unit test. --- src/test/script_tests.cpp | 364 ++++++++++++++++++++++++++++++++++++++ src/test/util/script.h | 39 ++++ 2 files changed, 403 insertions(+) diff --git a/src/test/script_tests.cpp b/src/test/script_tests.cpp index 0aaaf0a8f615..4429e54d288e 100644 --- a/src/test/script_tests.cpp +++ b/src/test/script_tests.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include