From fbde8fae9db9401e0596e4c7e355d3ba335bd453 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Wed, 9 Sep 2026 15:39:18 +0200 Subject: [PATCH 01/15] validation: include referenced block hashes in the script execution cache key Add PrecomputedTransactionData::m_block_hashes, a table of block hashes (by height) that inputs may commit to in their signature message. It is chain context that cannot be derived from the transaction, so validation fills it in rather than Init(). The script execution cache is keyed on the wtxid and flags only, on the assumption that everything a script check depends on is committed to by the wtxid. A block hash in the signature message breaks that assumption, so include the table in the key. Without this, a transaction verified against one chain would be reported valid from cache after a reorg that replaced the referenced block. The table is always empty until a later commit adds a rule that fills it. Co-authored-by: Claude Fable 5.1 --- src/script/interpreter.h | 5 +++++ src/test/txvalidationcache_tests.cpp | 5 +++++ src/validation.cpp | 8 +++++++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/script/interpreter.h b/src/script/interpreter.h index 47d9bfb09c90..7b35d7e347e5 100644 --- a/src/script/interpreter.h +++ b/src/script/interpreter.h @@ -182,6 +182,11 @@ struct PrecomputedTransactionData //! Whether m_spent_outputs is initialized. bool m_spent_outputs_ready = false; + /** Block hashes (by height, sorted) that inputs of this transaction may commit to in their + * signature message. This is chain context that is not derivable from the transaction, so it + * is filled in by the caller (validation) rather than by Init(). */ + std::vector> m_block_hashes; + PrecomputedTransactionData() = default; /** Initialize this PrecomputedTransactionData with transaction data. diff --git a/src/test/txvalidationcache_tests.cpp b/src/test/txvalidationcache_tests.cpp index 1995c10ed349..634aae3066b3 100644 --- a/src/test/txvalidationcache_tests.cpp +++ b/src/test/txvalidationcache_tests.cpp @@ -152,6 +152,11 @@ static void ValidateCheckInputsForAllFlags(const CTransaction &tx, script_verify std::vector scriptchecks; BOOST_CHECK(CheckInputScripts(tx, state, &active_coins_tip, test_flags, true, add_to_cache, txdata, validation_cache, &scriptchecks)); BOOST_CHECK(scriptchecks.empty()); + // ... but not if the inputs reference a different set of block hashes + PrecomputedTransactionData txdata_block_ref; + txdata_block_ref.m_block_hashes.emplace_back(0, uint256::ONE); + BOOST_CHECK(CheckInputScripts(tx, state, &active_coins_tip, test_flags, true, add_to_cache, txdata_block_ref, validation_cache, &scriptchecks)); + BOOST_CHECK_EQUAL(scriptchecks.size(), tx.vin.size()); } else { // Check that we get script executions to check, if the transaction // was invalid, or we didn't add to cache. diff --git a/src/validation.cpp b/src/validation.cpp index c85a3af7303f..2a6ca43b6981 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -2076,7 +2076,13 @@ bool CheckInputScripts(const CTransaction& tx, TxValidationState& state, // transaction). uint256 hashCacheEntry; CSHA256 hasher = validation_cache.ScriptExecutionCacheHasher(); - hasher.Write(UCharCast(tx.GetWitnessHash().begin()), 32).Write((unsigned char*)&flags, sizeof(flags)).Finalize(hashCacheEntry.begin()); + hasher.Write(UCharCast(tx.GetWitnessHash().begin()), 32).Write((unsigned char*)&flags, sizeof(flags)); + // Block hashes referenced by the inputs are part of the signature message but not of the wtxid, + // so a script execution result is only reusable for the same referenced blocks. + for (const auto& [height, block_hash] : txdata.m_block_hashes) { + hasher.Write(block_hash.begin(), 32); + } + hasher.Finalize(hashCacheEntry.begin()); AssertLockHeld(cs_main); //TODO: Remove this requirement by making CuckooCache not require external locks if (validation_cache.m_script_execution_cache.contains(hashCacheEntry, !cacheFullScriptStore)) { return true; From 16d6842d68fe5bbc46313e0739e53ef9156e41bb Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Wed, 9 Sep 2026 15:39:18 +0200 Subject: [PATCH 02/15] test: witness version and block hash parameters for Taproot helpers taproot_construct() gains a witver parameter so the resulting scriptPubKey can be a witness v2 program, and TaprootSignatureMsg() gains a block_hash parameter that sets ext_flag bit 1 (spend_type bit 2) and appends the hash after the tapscript extension. block_ref_annex() builds the annex used to reference a block by height. Co-authored-by: Claude Fable 5.1 --- test/functional/test_framework/script.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/test/functional/test_framework/script.py b/test/functional/test_framework/script.py index fb168928c3e8..880d4ae8afd3 100644 --- a/test/functional/test_framework/script.py +++ b/test/functional/test_framework/script.py @@ -28,6 +28,7 @@ MAX_PUBKEYS_PER_MULTI_A = 999 LOCKTIME_THRESHOLD = 500000000 ANNEX_TAG = 0x50 +BLOCK_REF_ANNEX_TYPE = 0x01 SEQUENCE_LOCKTIME_DISABLE_FLAG = (1<<31) SEQUENCE_LOCKTIME_TYPE_FLAG = (1<<22) # this means use time (0 means height) @@ -814,7 +815,9 @@ def BIP341_sha_sequences(txTo): def BIP341_sha_outputs(txTo): return sha256(b"".join(o.serialize() for o in txTo.vout)) -def TaprootSignatureMsg(txTo, spent_utxos, hash_type, input_index=0, *, scriptpath=False, leaf_script=None, codeseparator_pos=-1, annex=None, leaf_ver=LEAF_VERSION_TAPSCRIPT): +def TaprootSignatureMsg(txTo, spent_utxos, hash_type, input_index=0, *, scriptpath=False, leaf_script=None, codeseparator_pos=-1, annex=None, leaf_ver=LEAF_VERSION_TAPSCRIPT, block_hash=None): + """Compute the BIP341 signature message. If block_hash is given, the block reference + extension (witness v2, ext_flag bit 1) is included: the message commits to that hash.""" assert_equal(len(txTo.vin), len(spent_utxos)) assert input_index < len(txTo.vin) out_type = SIGHASH_ALL if hash_type == 0 else hash_type & 3 @@ -835,6 +838,8 @@ def TaprootSignatureMsg(txTo, spent_utxos, hash_type, input_index=0, *, scriptpa spend_type |= 1 if scriptpath: spend_type |= 2 + if block_hash is not None: + spend_type |= 4 ss += bytes([spend_type]) if in_type == SIGHASH_ANYONECANPAY: ss += txTo.vin[input_index].prevout.serialize() @@ -854,7 +859,9 @@ def TaprootSignatureMsg(txTo, spent_utxos, hash_type, input_index=0, *, scriptpa ss += TaggedHash("TapLeaf", bytes([leaf_ver]) + ser_string(leaf_script)) ss += bytes([0]) ss += codeseparator_pos.to_bytes(4, "little", signed=False) - assert_equal(len(ss), 175 - (in_type == SIGHASH_ANYONECANPAY) * 49 - (out_type != SIGHASH_ALL and out_type != SIGHASH_SINGLE) * 32 + (annex is not None) * 32 + scriptpath * 37) + if block_hash is not None: + ss += block_hash + assert_equal(len(ss), 175 - (in_type == SIGHASH_ANYONECANPAY) * 49 - (out_type != SIGHASH_ALL and out_type != SIGHASH_SINGLE) * 32 + (annex is not None) * 32 + scriptpath * 37 + (block_hash is not None) * 32) return ss def TaprootSignatureHash(*args, **kwargs): @@ -914,10 +921,15 @@ def taproot_tree_helper(scripts): # - merklebranch: the merkle branch to use for this leaf (32*N bytes) TaprootLeafInfo = namedtuple("TaprootLeafInfo", "script,version,merklebranch,leaf_hash") -def taproot_construct(pubkey, scripts=None, treat_internal_as_infinity=False): +def block_ref_annex(height): + """Annex committing to the block at the given height (witness v2 block reference).""" + return bytes([ANNEX_TAG, BLOCK_REF_ANNEX_TYPE]) + height.to_bytes(4, "little") + +def taproot_construct(pubkey, scripts=None, treat_internal_as_infinity=False, witver=1): """Construct a tree of Taproot spending conditions pubkey: a 32-byte xonly pubkey for the internal pubkey (bytes) + witver: witness version of the resulting scriptPubKey (1 for BIP341, 2 for v2 with block references) scripts: a list of items; each item is either: - a (name, CScript or bytes, leaf version) tuple - a (name, CScript or bytes) tuple (defaulting to leaf version 0xc0) @@ -938,7 +950,7 @@ def taproot_construct(pubkey, scripts=None, treat_internal_as_infinity=False): else: tweaked, negated = tweak_add_pubkey(pubkey, tweak) 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) + return TaprootInfo(CScript([CScriptOp.encode_op_n(witver), tweaked]), pubkey, negated + 0, tweak, leaves, h, tweaked) def is_op_success(o): 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 e7f262fa4f8381c2e721eabd72069f0a87abb719 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Wed, 9 Sep 2026 15:49:38 +0200 Subject: [PATCH 03/15] consensus: witness v2 Taproot outputs Introduce SCRIPT_VERIFY_TAPROOT_V2 and validate 32-byte witness v2 programs under the BIP341/342 rules, reusing the v1 code path. This is the host for block references: a rule that changes the signature message cannot be added to existing v1 outputs as a soft fork, so it needs a witness version that old nodes treat as anyone-can-spend. Activation is a buried deployment. It is never active on mainnet, testnet and signet, always active on regtest, and can be moved with -testactivationheight=taproot_v2@height. Witness v2 spends stay non-standard for now, so the functional test mines them directly. Co-authored-by: Claude Fable 5.1 --- src/chainparamsbase.cpp | 2 +- src/consensus/params.h | 7 +- src/deploymentinfo.cpp | 4 + src/kernel/chainparams.cpp | 9 ++ src/rpc/blockchain.cpp | 1 + src/script/interpreter.cpp | 14 ++- src/script/interpreter.h | 5 + src/validation.cpp | 5 + test/functional/feature_block_reference.py | 132 +++++++++++++++++++++ test/functional/p2p_segwit.py | 9 +- test/functional/rpc_blockchain.py | 3 +- test/functional/test_runner.py | 1 + 12 files changed, 179 insertions(+), 13 deletions(-) create mode 100755 test/functional/feature_block_reference.py diff --git a/src/chainparamsbase.cpp b/src/chainparamsbase.cpp index f62e455fac45..526614443d90 100644 --- a/src/chainparamsbase.cpp +++ b/src/chainparamsbase.cpp @@ -16,7 +16,7 @@ void SetupChainParamsBaseOptions(ArgsManager& argsman) argsman.AddArg("-chain=", "Use the chain (default: main). Allowed values: " LIST_CHAIN_NAMES, ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS); argsman.AddArg("-regtest", "Enter regression test mode, which uses a special chain in which blocks can be solved instantly. " "This is intended for regression testing tools and app development. Equivalent to -chain=regtest.", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); - argsman.AddArg("-testactivationheight=name@height.", "Set the activation height of 'name' (segwit, bip34, dersig, cltv, csv). (test-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); + argsman.AddArg("-testactivationheight=name@height.", "Set the activation height of 'name' (segwit, bip34, dersig, cltv, csv, taproot_v2). (test-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST); argsman.AddArg("-testnet", "Use the testnet3 chain. Equivalent to -chain=test. Support for testnet3 is deprecated and will be removed in an upcoming release. Consider moving to testnet4 now by using -testnet4.", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS); argsman.AddArg("-testnet4", "Use the testnet4 chain. Equivalent to -chain=testnet4.", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS); argsman.AddArg("-vbparams=deployment:start:end[:min_activation_height]", "Use given start/end times and min_activation_height for specified version bits deployment (test-only)", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CHAINPARAMS); diff --git a/src/consensus/params.h b/src/consensus/params.h index 99096e06d9a8..9f98e9276bf4 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -32,8 +32,9 @@ enum BuriedDeployment : int16_t { // SCRIPT_VERIFY_WITNESS is enforced from genesis, but the check for downloading // missing witness data is not. BIP 147 also relies on hardcoded activation height. DEPLOYMENT_SEGWIT, + DEPLOYMENT_TAPROOT_V2, }; -constexpr bool ValidDeployment(BuriedDeployment dep) { return dep <= DEPLOYMENT_SEGWIT; } +constexpr bool ValidDeployment(BuriedDeployment dep) { return dep <= DEPLOYMENT_TAPROOT_V2; } enum DeploymentPos : uint16_t { DEPLOYMENT_TESTDUMMY, @@ -108,6 +109,8 @@ struct Params { * Note that segwit v0 script rules are enforced on all blocks except the * BIP 16 exception blocks. */ int SegwitHeight; + /** Block height at which witness v2 Taproot (with block references) becomes active */ + int TaprootV2Height; /** Don't warn about unknown BIP 9 activations below this height. * This prevents us from warning about the CSV, segwit and taproot activations. */ int MinBIP9WarningHeight; @@ -153,6 +156,8 @@ struct Params { return CSVHeight; case DEPLOYMENT_SEGWIT: return SegwitHeight; + case DEPLOYMENT_TAPROOT_V2: + return TaprootV2Height; } // no default case, so the compiler can warn about missing cases return std::numeric_limits::max(); } diff --git a/src/deploymentinfo.cpp b/src/deploymentinfo.cpp index 551982b23ae7..8f88b9961b4a 100644 --- a/src/deploymentinfo.cpp +++ b/src/deploymentinfo.cpp @@ -29,6 +29,8 @@ std::string DeploymentName(Consensus::BuriedDeployment dep) return "csv"; case Consensus::DEPLOYMENT_SEGWIT: return "segwit"; + case Consensus::DEPLOYMENT_TAPROOT_V2: + return "taproot_v2"; } // no default case, so the compiler can warn about missing cases return ""; } @@ -45,6 +47,8 @@ std::optional GetBuriedDeployment(const std::string return Consensus::BuriedDeployment::DEPLOYMENT_CLTV; } else if (name == "csv") { return Consensus::BuriedDeployment::DEPLOYMENT_CSV; + } else if (name == "taproot_v2") { + return Consensus::BuriedDeployment::DEPLOYMENT_TAPROOT_V2; } return std::nullopt; } diff --git a/src/kernel/chainparams.cpp b/src/kernel/chainparams.cpp index 9764ba95c807..82b504c4d619 100644 --- a/src/kernel/chainparams.cpp +++ b/src/kernel/chainparams.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -92,6 +93,9 @@ void CChainParams::ApplyDeploymentOptions(const DeploymentOptions& opts) case Consensus::BuriedDeployment::DEPLOYMENT_CSV: consensus.CSVHeight = int{height}; break; + case Consensus::BuriedDeployment::DEPLOYMENT_TAPROOT_V2: + consensus.TaprootV2Height = int{height}; + break; } } @@ -122,6 +126,7 @@ class CMainParams : public CChainParams { consensus.BIP66Height = 363725; // 00000000000000000379eaa19dce8c9b722d46ae6a57c2f1a988119488b50931 consensus.CSVHeight = 419328; // 000000000000000004a1b34462cb8aeebd5799177f7a29cf28f2d1961716b5b5 consensus.SegwitHeight = 481824; // 0000000000000000001c8018d9cb3b742ef25114f27563e3fc4a1902167f9893 + consensus.TaprootV2Height = std::numeric_limits::max(); // Not deployed consensus.MinBIP9WarningHeight = 711648; // taproot activation height + miner confirmation window consensus.powLimit = uint256{"00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}; consensus.nPowTargetTimespan = 14 * 24 * 60 * 60; // two weeks @@ -246,6 +251,7 @@ class CTestNetParams : public CChainParams { consensus.BIP66Height = 330776; // 000000002104c8c45e99a8853285a3b592602a3ccde2b832481da85e9e4ba182 consensus.CSVHeight = 770112; // 00000000025e930139bac5c6c31a403776da130831ab85be56578f3fa75369bb consensus.SegwitHeight = 834624; // 00000000002b980fcd729daaa248fd9316a5200e9b367f4ff2c42453e84201ca + consensus.TaprootV2Height = std::numeric_limits::max(); // Not deployed consensus.MinBIP9WarningHeight = 2013984; // taproot activation height + miner confirmation window consensus.powLimit = uint256{"00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}; consensus.nPowTargetTimespan = 14 * 24 * 60 * 60; // two weeks @@ -346,6 +352,7 @@ class CTestNet4Params : public CChainParams { consensus.BIP66Height = 1; consensus.CSVHeight = 1; consensus.SegwitHeight = 1; + consensus.TaprootV2Height = std::numeric_limits::max(); // Not deployed consensus.MinBIP9WarningHeight = 0; consensus.powLimit = uint256{"00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}; consensus.nPowTargetTimespan = 14 * 24 * 60 * 60; // two weeks @@ -492,6 +499,7 @@ class SigNetParams : public CChainParams { consensus.BIP66Height = 1; consensus.CSVHeight = 1; consensus.SegwitHeight = 1; + consensus.TaprootV2Height = std::numeric_limits::max(); // Not deployed consensus.nPowTargetTimespan = 14 * 24 * 60 * 60; // two weeks consensus.nPowTargetSpacing = 10 * 60; consensus.fPowAllowMinDifficultyBlocks = false; @@ -575,6 +583,7 @@ class CRegTestParams : public CChainParams consensus.BIP66Height = 1; // Always active unless overridden consensus.CSVHeight = 1; // Always active unless overridden consensus.SegwitHeight = 0; // Always active unless overridden + consensus.TaprootV2Height = 0; // Always active unless overridden consensus.MinBIP9WarningHeight = 0; consensus.powLimit = uint256{"7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}; consensus.nPowTargetTimespan = 24 * 60 * 60; // one day diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 6677cdeaf3ba..070f65e35b03 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1507,6 +1507,7 @@ UniValue DeploymentInfo(const CBlockIndex* blockindex, const ChainstateManager& SoftForkDescPushBack(blockindex, softforks, chainman, Consensus::DEPLOYMENT_CLTV); SoftForkDescPushBack(blockindex, softforks, chainman, Consensus::DEPLOYMENT_CSV); SoftForkDescPushBack(blockindex, softforks, chainman, Consensus::DEPLOYMENT_SEGWIT); + SoftForkDescPushBack(blockindex, softforks, chainman, Consensus::DEPLOYMENT_TAPROOT_V2); SoftForkDescPushBack(blockindex, softforks, chainman, Consensus::DEPLOYMENT_TESTDUMMY); return softforks; } diff --git a/src/script/interpreter.cpp b/src/script/interpreter.cpp index 98b16eca6bcd..949a1432cf05 100644 --- a/src/script/interpreter.cpp +++ b/src/script/interpreter.cpp @@ -1426,9 +1426,9 @@ void PrecomputedTransactionData::Init(const T& txTo, std::vector&& spent for (size_t inpos = 0; inpos < txTo.vin.size() && !(uses_bip143_segwit && uses_bip341_taproot); ++inpos) { if (!txTo.vin[inpos].scriptWitness.IsNull()) { if (m_spent_outputs_ready && m_spent_outputs[inpos].scriptPubKey.size() == 2 + WITNESS_V1_TAPROOT_SIZE && - m_spent_outputs[inpos].scriptPubKey[0] == OP_1) { - // Treat every witness-bearing spend with 34-byte scriptPubKey that starts with OP_1 as a Taproot - // spend. This only works if spent_outputs was provided as well, but if it wasn't, actual validation + (m_spent_outputs[inpos].scriptPubKey[0] == OP_1 || m_spent_outputs[inpos].scriptPubKey[0] == OP_2)) { + // Treat every witness-bearing spend with 34-byte scriptPubKey that starts with OP_1 or OP_2 as a + // Taproot (v1 or v2) spend. This only works if spent_outputs was provided as well, but if it wasn't, actual validation // will fail anyway. Note that this branch may trigger for scriptPubKeys that aren't actually segwit // but in that case validation will fail as SCRIPT_ERR_WITNESS_UNEXPECTED anyway. uses_bip341_taproot = true; @@ -1954,9 +1954,10 @@ static bool VerifyWitnessProgram(const CScriptWitness& witness, int witversion, } else { return set_error(serror, SCRIPT_ERR_WITNESS_PROGRAM_WRONG_LENGTH); } - } else if (witversion == 1 && program.size() == WITNESS_V1_TAPROOT_SIZE && !is_p2sh) { - // BIP341 Taproot: 32-byte non-P2SH witness v1 program (which encodes a P2C-tweaked pubkey) - if (!(flags & SCRIPT_VERIFY_TAPROOT)) return set_success(serror); + } else if ((witversion == 1 || (witversion == 2 && (flags & SCRIPT_VERIFY_TAPROOT_V2))) && program.size() == WITNESS_V1_TAPROOT_SIZE && !is_p2sh) { + // BIP341 Taproot: 32-byte non-P2SH witness v1 program (which encodes a P2C-tweaked pubkey). + // Witness v2 programs of the same size follow the same rules (see below for the difference). + if (witversion == 1 && !(flags & SCRIPT_VERIFY_TAPROOT)) return set_success(serror); if (stack.size() == 0) return set_error(serror, SCRIPT_ERR_WITNESS_PROGRAM_WITNESS_EMPTY); if (stack.size() >= 2 && !stack.back().empty() && stack.back()[0] == ANNEX_TAG) { // Drop annex (this is non-standard; see IsWitnessStandard) @@ -2200,6 +2201,7 @@ const std::map& ScriptFlagNamesToEnum() FLAG_NAME(DISCOURAGE_UPGRADABLE_PUBKEYTYPE), FLAG_NAME(DISCOURAGE_OP_SUCCESS), FLAG_NAME(DISCOURAGE_UPGRADABLE_TAPROOT_VERSION), + FLAG_NAME(TAPROOT_V2), }; #undef FLAG_NAME return g_names_to_enum; diff --git a/src/script/interpreter.h b/src/script/interpreter.h index 7b35d7e347e5..0df59b0d6412 100644 --- a/src/script/interpreter.h +++ b/src/script/interpreter.h @@ -146,6 +146,10 @@ enum class script_verify_flag_name : uint8_t { // Making unknown public key versions (in BIP 342 scripts) non-standard SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_PUBKEYTYPE, + // Witness v2 Taproot validation (BIP341/342 rules, plus block references) + // + SCRIPT_VERIFY_TAPROOT_V2, + // Constants to point to the highest flag in use. Add new flags above this line. // SCRIPT_VERIFY_END_MARKER @@ -243,6 +247,7 @@ struct ScriptExecutionData inline constexpr size_t WITNESS_V0_SCRIPTHASH_SIZE = 32; inline constexpr size_t WITNESS_V0_KEYHASH_SIZE = 20; inline constexpr size_t WITNESS_V1_TAPROOT_SIZE = 32; +inline constexpr size_t WITNESS_V2_TAPROOT_SIZE = 32; inline constexpr uint8_t TAPROOT_LEAF_MASK = 0xfe; inline constexpr uint8_t TAPROOT_LEAF_TAPSCRIPT = 0xc0; diff --git a/src/validation.cpp b/src/validation.cpp index 2a6ca43b6981..22aa9c2b53eb 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -2290,6 +2290,11 @@ script_verify_flags GetBlockScriptFlags(const CBlockIndex& block_index, const Ch flags |= SCRIPT_VERIFY_NULLDUMMY; } + // Enforce witness v2 Taproot (with block references) + if (DeploymentActiveAt(block_index, chainman, Consensus::DEPLOYMENT_TAPROOT_V2)) { + flags |= SCRIPT_VERIFY_TAPROOT_V2; + } + return flags; } diff --git a/test/functional/feature_block_reference.py b/test/functional/feature_block_reference.py new file mode 100755 index 000000000000..8612216153c7 --- /dev/null +++ b/test/functional/feature_block_reference.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026-present The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test witness v2 Taproot outputs (BIP341/342 semantics plus block references). + +A witness v2 output is spent exactly like a v1 Taproot output. In addition, an +input may carry an annex with a block reference; the signature message then +commits to the hash of the referenced block (see doc/block-reference.md). +""" + +from test_framework.blocktools import ( + COINBASE_MATURITY, + add_witness_commitment, + create_block, + create_coinbase, +) +from test_framework.key import ( + ECKey, + compute_xonly_pubkey, + sign_schnorr, + tweak_add_privkey, +) +from test_framework.messages import ( + COutPoint, + CTransaction, + CTxIn, + CTxInWitness, + CTxOut, +) +from test_framework.script import ( + CScript, + OP_CHECKSIG, + TaprootSignatureHash, + block_ref_annex, + taproot_construct, +) +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, +) +from test_framework.wallet import MiniWallet + +FEE = 1000 +AMOUNT = 100_000 + + +class V2Coin: + """A witness v2 output funded from the MiniWallet, with everything needed to spend it.""" + + def __init__(self, test, node, amount=AMOUNT): + self.node = node + self.key = ECKey() + self.key.generate() + self.seckey = self.key.get_bytes() + self.pubkey = compute_xonly_pubkey(self.seckey)[0] + self.leaf_script = CScript([self.pubkey, OP_CHECKSIG]) + self.tap = taproot_construct(self.pubkey, [("leaf", self.leaf_script)], witver=2) + self.amount = amount + funding = test.wallet.send_to(from_node=node, scriptPubKey=self.tap.scriptPubKey, amount=amount) + test.generate(node, 1) + self.outpoint = COutPoint(int(funding["txid"], 16), funding["sent_vout"]) + self.utxo = CTxOut(amount, self.tap.scriptPubKey) + + def spend(self, test, *, scriptpath=False, ref_height=None, block_hash=None, annex=None): + """Build a transaction spending this coin back to the MiniWallet. + + ref_height: put a block reference annex for this height in the witness. + block_hash: the hash committed to by the signature (defaults to the hash + at ref_height on self.node); pass a different one to sign + for another chain. + annex: raw annex to use instead of the block reference one. + """ + tx = CTransaction() + tx.vin = [CTxIn(self.outpoint)] + tx.vout = [CTxOut(self.amount - FEE, test.wallet.get_output_script())] + if ref_height is not None: + annex = block_ref_annex(ref_height) + if block_hash is None: + block_hash = bytes.fromhex(self.node.getblockhash(ref_height))[::-1] + if scriptpath: + leaf = self.tap.leaves["leaf"] + sighash = TaprootSignatureHash(tx, [self.utxo], 0, scriptpath=True, leaf_script=self.leaf_script, codeseparator_pos=0xFFFFFFFF, annex=annex, block_hash=block_hash) + control = bytes([leaf.version + self.tap.negflag]) + self.tap.internal_pubkey + leaf.merklebranch + stack = [sign_schnorr(self.seckey, sighash), self.leaf_script, control] + else: + sighash = TaprootSignatureHash(tx, [self.utxo], 0, annex=annex, block_hash=block_hash) + stack = [sign_schnorr(tweak_add_privkey(self.seckey, self.tap.tweak), sighash)] + if annex is not None: + stack.append(annex) + tx.wit.vtxinwit = [CTxInWitness()] + tx.wit.vtxinwit[0].scriptWitness.stack = stack + return tx + + +class BlockReferenceTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 2 + + def submit_block(self, node, txs): + """Mine a block containing txs on top of node's tip; return submitblock's result (None if accepted).""" + height = node.getblockcount() + 1 + block = create_block(int(node.getbestblockhash(), 16), create_coinbase(height), txlist=txs, ntime=node.getblock(node.getbestblockhash())["time"] + 1) + add_witness_commitment(block) + block.solve() + result = node.submitblock(block.serialize().hex()) + if result is None: + self.sync_blocks() + return result + + def run_test(self): + self.wallet = MiniWallet(self.nodes[0]) + self.test_v2_spends() + + def test_v2_spends(self): + self.log.info("Witness v2 outputs spend like Taproot") + node = self.nodes[0] + for scriptpath in (False, True): + coin = V2Coin(self, node) + tx = coin.spend(self, scriptpath=scriptpath) + # Not standard yet, so mine it directly + assert_equal(self.submit_block(node, [tx]), None) + assert_equal(node.gettxout(tx.txid_hex, 0)["confirmations"], 1) + # An invalid signature is rejected (v2 is not anyone-can-spend) + bad = V2Coin(self, node).spend(self, scriptpath=scriptpath) + bad.wit.vtxinwit[0].scriptWitness.stack[0] = bytes(64) + assert_equal(self.submit_block(node, [bad]), "block-script-verify-flag-failed (Invalid Schnorr signature)") + + +if __name__ == '__main__': + BlockReferenceTest(__file__).main() diff --git a/test/functional/p2p_segwit.py b/test/functional/p2p_segwit.py index 41e6764fc09f..fe0b08b7bdc4 100755 --- a/test/functional/p2p_segwit.py +++ b/test/functional/p2p_segwit.py @@ -54,6 +54,7 @@ OP_0, OP_1, OP_2, + OP_3, OP_16, OP_2DROP, OP_CHECKMULTISIG, @@ -1349,8 +1350,8 @@ def test_segwit_versions(self): assert_equal(len(self.nodes[1].getrawmempool()), 0) for version in list(range(OP_1, OP_16 + 1)) + [OP_0]: # First try to spend to a future version segwit script_pubkey. - if version == OP_1: - # Don't use 32-byte v1 witness (used by Taproot; see BIP 341) + if version in (OP_1, OP_2): + # Don't use 32-byte v1/v2 witness (used by Taproot; see BIP 341) script_pubkey = CScript([CScriptOp(version), witness_hash + b'\x00']) else: script_pubkey = CScript([CScriptOp(version), witness_hash]) @@ -1364,9 +1365,9 @@ def test_segwit_versions(self): self.generate(self.nodes[0], 1) # Mine all the transactions assert_equal(len(self.nodes[0].getrawmempool()), 0) - # Finally, verify that version 0 -> version 2 transactions + # Finally, verify that version 0 -> version 3 transactions # are standard - script_pubkey = CScript([CScriptOp(OP_2), witness_hash]) + script_pubkey = CScript([CScriptOp(OP_3), witness_hash]) tx2 = CTransaction() tx2.vin = [CTxIn(COutPoint(tx.txid_int, 0), b"")] tx2.vout = [CTxOut(tx.vout[0].nValue - 1000, script_pubkey)] diff --git a/test/functional/rpc_blockchain.py b/test/functional/rpc_blockchain.py index d731907fd8ca..979fda1e3fc7 100755 --- a/test/functional/rpc_blockchain.py +++ b/test/functional/rpc_blockchain.py @@ -216,13 +216,14 @@ 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","TAPROOT_V2","WITNESS"], "deployments": { 'bip34': {'type': 'buried', 'active': True, 'height': 2}, 'bip66': {'type': 'buried', 'active': True, 'height': 3}, 'bip65': {'type': 'buried', 'active': True, 'height': 4}, 'csv': {'type': 'buried', 'active': True, 'height': 5}, 'segwit': {'type': 'buried', 'active': True, 'height': 6}, + 'taproot_v2': {'type': 'buried', 'active': True, 'height': 0}, 'testdummy': { 'type': 'bip9', 'bip9': { diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 6a9c2ec3c62e..1c157841576d 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -104,6 +104,7 @@ # vv Tests less than 5m vv 'feature_fee_estimation.py', 'feature_taproot.py', + 'feature_block_reference.py', 'feature_block.py', 'mempool_ephemeral_dust.py', 'wallet_conflicts.py', From fbabd3e244af4571112f2f30b3299b2e36e7a631 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Wed, 9 Sep 2026 15:54:09 +0200 Subject: [PATCH 04/15] policy: make witness v2 Taproot spends standard Add SCRIPT_VERIFY_TAPROOT_V2 to the standard script flags, apply the Taproot witness limits to v2 spends in IsWitnessStandard, and teach the solver about the new output type so AreInputsStandard no longer rejects the input as an undefined witness program. Annexes stay non-standard. The output type is wired through the switch statements that need it; signing support comes with the wallet changes later. Co-authored-by: Claude Fable 5.1 --- src/addresstype.cpp | 4 ++++ src/policy/policy.cpp | 4 ++-- src/policy/policy.h | 3 ++- src/rpc/rawtransaction.cpp | 2 ++ src/script/sign.cpp | 1 + src/script/solver.cpp | 5 +++++ src/script/solver.h | 1 + src/test/script_standard_tests.cpp | 7 +++++++ src/test/transaction_tests.cpp | 2 +- src/wallet/scriptpubkeyman.cpp | 1 + src/wallet/spend.cpp | 1 + test/functional/feature_block_reference.py | 8 ++++++-- test/functional/feature_taproot.py | 4 ++-- 13 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/addresstype.cpp b/src/addresstype.cpp index 410879b40d60..e714852e5095 100644 --- a/src/addresstype.cpp +++ b/src/addresstype.cpp @@ -91,6 +91,10 @@ bool ExtractDestination(const CScript& scriptPubKey, CTxDestination& addressRet) addressRet = PayToAnchor(); return true; } + case TxoutType::WITNESS_V2_TAPROOT: { + addressRet = WitnessUnknown{2, vSolutions[0]}; + return true; + } case TxoutType::WITNESS_UNKNOWN: { addressRet = WitnessUnknown{vSolutions[0][0], vSolutions[1]}; return true; diff --git a/src/policy/policy.cpp b/src/policy/policy.cpp index 83ceb63cbcd7..a83d388d9a41 100644 --- a/src/policy/policy.cpp +++ b/src/policy/policy.cpp @@ -321,8 +321,8 @@ bool IsWitnessStandard(const CTransaction& tx, const CCoinsViewCache& mapInputs) // Check policy limits for Taproot spends: // - MAX_STANDARD_TAPSCRIPT_STACK_ITEM_SIZE limit for stack item size // - No annexes - if (witnessversion == 1 && witnessprogram.size() == WITNESS_V1_TAPROOT_SIZE && !p2sh) { - // Taproot spend (non-P2SH-wrapped, version 1, witness program size 32; see BIP 341) + if ((witnessversion == 1 || witnessversion == 2) && witnessprogram.size() == WITNESS_V1_TAPROOT_SIZE && !p2sh) { + // Taproot spend (non-P2SH-wrapped, version 1 or 2, witness program size 32; see BIP 341) std::span stack{tx.vin[i].scriptWitness.stack}; if (stack.size() >= 2 && !stack.back().empty() && stack.back()[0] == ANNEX_TAG) { // Annexes are nonstandard as long as no semantics are defined for them. diff --git a/src/policy/policy.h b/src/policy/policy.h index ea66fa4b84e9..ca15916efef0 100644 --- a/src/policy/policy.h +++ b/src/policy/policy.h @@ -107,7 +107,8 @@ inline 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_TAPROOT_V2}; /** * Standard script verification flags that standard transactions will comply diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index e451305a3613..d857d584f7e8 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -508,6 +508,7 @@ static RPCMethod decodescript() case TxoutType::SCRIPTHASH: case TxoutType::WITNESS_UNKNOWN: case TxoutType::WITNESS_V1_TAPROOT: + case TxoutType::WITNESS_V2_TAPROOT: case TxoutType::ANCHOR: // Should not be wrapped return false; @@ -551,6 +552,7 @@ static RPCMethod decodescript() case TxoutType::WITNESS_V0_KEYHASH: case TxoutType::WITNESS_V0_SCRIPTHASH: case TxoutType::WITNESS_V1_TAPROOT: + case TxoutType::WITNESS_V2_TAPROOT: case TxoutType::ANCHOR: // Should not be wrapped return false; diff --git a/src/script/sign.cpp b/src/script/sign.cpp index efee5a35fe01..6caeb63e6e44 100644 --- a/src/script/sign.cpp +++ b/src/script/sign.cpp @@ -655,6 +655,7 @@ static bool SignStep(const SigningProvider& provider, const BaseSignatureCreator case TxoutType::NONSTANDARD: case TxoutType::NULL_DATA: case TxoutType::WITNESS_UNKNOWN: + case TxoutType::WITNESS_V2_TAPROOT: // TODO: signing support return false; case TxoutType::PUBKEY: if (!CreateSig(creator, sigdata, provider, sig, CPubKey(vSolutions[0]), scriptPubKey, sigversion)) return false; diff --git a/src/script/solver.cpp b/src/script/solver.cpp index e99f1f099c92..58dfbe88f49c 100644 --- a/src/script/solver.cpp +++ b/src/script/solver.cpp @@ -28,6 +28,7 @@ std::string GetTxnOutputType(TxoutType t) case TxoutType::WITNESS_V0_KEYHASH: return "witness_v0_keyhash"; case TxoutType::WITNESS_V0_SCRIPTHASH: return "witness_v0_scripthash"; case TxoutType::WITNESS_V1_TAPROOT: return "witness_v1_taproot"; + case TxoutType::WITNESS_V2_TAPROOT: return "witness_v2_taproot"; case TxoutType::WITNESS_UNKNOWN: return "witness_unknown"; } // no default case, so the compiler can warn about missing cases assert(false); @@ -166,6 +167,10 @@ TxoutType Solver(const CScript& scriptPubKey, std::vector Date: Wed, 9 Sep 2026 15:59:06 +0200 Subject: [PATCH 05/15] consensus: block references for witness v2 spends A witness v2 spend whose annex payload starts with type byte 0x01 references the block at the 4-byte little endian height that follows. Two rules attach to the reference (see doc/block-reference.md): - The block containing the spend must be at least COINBASE_MATURITY blocks after the referenced block. This is checked at transaction level in ConnectBlock and in mempool acceptance, so it also holds under assumevalid, and it makes a referencing transaction exactly as reorg-safe as a spend of a matured coinbase. - Every signature on the input commits to the hash of the referenced block: ext_flag bit 1 is set in the BIP341 message and the hash is appended after the BIP342 extension. A signature made for one chain does not verify on any chain whose block at that height differs. The script interpreter cannot look up block hashes itself, so validation resolves them from the chain and passes them in via PrecomputedTransactionData::m_block_hashes before the script checks run. Bytes after the height and annexes with other type bytes keep their BIP341 treatment and are ignored. A block reference annex is still non-standard, so the functional test exercises the rules through block submission. Co-authored-by: Claude Fable 5.1 --- doc/block-reference.md | 80 ++++++++++++++++++++++ src/consensus/tx_verify.cpp | 20 ++++++ src/consensus/tx_verify.h | 8 +++ src/script/interpreter.cpp | 26 +++++++ src/script/interpreter.h | 13 ++++ src/script/script_error.cpp | 2 + src/script/script_error.h | 1 + src/test/script_tests.cpp | 1 + src/validation.cpp | 42 ++++++++++++ test/functional/feature_block_reference.py | 29 ++++++++ 10 files changed, 222 insertions(+) create mode 100644 doc/block-reference.md diff --git a/doc/block-reference.md b/doc/block-reference.md new file mode 100644 index 000000000000..958f84828766 --- /dev/null +++ b/doc/block-reference.md @@ -0,0 +1,80 @@ +# Witness v2 Taproot with block references + +This document specifies the consensus rules for witness version 2 outputs +with a 32-byte program, as implemented in this prototype. Activation +parameters are out of scope; on regtest the rules are always active. + +## Motivation + +A transaction that is valid on both sides of a chain split can be replayed. +Holders who want to spend on one side only currently need a coin whose +history already differs between the two chains, such as a coinbase +descendant. A block reference lets any spender opt in to validity on one +chain, at the cost of a few witness bytes, with no coordination with anyone +else. + +## Rules + +A witness v2 output with a 32-byte program is spent under the +[BIP341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki) and +[BIP342](https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki) +rules, with one addition. + +### Block reference annex + +If the spend carries an annex and the first byte after the `0x50` annex +marker is `0x01`, the annex is a *block reference*: + + 0x50 || 0x01 || height (4 bytes, little endian) || further bytes (ignored) + +An annex shorter than 6 bytes whose second byte is `0x01` makes the spend +invalid. Bytes after the height have no consensus meaning, exactly like the +rest of the annex today. Annexes whose second byte is not `0x01` keep their +BIP341 treatment: no meaning, always valid. + +The type byte `0x01` means "this input references the block at `height`". +This document attaches two effects to the reference; future rules may attach +more. + +### Maturity and activation + +A block containing the transaction at height `H` is invalid unless +`height + 100 <= H` for every block reference in the transaction. The +constant is `COINBASE_MATURITY`. As a consequence the referenced block is at +least 100 deep, and a transaction with a block reference is exactly as safe +against reorganisation as a spend of a matured coinbase output. + +A reference to a block below the activation height of these rules is +invalid. Before activation, witness v2 spends are anyone-can-spend and an +annex has no meaning, exactly as for any other undefined witness version; +neither rule in this document applies to blocks before activation. (The +mempool applies the witness v2 rules as policy regardless.) + +### Signature message + +Every signature checked for an input with a block reference, on the key +path or in tapscript, is over the BIP341 message with `ext_flag` bit 1 set +(so `spend_type` gains the value 4) and the 32-byte hash of the block at +`height` in the active chain appended after the message, after the BIP342 +extension if present. + +Since the hash of the referenced block is part of the message and not of +the transaction, a signature made for one chain does not verify on any chain +whose block at that height differs, nor on a chain that computes the plain +BIP341 message. An input without a block reference is signed exactly as a +witness v1 input and is valid on every chain. + +## Non-consensus considerations + +- The annex is committed to by every signature on the input, so a third + party cannot add, remove, or alter a block reference on a signed input. + A script-path spend that checks no signature can carry a reference, but + it then only imposes the maturity rule. +- A transaction with a block reference never expires. It becomes invalid + only if a reorganisation removes the referenced block, which requires a + reorganisation of at least 100 blocks. +- Wallets that want a spend to be specific to the current chain should + reference the most recent eligible block, that is the block at + `tip - 99`. +- Policy: a block reference annex of exactly 6 bytes is standard for v2 + spends. All other annexes remain non-standard. diff --git a/src/consensus/tx_verify.cpp b/src/consensus/tx_verify.cpp index e580a9d2947b..7841c8d33641 100644 --- a/src/consensus/tx_verify.cpp +++ b/src/consensus/tx_verify.cpp @@ -18,6 +18,8 @@ #include #include +#include +#include #include bool IsFinalTx(const CTransaction &tx, int nBlockHeight, int64_t nBlockTime) @@ -167,6 +169,24 @@ int64_t GetTransactionSigOpCost(const CTransaction& tx, const CCoinsViewCache& i return nSigOps; } +std::vector Consensus::GetBlockReferences(const CTransaction& tx, const CCoinsViewCache& inputs) +{ + std::vector heights; + for (const CTxIn& txin : tx.vin) { + const auto& stack{txin.scriptWitness.stack}; + if (stack.size() < 2 || stack.back().size() < BLOCK_REF_ANNEX_SIZE || stack.back()[0] != ANNEX_TAG) continue; + int witnessversion; + std::vector witnessprogram; + const CScript& spk{inputs.AccessCoin(txin.prevout).out.scriptPubKey}; + if (!spk.IsWitnessProgram(witnessversion, witnessprogram) || witnessversion != 2 || witnessprogram.size() != WITNESS_V2_TAPROOT_SIZE) continue; + std::optional height; + if (ParseBlockReference(stack.back(), height) && height) heights.push_back(*height); + } + std::ranges::sort(heights); + heights.erase(std::ranges::unique(heights).begin(), heights.end()); + return heights; +} + bool Consensus::CheckTxInputs(const CTransaction& tx, TxValidationState& state, const CCoinsViewCache& inputs, int nSpendHeight, CAmount& txfee) { // are the actual inputs available? diff --git a/src/consensus/tx_verify.h b/src/consensus/tx_verify.h index 76faeaeaea8e..505a196e0dc3 100644 --- a/src/consensus/tx_verify.h +++ b/src/consensus/tx_verify.h @@ -27,6 +27,14 @@ namespace Consensus { * Preconditions: tx.IsCoinBase() is false. */ [[nodiscard]] bool CheckTxInputs(const CTransaction& tx, TxValidationState& state, const CCoinsViewCache& inputs, int nSpendHeight, CAmount& txfee); + +/** + * Heights of the blocks referenced by the inputs of a transaction (block reference annexes of + * witness v2 spends, see doc/block-reference.md), sorted and without duplicates. Malformed + * references are skipped; script validation rejects them. + * Preconditions: all inputs are available in `inputs`. + */ +std::vector GetBlockReferences(const CTransaction& tx, const CCoinsViewCache& inputs); } // namespace Consensus /** Auxiliary functions for transaction validation (ideally should not be exposed) */ diff --git a/src/script/interpreter.cpp b/src/script/interpreter.cpp index 949a1432cf05..9761ccc8eb32 100644 --- a/src/script/interpreter.cpp +++ b/src/script/interpreter.cpp @@ -5,6 +5,7 @@ #include