From aeb221c6c9266f0dd20896f706f0419011d1df54 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Fri, 30 Jan 2026 16:53:56 -0500 Subject: [PATCH 01/16] miniscript: introduce a new 't' property We use the 's' property in two ways: 1. To reason about malleability, as per the Miniscript specifications; 2. To sanity check that a top-level Miniscript fragment requires a transaction signature to be spent. This is fine as long as the only way to fix the transaction is by using a signature, and as long as all signatures are over the transaction. But in the following commits we are going to introduce fragments that either fix the transaction but do not require access to a private key (hence should not be 's' when reasoning about malleability), or allow to sign messages that are no the transaction itself and therefore should not be considered for the sanity check. Therefore, separate the two roles into two properties. Keep 's' for reasoning about malleability, and introduce a 't' property that determines whether the fragment's satisfaction fixes the transaction. --- src/script/miniscript.cpp | 37 ++++++++++++++++++++----------------- src/script/miniscript.h | 9 +++++++-- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/script/miniscript.cpp b/src/script/miniscript.cpp index 4b8d3673f95f..ec021c9e7327 100644 --- a/src/script/miniscript.cpp +++ b/src/script/miniscript.cpp @@ -101,51 +101,51 @@ Type ComputeType(Fragment fragment, Type x, Type y, Type z, const std::vector 1 and @@ -256,6 +258,7 @@ Type ComputeType(Fragment fragment, Type x, Type y, Type z, const std::vector= n_subs - k) | // m=all e, >=(n-k) s "s"_mst.If(num_s >= n_subs - k + 1) | // s= >=(n-k+1) s + "t"_mst.If(num_t >= n_subs - k + 1) | // t= >=(n-k+1) t acc_tl; // timelock info } } diff --git a/src/script/miniscript.h b/src/script/miniscript.h index 03f0a7c36542..cc7648180414 100644 --- a/src/script/miniscript.h +++ b/src/script/miniscript.h @@ -101,6 +101,10 @@ namespace miniscript { * - This generally requires 'm' for all subexpressions, and 'e' for all subexpressions * which are dissatisfied when satisfying the parent. * + * An additional type property helps reasoning about "sanity": + * - "t" Transaction signed: + * - Satisfactions (if any) for this expression always involve at least one signature. + * * One type property is an implementation detail: * - "x" Expensive verify: * - Expressions with this property have a script whose last opcode is not EQUAL, CHECKSIG, or CHECKMULTISIG. @@ -179,6 +183,7 @@ inline consteval Type operator""_mst(const char* c, size_t l) *p == 'i' ? 1 << 16 : // after: contains time timelock (cltv_time) *p == 'j' ? 1 << 17 : // after: contains height timelock (cltv_height) *p == 'k' ? 1 << 18 : // does not contain a combination of height and time locks + *p == 't' ? 1 << 19 : // Transaction signed property (throw std::logic_error("Unknown character in _mst literal"), 0) ); } @@ -1622,8 +1627,8 @@ struct Node { //! Check whether this script can always be satisfied in a non-malleable way. bool IsNonMalleable() const { return GetType() << "m"_mst; } - //! Check whether this script always needs a signature. - bool NeedsSignature() const { return GetType() << "s"_mst; } + //! Check whether this script always needs a transaction signature. + bool NeedsSignature() const { return GetType() << "t"_mst; } //! Check whether there is no satisfaction path that contains both timelocks and heightlocks bool CheckTimeLocksMix() const { return GetType() << "k"_mst; } From a2df46eb3c420cb6104a31af4760ff4a216e3829 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Wed, 10 Dec 2025 11:24:51 -0500 Subject: [PATCH 02/16] miniscript: introduce a pk_i() fragment for OP_INTERNALKEY This is a bit convoluted since no other fragment relied on the Taproot internal key yet, and it needs to be passed from the context, and therefore all places where we create a context: in signing, descriptor parsing / inference, unit tests and fuzz tests. The approach taken is to only query the internal key once at parsing / inference time and store it in the fragment's keys vector. This way it naturally integrates with the existing code for pk_k(), such for serialization, signing and duplicate key checks. Care was taken in fuzz harnesses to not invalidate existing seeds, but also use a meaningful key in TestNode() (i.e. in miniscript_smart and miniscript_stable targets). A new optional internal_key parameter is adding down the call chain as an "out" parameter: the first time the fuzzer generates a pk_i() fragment (if any) the internal key is set and reused for all pk_i() fragments. This way we can roundtrip ser/parsing, make it available to the satisfier to sign, etc.. --- src/script/descriptor.cpp | 42 ++++++++++++---- src/script/miniscript.cpp | 6 ++- src/script/miniscript.h | 31 +++++++++++- src/script/sign.cpp | 8 +++ src/test/fuzz/miniscript.cpp | 95 +++++++++++++++++++++++++++-------- src/test/miniscript_tests.cpp | 20 +++++--- 6 files changed, 160 insertions(+), 42 deletions(-) diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp index c8802d2bf893..e35582d054db 100644 --- a/src/script/descriptor.cpp +++ b/src/script/descriptor.cpp @@ -1662,6 +1662,8 @@ struct KeyParser { FlatSigningProvider* m_out; //! Must not be nullptr if parsing from Script. const SigningProvider* m_in; + //! Multipath expanded Taproot internal key if parsing a Tapscript. + const std::vector>* m_tr_internal_keys; //! List of multipath expanded keys contained in the Miniscript. mutable std::vector>> m_keys; //! Used to detect key parsing errors within a Miniscript. @@ -1672,8 +1674,12 @@ struct KeyParser { uint32_t m_offset; KeyParser(FlatSigningProvider* out LIFETIMEBOUND, const SigningProvider* in LIFETIMEBOUND, + const std::vector>* tr_internal_keys LIFETIMEBOUND, miniscript::MiniscriptContext ctx, uint32_t offset = 0) - : m_out(out), m_in(in), m_script_ctx(ctx), m_offset(offset) {} + : m_out(out), m_in(in), m_tr_internal_keys{tr_internal_keys}, m_script_ctx(ctx), m_offset(offset) + { + Assert(m_tr_internal_keys != nullptr || m_script_ctx != miniscript::MiniscriptContext::TAPSCRIPT); + } bool KeyCompare(const Key& a, const Key& b) const { return *m_keys.at(a).at(0) < *m_keys.at(b).at(0); @@ -1744,6 +1750,18 @@ struct KeyParser { return {}; } + Key GetInternalPK() const + { + Assert(m_tr_internal_keys); + std::vector> internal_keys; + for (const auto& ik: *m_tr_internal_keys) { + internal_keys.emplace_back(Assert(ik)->Clone()); + } + const auto key{m_keys.size()}; + m_keys.emplace_back(std::move(internal_keys)); + return key; + } + miniscript::MiniscriptContext MsContext() const { return m_script_ctx; } @@ -1751,7 +1769,7 @@ struct KeyParser { /** Parse a script in a particular context. */ // NOLINTNEXTLINE(misc-no-recursion) -std::vector> ParseScript(uint32_t& key_exp_index, Span& sp, ParseScriptContext ctx, FlatSigningProvider& out, std::string& error) +std::vector> ParseScript(uint32_t& key_exp_index, Span& sp, ParseScriptContext ctx, FlatSigningProvider& out, std::string& error, std::vector>* internal_pubkeys = nullptr) { using namespace script; Assume(ctx == ParseScriptContext::TOP || ctx == ParseScriptContext::P2SH || ctx == ParseScriptContext::P2WSH || ctx == ParseScriptContext::P2TR); @@ -1973,7 +1991,7 @@ std::vector> ParseScript(uint32_t& key_exp_index } // Process the actual script expression. auto sarg = Expr(expr); - subscripts.emplace_back(ParseScript(key_exp_index, sarg, ParseScriptContext::P2TR, out, error)); + subscripts.emplace_back(ParseScript(key_exp_index, sarg, ParseScriptContext::P2TR, out, error, &internal_keys)); if (subscripts.back().empty()) return {}; max_providers_len = std::max(max_providers_len, subscripts.back().size()); depths.push_back(branches.size()); @@ -2076,8 +2094,9 @@ std::vector> ParseScript(uint32_t& key_exp_index } // Process miniscript expressions. { - const auto script_ctx{ctx == ParseScriptContext::P2WSH ? miniscript::MiniscriptContext::P2WSH : miniscript::MiniscriptContext::TAPSCRIPT}; - KeyParser parser(/*out = */&out, /* in = */nullptr, /* ctx = */script_ctx, key_exp_index); + const auto script_ctx{ctx == ParseScriptContext::P2TR ? miniscript::MiniscriptContext::TAPSCRIPT : miniscript::MiniscriptContext::P2WSH}; + CHECK_NONFATAL(internal_pubkeys != nullptr || ctx != ParseScriptContext::P2TR); + KeyParser parser(/*out = */&out, /* in = */nullptr, internal_pubkeys, /* ctx = */script_ctx, key_exp_index); auto node = miniscript::FromString(std::string(expr.begin(), expr.end()), parser); if (parser.m_key_parsing_error != "") { error = std::move(parser.m_key_parsing_error); @@ -2175,7 +2194,7 @@ std::unique_ptr InferMultiA(const CScript& script, ParseScriptCo } // NOLINTNEXTLINE(misc-no-recursion) -std::unique_ptr InferScript(const CScript& script, ParseScriptContext ctx, const SigningProvider& provider) +std::unique_ptr InferScript(const CScript& script, ParseScriptContext ctx, const SigningProvider& provider, const std::vector>* internal_pubkeys = nullptr) { if (ctx == ParseScriptContext::P2TR && script.size() == 34 && script[0] == 32 && script[33] == OP_CHECKSIG) { XOnlyPubKey key{Span{script}.subspan(1, 32)}; @@ -2257,6 +2276,9 @@ std::unique_ptr InferScript(const CScript& script, ParseScriptCo // If found, convert it back to tree form. auto tree = InferTaprootTree(tap, pubkey); if (tree) { + std::vector> internal_keys; + internal_keys.emplace_back(InferXOnlyPubkey(tap.internal_key, ParseScriptContext::P2TR, provider)); + CHECK_NONFATAL(internal_keys.at(0)); // If that works, try to infer subdescriptors for all leaves. bool ok = true; std::vector> subscripts; //!< list of script subexpressions @@ -2264,7 +2286,7 @@ std::unique_ptr InferScript(const CScript& script, ParseScriptCo for (const auto& [depth, script, leaf_ver] : *tree) { std::unique_ptr subdesc; if (leaf_ver == TAPROOT_LEAF_TAPSCRIPT) { - subdesc = InferScript(CScript(script.begin(), script.end()), ParseScriptContext::P2TR, provider); + subdesc = InferScript(CScript(script.begin(), script.end()), ParseScriptContext::P2TR, provider, &internal_keys); } if (!subdesc) { ok = false; @@ -2275,8 +2297,7 @@ std::unique_ptr InferScript(const CScript& script, ParseScriptCo } } if (ok) { - auto key = InferXOnlyPubkey(tap.internal_key, ParseScriptContext::P2TR, provider); - return std::make_unique(std::move(key), std::move(subscripts), std::move(depths)); + return std::make_unique(std::move(internal_keys.at(0)), std::move(subscripts), std::move(depths)); } } } @@ -2291,7 +2312,8 @@ std::unique_ptr InferScript(const CScript& script, ParseScriptCo if (ctx == ParseScriptContext::P2WSH || ctx == ParseScriptContext::P2TR) { const auto script_ctx{ctx == ParseScriptContext::P2WSH ? miniscript::MiniscriptContext::P2WSH : miniscript::MiniscriptContext::TAPSCRIPT}; - KeyParser parser(/* out = */nullptr, /* in = */&provider, /* ctx = */script_ctx); + CHECK_NONFATAL(internal_pubkeys != nullptr || ctx == ParseScriptContext::P2WSH); + KeyParser parser(/* out = */nullptr, /* in = */&provider, /* tr_internal_keys = */internal_pubkeys, /* ctx = */script_ctx); auto node = miniscript::FromScript(script, parser); if (node && node->IsSane()) { std::vector> keys; diff --git a/src/script/miniscript.cpp b/src/script/miniscript.cpp index ec021c9e7327..b8af8c731e74 100644 --- a/src/script/miniscript.cpp +++ b/src/script/miniscript.cpp @@ -70,7 +70,7 @@ Type ComputeType(Fragment fragment, Type x, Type y, Type z, const std::vector= 1 && n_keys <= MAX_PUBKEYS_PER_MULTISIG); @@ -86,7 +86,8 @@ Type ComputeType(Fragment fragment, Type x, Type y, Type z, const std::vector subres) -> InputResult { switch (node.fragment) { - case Fragment::PK_K: { + case Fragment::PK_K: + case Fragment::PK_I: { std::vector sig; Availability avail = ctx.Sign(node.keys[0], sig); return {ZERO, InputStack(std::move(sig)).SetWithSig().SetAvailable(avail)}; @@ -1471,7 +1484,8 @@ struct Node { } // Start building the set of keys involved in this node and children. - // Start by keys in this node directly. + // Start by keys in this node directly. Note this also takes pk_i() fragments + // into account since we store the internal key in node.keys for those. size_t keys_count = node.keys.size(); keyset key_set{node.keys.begin(), node.keys.end(), Comp(ctx)}; if (key_set.size() != keys_count) { @@ -1587,6 +1601,7 @@ struct Node { return true; case Fragment::PK_K: case Fragment::PK_H: + case Fragment::PK_I: case Fragment::MULTI: case Fragment::MULTI_A: case Fragment::AFTER: @@ -1951,6 +1966,11 @@ inline NodeRef Parse(Span in, const Ctx& ctx) constructed.push_back(MakeNodeRef(internal::NoDupCheck{}, ctx.MsContext(), Fragment::PK_H, Vector(std::move(key)))); in = in.subspan(key_size + 1); script_size += 23; + } else if (Const("pk_i(", in)) { + if (!IsTapscript(ctx.MsContext())) return {}; + const auto pubkey{ctx.GetInternalPK()}; + constructed.push_back(MakeNodeRef(internal::NoDupCheck{}, ctx.MsContext(), Fragment::PK_I, Vector(std::move(pubkey)))); + in = in.subspan(1); } else if (Const("sha256(", in)) { auto res = ParseHexStrEnd(in, 32, ctx); if (!res) return {}; @@ -2301,6 +2321,13 @@ inline NodeRef DecodeScript(I& in, I last, const Ctx& ctx) constructed.push_back(MakeNodeRef(internal::NoDupCheck{}, ctx.MsContext(), Fragment::PK_H, Vector(std::move(*key)))); break; } + if (in[0].first == OP_INTERNALKEY) { + if (!IsTapscript(ctx.MsContext())) return {}; + const auto pubkey{ctx.GetInternalPK()}; + constructed.push_back(MakeNodeRef(internal::NoDupCheck{}, ctx.MsContext(), Fragment::PK_I, Vector(std::move(pubkey)))); + ++in; + break; + } // Time locks std::optional num; if (last - in >= 2 && in[0].first == OP_CHECKSEQUENCEVERIFY && (num = ParseScriptNumber(in[1]))) { diff --git a/src/script/sign.cpp b/src/script/sign.cpp index 61f578769c13..de476ce32816 100644 --- a/src/script/sign.cpp +++ b/src/script/sign.cpp @@ -259,6 +259,10 @@ struct WshSatisfier: Satisfier { const BaseSignatureCreator& creator LIFETIMEBOUND, const CScript& witscript LIFETIMEBOUND) : Satisfier(provider, sig_data, creator, witscript, miniscript::MiniscriptContext::P2WSH) {} + CPubKey GetInternalPK() const { + return CPubKey{}; + } + //! Conversion from a raw compressed public key. template std::optional FromPKBytes(I first, I last) const { @@ -292,6 +296,10 @@ struct TapSatisfier: Satisfier { : Satisfier(provider, sig_data, creator, script, miniscript::MiniscriptContext::TAPSCRIPT), m_leaf_hash(leaf_hash) {} + XOnlyPubKey GetInternalPK() const { + return m_sig_data.tr_spenddata.internal_key; + } + //! Conversion from a raw xonly public key. template std::optional FromPKBytes(I first, I last) const { diff --git a/src/test/fuzz/miniscript.cpp b/src/test/fuzz/miniscript.cpp index 18681cf503d0..0f86dafa8b1e 100644 --- a/src/test/fuzz/miniscript.cpp +++ b/src/test/fuzz/miniscript.cpp @@ -117,8 +117,10 @@ struct ParserContext { typedef CPubKey Key; const MsCtx script_ctx; + const std::optional& m_tr_internal_pubkey; - constexpr ParserContext(MsCtx ctx) noexcept : script_ctx(ctx) {} + constexpr ParserContext(MsCtx ctx, const std::optional& tr_internal_pubkey LIFETIMEBOUND) noexcept + : script_ctx(ctx), m_tr_internal_pubkey{tr_internal_pubkey} {} bool KeyCompare(const Key& a, const Key& b) const { return a < b; @@ -183,6 +185,14 @@ struct ParserContext { MsCtx MsContext() const { return script_ctx; } + + Key GetInternalPK() const { + Assert(script_ctx == MsCtx::TAPSCRIPT); + // For TestNode() it is always set when a pk_i() fragment is present. When there is + // no such fragment GetInternalPK() will never be called. + Assert(m_tr_internal_pubkey.has_value()); + return m_tr_internal_pubkey.value(); + } }; //! Context that implements naive conversion from/to script only, for roundtrip testing. @@ -235,12 +245,21 @@ struct ScriptParserContext { MsCtx MsContext() const { return script_ctx; } + + Key GetInternalPK() const { + const auto pubkey{XOnlyPubKey::NUMS_H.GetEvenCorrespondingCPubKey()}; + return Key { + .is_hash = false, + .data = {pubkey.begin(), pubkey.end()}, + }; + } }; //! Context to produce a satisfaction for a Miniscript node using the pre-computed data. struct SatisfierContext : ParserContext { - constexpr SatisfierContext(MsCtx ctx) noexcept : ParserContext(ctx) {} + constexpr SatisfierContext(MsCtx ctx, const std::optional& tr_internal_pubkey) noexcept + : ParserContext(ctx, tr_internal_pubkey) {} // Timelock challenges satisfaction. Make the value (deterministically) vary to explore different // paths. @@ -385,7 +404,7 @@ std::optional ConsumeTimeLock(FuzzedDataProvider& provider) { * - For multi_a(), same as for multi() but the threshold and the keys count are encoded on two bytes. * - For thresh(), the next byte defines the threshold value and the following one the number of subs. */ -std::optional ConsumeNodeStable(MsCtx script_ctx, FuzzedDataProvider& provider, Type type_needed) { +std::optional ConsumeNodeStable(MsCtx script_ctx, FuzzedDataProvider& provider, Type type_needed, std::optional& tr_internal_key) { bool allow_B = (type_needed == ""_mst) || (type_needed << "B"_mst); bool allow_K = (type_needed == ""_mst) || (type_needed << "K"_mst); bool allow_V = (type_needed == ""_mst) || (type_needed << "V"_mst); @@ -500,6 +519,13 @@ std::optional ConsumeNodeStable(MsCtx script_ctx, FuzzedDataProvider& for (auto& key: keys) key = ConsumePubKey(provider); return {{Fragment::MULTI_A, k, std::move(keys)}}; } + case 28: { + if (!allow_K || !IsTapscript(script_ctx)) return {}; + if (!tr_internal_key.has_value()) { + tr_internal_key = ConsumePubKey(provider); + } + return {{Fragment::PK_I, tr_internal_key.value()}}; + } default: break; } @@ -575,6 +601,15 @@ struct SmartInfo * super-recipe got added. */ std::sort(types.begin(), types.end()); + /** Whether a fragment must only be used in Tapscript. */ + auto requires_tapscript{[](const Fragment& frag) { + switch (frag) { + case Fragment::MULTI_A: + case Fragment::PK_I: return true; + default: return false; + } + }}; + // Iterate over all possible fragments. for (int fragidx = 0; fragidx <= int(Fragment::MULTI_A); ++fragidx) { int sub_count = 0; //!< The minimum number of child nodes this recipe has. @@ -585,7 +620,7 @@ struct SmartInfo Fragment frag{fragidx}; // Only produce recipes valid in the given context. - if ((!miniscript::IsTapscript(script_ctx) && frag == Fragment::MULTI_A) + if ((!miniscript::IsTapscript(script_ctx) && requires_tapscript(frag)) || (miniscript::IsTapscript(script_ctx) && frag == Fragment::MULTI)) { continue; } @@ -594,6 +629,7 @@ struct SmartInfo switch (frag) { case Fragment::PK_K: case Fragment::PK_H: + case Fragment::PK_I: n_keys = 1; break; case Fragment::MULTI: @@ -773,7 +809,7 @@ struct SmartInfo * (as improvements to the tables or changes to the typing rules could invalidate * everything). */ -std::optional ConsumeNodeSmart(MsCtx script_ctx, FuzzedDataProvider& provider, Type type_needed) { +std::optional ConsumeNodeSmart(MsCtx script_ctx, FuzzedDataProvider& provider, Type type_needed, std::optional& tr_internal_key) { /** Table entry for the requested type. */ const auto& table{IsTapscript(script_ctx) ? SMARTINFO.tap_table : SMARTINFO.wsh_table}; auto recipes_it = table.find(type_needed); @@ -786,6 +822,12 @@ std::optional ConsumeNodeSmart(MsCtx script_ctx, FuzzedDataProvider& p case Fragment::PK_K: case Fragment::PK_H: return {{frag, ConsumePubKey(provider)}}; + case Fragment::PK_I: + Assert(IsTapscript(script_ctx)); + if (!tr_internal_key.has_value()) { + tr_internal_key = ConsumePubKey(provider); + } + return {{frag, tr_internal_key.value()}}; case Fragment::MULTI: { const auto n_keys = provider.ConsumeIntegralInRange(1, 20); const auto k = provider.ConsumeIntegralInRange(1, n_keys); @@ -856,7 +898,7 @@ std::optional ConsumeNodeSmart(MsCtx script_ctx, FuzzedDataProvider& p * a NodeRef whose Type() matches the type fed to ConsumeNode. */ template -NodeRef GenNode(MsCtx script_ctx, F ConsumeNode, Type root_type, bool strict_valid = false) { +NodeRef GenNode(MsCtx script_ctx, F ConsumeNode, Type root_type, std::optional& tr_internal_key, bool strict_valid = false) { /** A stack of miniscript Nodes being built up. */ std::vector stack; /** The queue of instructions. */ @@ -872,7 +914,7 @@ NodeRef GenNode(MsCtx script_ctx, F ConsumeNode, Type root_type, bool strict_val auto type_needed = todo.back().first; if (!todo.back().second) { // Fragment/children have not been decided yet. Decide them. - auto node_info = ConsumeNode(type_needed); + auto node_info = ConsumeNode(type_needed, tr_internal_key); if (!node_info) return {}; // Update predicted resource limits. Since every leaf Miniscript node is at least one // byte long, we move one byte from each child to their parent. A similar technique is @@ -889,6 +931,9 @@ NodeRef GenNode(MsCtx script_ctx, F ConsumeNode, Type root_type, bool strict_val case Fragment::PK_H: ops += 3; break; + case Fragment::PK_I: + ops += 1; + break; case Fragment::OLDER: case Fragment::AFTER: ops += 1; @@ -1011,13 +1056,13 @@ NodeRef GenNode(MsCtx script_ctx, F ConsumeNode, Type root_type, bool strict_val } //! The spk for this script under the given context. If it's a Taproot output also record the spend data. -CScript ScriptPubKey(MsCtx ctx, const CScript& script, TaprootBuilder& builder) +CScript ScriptPubKey(MsCtx ctx, const CScript& script, TaprootBuilder& builder, const std::optional& tr_internal_key) { if (!miniscript::IsTapscript(ctx)) return CScript() << OP_0 << WitnessV0ScriptHash(script); // For Taproot outputs we always use a tree with a single script and a dummy internal key. builder.Add(0, script, TAPROOT_LEAF_TAPSCRIPT); - builder.Finalize(XOnlyPubKey::NUMS_H); + builder.Finalize(tr_internal_key ? XOnlyPubKey{*tr_internal_key} : XOnlyPubKey::NUMS_H); return GetScriptForDestination(builder.GetOutput()); } @@ -1031,12 +1076,12 @@ void SatisfactionToWitness(MsCtx ctx, CScriptWitness& witness, const CScript& sc } /** Perform various applicable tests on a miniscript Node. */ -void TestNode(const MsCtx script_ctx, const NodeRef& node, FuzzedDataProvider& provider) +void TestNode(const MsCtx script_ctx, const NodeRef& node, std::optional& tr_internal_key, FuzzedDataProvider& provider) { if (!node) return; // Check that it roundtrips to text representation - const ParserContext parser_ctx{script_ctx}; + const ParserContext parser_ctx{script_ctx, tr_internal_key}; std::optional str{node->ToString(parser_ctx)}; assert(str); auto parsed = miniscript::FromString(*str, parser_ctx); @@ -1100,11 +1145,11 @@ void TestNode(const MsCtx script_ctx, const NodeRef& node, FuzzedDataProvider& p } } - const SatisfierContext satisfier_ctx{script_ctx}; + const SatisfierContext satisfier_ctx{script_ctx, tr_internal_key}; // Get the ScriptPubKey for this script, filling spend data if it's Taproot. TaprootBuilder builder; - const CScript script_pubkey{ScriptPubKey(script_ctx, script, builder)}; + const CScript script_pubkey{ScriptPubKey(script_ctx, script, builder, tr_internal_key)}; // Run malleable satisfaction algorithm. std::vector> stack_mal; @@ -1133,7 +1178,9 @@ void TestNode(const MsCtx script_ctx, const NodeRef& node, FuzzedDataProvider& p ScriptError serror; bool res = VerifyScript(DUMMY_SCRIPTSIG, script_pubkey, &witness_nonmal, STANDARD_SCRIPT_VERIFY_FLAGS, CHECKER_CTX, &serror); // Non-malleable satisfactions are guaranteed to be valid if ValidSatisfactions(). - if (node->ValidSatisfactions()) assert(res); + if (node->ValidSatisfactions()) { + assert(res); + } // More detailed: non-malleable satisfactions must be valid, or could fail with ops count error (if CheckOpsLimit failed), // or with a stack size error (if CheckStackSize check failed). assert(res || @@ -1169,6 +1216,7 @@ void TestNode(const MsCtx script_ctx, const NodeRef& node, FuzzedDataProvider& p switch (node.fragment) { case Fragment::PK_K: case Fragment::PK_H: + case Fragment::PK_I: return is_key_satisfiable(node.keys[0]); case Fragment::MULTI: case Fragment::MULTI_A: { @@ -1216,9 +1264,10 @@ FUZZ_TARGET(miniscript_stable, .init = FuzzInit) // Run it under both P2WSH and Tapscript contexts. for (const auto script_ctx: {MsCtx::P2WSH, MsCtx::TAPSCRIPT}) { FuzzedDataProvider provider(buffer.data(), buffer.size()); - TestNode(script_ctx, GenNode(script_ctx, [&](Type needed_type) { - return ConsumeNodeStable(script_ctx, provider, needed_type); - }, ""_mst), provider); + std::optional tr_internal_key; + TestNode(script_ctx, GenNode(script_ctx, [&](Type needed_type, std::optional& tr_internal_key) { + return ConsumeNodeStable(script_ctx, provider, needed_type, tr_internal_key); + }, ""_mst, tr_internal_key), tr_internal_key, provider); } } @@ -1230,9 +1279,12 @@ FUZZ_TARGET(miniscript_smart, .init = FuzzInitSmart) FuzzedDataProvider provider(buffer.data(), buffer.size()); const auto script_ctx{(MsCtx)provider.ConsumeBool()}; - TestNode(script_ctx, GenNode(script_ctx, [&](Type needed_type) { - return ConsumeNodeSmart(script_ctx, provider, needed_type); - }, PickValue(provider, BASE_TYPES), true), provider); + std::optional tr_internal_key; + auto consume_node{[&](Type needed_type, std::optional& tr_internal_key) { + return ConsumeNodeSmart(script_ctx, provider, needed_type, tr_internal_key); + }}; + const auto fragment{GenNode(script_ctx, consume_node, PickValue(provider, BASE_TYPES), tr_internal_key, /* strict_valid = */ true)}; + TestNode(script_ctx, fragment, tr_internal_key, provider); } /* Fuzz tests that test parsing from a string, and roundtripping via string. */ @@ -1241,7 +1293,8 @@ FUZZ_TARGET(miniscript_string, .init = FuzzInit) if (buffer.empty()) return; FuzzedDataProvider provider(buffer.data(), buffer.size()); auto str = provider.ConsumeBytesAsString(provider.remaining_bytes() - 1); - const ParserContext parser_ctx{(MsCtx)provider.ConsumeBool()}; + const std::optional tr_internal_key{XOnlyPubKey::NUMS_H.GetEvenCorrespondingCPubKey()}; + const ParserContext parser_ctx{(MsCtx)provider.ConsumeBool(), tr_internal_key}; auto parsed = miniscript::FromString(str, parser_ctx); if (!parsed) return; diff --git a/src/test/miniscript_tests.cpp b/src/test/miniscript_tests.cpp index 28a8947231ab..5b13ca86071d 100644 --- a/src/test/miniscript_tests.cpp +++ b/src/test/miniscript_tests.cpp @@ -131,8 +131,10 @@ struct KeyConverter { typedef CPubKey Key; const miniscript::MiniscriptContext m_script_ctx; + const CPubKey m_tr_internal_key; - constexpr KeyConverter(miniscript::MiniscriptContext ctx) noexcept : m_script_ctx{ctx} {} + KeyConverter(miniscript::MiniscriptContext ctx, CPubKey tr_internal_key) noexcept + : m_script_ctx{ctx}, m_tr_internal_key{tr_internal_key} {} bool KeyCompare(const Key& a, const Key& b) const { return a < b; @@ -195,12 +197,16 @@ struct KeyConverter { miniscript::MiniscriptContext MsContext() const { return m_script_ctx; } + + Key GetInternalPK() const { + return m_tr_internal_key; + } }; /** A class that encapsulates all signing/hash revealing operations. */ struct Satisfier : public KeyConverter { - Satisfier(miniscript::MiniscriptContext ctx) noexcept : KeyConverter{ctx} {} + Satisfier(miniscript::MiniscriptContext ctx, CPubKey tr_internal_key) noexcept : KeyConverter{ctx, tr_internal_key} {} //! Which keys/timelocks/hash preimages are available. std::set supported; @@ -351,7 +357,7 @@ void TestSatisfy(const KeyConverter& converter, const std::string& testcase, con std::vector challist(challenges.begin(), challenges.end()); for (int iter = 0; iter < 3; ++iter) { std::shuffle(challist.begin(), challist.end(), m_rng); - Satisfier satisfier(converter.MsContext()); + Satisfier satisfier(converter.MsContext(), converter.GetInternalPK()); TestSignatureChecker checker(satisfier); bool prev_mal_success = false, prev_nonmal_success = false; // Go over all challenges involved in this miniscript in random order. @@ -480,9 +486,9 @@ void Test(const std::string& ms, const std::string& hexscript, const std::string std::optional max_tap_wit_size, std::optional stack_exec) { - KeyConverter wsh_converter(miniscript::MiniscriptContext::P2WSH); + KeyConverter wsh_converter(miniscript::MiniscriptContext::P2WSH, /*tr_internal_key=*/CPubKey{}); Test(ms, hexscript, mode, wsh_converter, opslimit, stacklimit, max_wit_size, stack_exec); - KeyConverter tap_converter(miniscript::MiniscriptContext::TAPSCRIPT); + KeyConverter tap_converter(miniscript::MiniscriptContext::TAPSCRIPT, /*tr_internal_key=*/XOnlyPubKey::NUMS_H.GetEvenCorrespondingCPubKey()); Test(ms, hextapscript == "=" ? hexscript : hextapscript, mode, tap_converter, opslimit, stacklimit, max_tap_wit_size, stack_exec); } @@ -596,8 +602,8 @@ BOOST_AUTO_TEST_CASE(fixed_tests) // - no pubkey at all // - no pubkey before a CHECKSIGADD // - no pubkey before the CHECKSIG - constexpr KeyConverter tap_converter{miniscript::MiniscriptContext::TAPSCRIPT}; - constexpr KeyConverter wsh_converter{miniscript::MiniscriptContext::P2WSH}; + const KeyConverter tap_converter{miniscript::MiniscriptContext::TAPSCRIPT, /*tr_internal_key=*/XOnlyPubKey::NUMS_H.GetEvenCorrespondingCPubKey()}; + const KeyConverter wsh_converter{miniscript::MiniscriptContext::P2WSH, /*tr_internal_key=*/CPubKey{}}; const auto no_pubkey{"ac519c"_hex_u8}; BOOST_CHECK(miniscript::FromScript({no_pubkey.begin(), no_pubkey.end()}, tap_converter) == nullptr); const auto incomplete_multi_a{"ba20c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5ba519c"_hex_u8}; From 407b222ff0a01eb132baf6da041daf440793cbf2 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Wed, 10 Dec 2025 11:53:26 -0500 Subject: [PATCH 03/16] miniscript: introduce pki() alias for c:pk_i() Likewise pk() and pkh(), this is syntactic sugar for the common case of an internal public key signature check. --- src/script/miniscript.h | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/script/miniscript.h b/src/script/miniscript.h index 3021f232a776..17ef490d0cfe 100644 --- a/src/script/miniscript.h +++ b/src/script/miniscript.h @@ -871,6 +871,10 @@ struct Node { if (!key_str) return {}; return std::move(ret) + "pkh(" + std::move(*key_str) + ")"; } + if (node.subs[0]->fragment == Fragment::PK_I) { + // pki() is syntactic sugar for c:pk_i() + return std::move(ret) + "pki()"; + } return "c" + std::move(subs[0]); case Fragment::WRAP_D: return "d" + std::move(subs[0]); case Fragment::WRAP_V: return "v" + std::move(subs[0]); @@ -1952,6 +1956,12 @@ inline NodeRef Parse(Span in, const Ctx& ctx) constructed.push_back(MakeNodeRef(internal::NoDupCheck{}, ctx.MsContext(), Fragment::WRAP_C, Vector(MakeNodeRef(internal::NoDupCheck{}, ctx.MsContext(), Fragment::PK_H, Vector(std::move(key)))))); in = in.subspan(key_size + 1); script_size += 24; + } else if (Const("pki(", in)) { + if (!IsTapscript(ctx.MsContext())) return {}; + const auto pubkey{ctx.GetInternalPK()}; + constructed.push_back(MakeNodeRef(internal::NoDupCheck{}, ctx.MsContext(), Fragment::WRAP_C, Vector(MakeNodeRef(internal::NoDupCheck{}, ctx.MsContext(), Fragment::PK_I, Vector(std::move(pubkey)))))); + in = in.subspan(1); + script_size += 1; } else if (Const("pk_k(", in)) { auto res = ParseKeyEnd(in, ctx); if (!res) return {}; From f94af6262f6259f292edb8f6b491b7adf974d98b Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Wed, 10 Dec 2025 14:35:12 -0500 Subject: [PATCH 04/16] qa: test OP_INTERNALKEY integration in Miniscript descriptors --- src/test/descriptor_tests.cpp | 27 +++++++++++++++++++++++++++ src/test/miniscript_tests.cpp | 18 +++++++++++++----- test/functional/wallet_miniscript.py | 2 ++ 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/test/descriptor_tests.cpp b/src/test/descriptor_tests.cpp index 63c53a842c1f..601a489ea981 100644 --- a/src/test/descriptor_tests.cpp +++ b/src/test/descriptor_tests.cpp @@ -1071,6 +1071,33 @@ BOOST_AUTO_TEST_CASE(descriptor_test) CheckInferDescriptor("76a914a31725c74421fadc50d35520ab8751ed120af80588ac", "pkh(04c56fe4a92d401bcbf1b3dfbe4ac3dac5602ca155a3681497f02c1b9a733b92d704e2da6ec4162e4846af9236ef4171069ac8b7f8234a8405b6cadd96f34f5a31)", {}, {{"04c56fe4a92d401bcbf1b3dfbe4ac3dac5602ca155a3681497f02c1b9a733b92d704e2da6ec4162e4846af9236ef4171069ac8b7f8234a8405b6cadd96f34f5a31", ""}}); // Infer pk() from p2pk with uncompressed key CheckInferDescriptor("4104032540df1d3c7070a8ab3a9cdd304dfc7fd1e6541369c53c4c3310b2537d91059afc8b8e7673eb812a32978dabb78c40f2e423f7757dca61d11838c7aeeb5220ac", "pk(04032540df1d3c7070a8ab3a9cdd304dfc7fd1e6541369c53c4c3310b2537d91059afc8b8e7673eb812a32978dabb78c40f2e423f7757dca61d11838c7aeeb5220)", {}, {{"04032540df1d3c7070a8ab3a9cdd304dfc7fd1e6541369c53c4c3310b2537d91059afc8b8e7673eb812a32978dabb78c40f2e423f7757dca61d11838c7aeeb5220", ""}}); + + // OP_INTERNALKEY tests + CheckMultipath("tr(xprv9yYge4PS54XkYT9KiLfCRwc8Jeuz8DucxQGtuEecJZYhKNiqbPxYHTPzXtskmzWBqdqkRAGsghNmZzNsfU2wstaB3XjDQFPv567aQSSuPyo/<2;3>/*,l:pki())", + "tr(xpub6CY33ZvKuS63kwDnpNCCo5YrrgkUXgdUKdCVhd4Dru5gCB3z8wGnqFiUP98Za5pYSYF5KmvBHTY3Ra8FAJGggzBjuHS69WzN8gscPupuZwK/<2;3>/*,l:pki())", + { + "tr(xprv9yYge4PS54XkYT9KiLfCRwc8Jeuz8DucxQGtuEecJZYhKNiqbPxYHTPzXtskmzWBqdqkRAGsghNmZzNsfU2wstaB3XjDQFPv567aQSSuPyo/2/*,l:pki())", + "tr(xprv9yYge4PS54XkYT9KiLfCRwc8Jeuz8DucxQGtuEecJZYhKNiqbPxYHTPzXtskmzWBqdqkRAGsghNmZzNsfU2wstaB3XjDQFPv567aQSSuPyo/3/*,l:pki())", + }, + { + "tr(xpub6CY33ZvKuS63kwDnpNCCo5YrrgkUXgdUKdCVhd4Dru5gCB3z8wGnqFiUP98Za5pYSYF5KmvBHTY3Ra8FAJGggzBjuHS69WzN8gscPupuZwK/2/*,l:pki())", + "tr(xpub6CY33ZvKuS63kwDnpNCCo5YrrgkUXgdUKdCVhd4Dru5gCB3z8wGnqFiUP98Za5pYSYF5KmvBHTY3Ra8FAJGggzBjuHS69WzN8gscPupuZwK/3/*,l:pki())", + }, + { + "tr(xpub6CY33ZvKuS63kwDnpNCCo5YrrgkUXgdUKdCVhd4Dru5gCB3z8wGnqFiUP98Za5pYSYF5KmvBHTY3Ra8FAJGggzBjuHS69WzN8gscPupuZwK/2/*,l:pki())", + "tr(xpub6CY33ZvKuS63kwDnpNCCo5YrrgkUXgdUKdCVhd4Dru5gCB3z8wGnqFiUP98Za5pYSYF5KmvBHTY3Ra8FAJGggzBjuHS69WzN8gscPupuZwK/3/*,l:pki())", + }, + XONLY_KEYS | RANGE, + { + {{"51208d62d3a2229d67dd197cb8601494de6c2088e62609389e8e6e0e2a9830388753"}, {"5120cafef28b139ce0f688aac7bd5f6700791e039c30d29bd36102eddb22f1f159e5"}}, + {{"5120b0f1b0c5b0078543bfdee10113b108a5feb4d9270c27dca8ca34d3a30cc42fa3"}, {"5120f9ca70149b257fdd8fb9b6a3474709b9837396324b74515b3a4278229b28bc09"}}, + }, + OutputType::BECH32M, + { + {{2, 0}, {2, 1}}, + {{3, 0}, {3, 1}}, + } + ); } BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/miniscript_tests.cpp b/src/test/miniscript_tests.cpp index 5b13ca86071d..d804d896c900 100644 --- a/src/test/miniscript_tests.cpp +++ b/src/test/miniscript_tests.cpp @@ -330,13 +330,13 @@ std::set FindChallenges(const NodeRef& ref) { } //! The spk for this script under the given context. If it's a Taproot output also record the spend data. -CScript ScriptPubKey(miniscript::MiniscriptContext ctx, const CScript& script, TaprootBuilder& builder) +CScript ScriptPubKey(const KeyConverter& converter, const CScript& script, TaprootBuilder& builder) { - if (!miniscript::IsTapscript(ctx)) return CScript() << OP_0 << WitnessV0ScriptHash(script); + if (!miniscript::IsTapscript(converter.MsContext())) return CScript() << OP_0 << WitnessV0ScriptHash(script); // For Taproot outputs we always use a tree with a single script and a dummy internal key. builder.Add(0, script, TAPROOT_LEAF_TAPSCRIPT); - builder.Finalize(XOnlyPubKey::NUMS_H); + builder.Finalize(XOnlyPubKey{converter.GetInternalPK()}); return GetScriptForDestination(builder.GetOutput()); } @@ -366,7 +366,7 @@ void TestSatisfy(const KeyConverter& converter, const std::string& testcase, con // Get the ScriptPubKey for this script, filling spend data if it's Taproot. TaprootBuilder builder; - const CScript script_pubkey{ScriptPubKey(converter.MsContext(), script, builder)}; + const CScript script_pubkey{ScriptPubKey(converter, script, builder)}; // Run malleable satisfaction algorithm. CScriptWitness witness_mal; @@ -488,7 +488,8 @@ void Test(const std::string& ms, const std::string& hexscript, const std::string { KeyConverter wsh_converter(miniscript::MiniscriptContext::P2WSH, /*tr_internal_key=*/CPubKey{}); Test(ms, hexscript, mode, wsh_converter, opslimit, stacklimit, max_wit_size, stack_exec); - KeyConverter tap_converter(miniscript::MiniscriptContext::TAPSCRIPT, /*tr_internal_key=*/XOnlyPubKey::NUMS_H.GetEvenCorrespondingCPubKey()); + const auto internal_pubkey{g_testdata->pubkeys[g_testdata->pubkeys.size() - 1]}; + KeyConverter tap_converter(miniscript::MiniscriptContext::TAPSCRIPT, /*tr_internal_key=*/internal_pubkey); Test(ms, hextapscript == "=" ? hexscript : hextapscript, mode, tap_converter, opslimit, stacklimit, max_tap_wit_size, stack_exec); } @@ -729,6 +730,13 @@ BOOST_AUTO_TEST_CASE(fixed_tests) // This is actually non-malleable in practice, but we cannot detect it in type system. See above rationale Test("thresh(1,c:pk_k(03d30199d74fb5a22d47b6e054e2f378cedacffcb89904a61d75d0dbd407143e65),altv:after(1000000000),altv:after(100))", "?", "?", TESTMODE_VALID); // thresh with k = 1 + // OP_INTERNALKEY tests + Test("and_b(older(42),sc:pk_i())", "012ab27ccbac9a", "012ab27ccbac9a", TESTMODE_VALID | TESTMODE_NONMAL | TESTMODE_NEEDSIG | TESTMODE_P2WSH_INVALID); + Test("and_b(older(42),s:pki())", "012ab27ccbac9a", "012ab27ccbac9a", TESTMODE_VALID | TESTMODE_NONMAL | TESTMODE_NEEDSIG | TESTMODE_P2WSH_INVALID); + std::string ms_ik{"and_b(older(42),ac:or_i(pk_i(),pk_h("}; + ms_ik += HexStr(g_testdata->pubkeys[21]) + ")))"; + Test(ms_ik, "?", "?", TESTMODE_VALID | TESTMODE_NONMAL | TESTMODE_NEEDSIG | TESTMODE_P2WSH_INVALID); + g_testdata.reset(); } diff --git a/test/functional/wallet_miniscript.py b/test/functional/wallet_miniscript.py index 064eac499b2f..de4ea277f9b3 100755 --- a/test/functional/wallet_miniscript.py +++ b/test/functional/wallet_miniscript.py @@ -55,6 +55,8 @@ f"tr(4d54bb9928a0683b7e383de72943b214b0716f58aa54c7ba6bcea2328bc9c768,{{{{{P2WSH_MINISCRIPTS[0]},{P2WSH_MINISCRIPTS[1]}}},{P2WSH_MINISCRIPTS[2].replace('multi', 'multi_a')}}})", # A Taproot with all above scripts in its tree. f"tr(4d54bb9928a0683b7e383de72943b214b0716f58aa54c7ba6bcea2328bc9c768,{{{{{P2WSH_MINISCRIPTS[0]},{P2WSH_MINISCRIPTS[1]}}},{{{P2WSH_MINISCRIPTS[2].replace('multi', 'multi_a')},{P2WSH_MINISCRIPTS[3]}}}}})", + # A Taproot with an OP_INTERNALKEY in one of the leaves. + f"tr({TPUBS[0]}/*,{{thresh(2,pki(),a:multi_a(1,{TPUBS[1]}/*)),pk({TPUBS[2]})}})", ] DESCS_PRIV = [ From c6220f846768aa61f4d26978b43f2fe810b83321 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Thu, 11 Dec 2025 14:19:49 -0500 Subject: [PATCH 05/16] miniscript: introduce a 'th(hash)' fragment This fragment checks the spending transaction's template hash. Because it commits to the spending transaction (with more malleable fields than SIGHASH_ALL, but less than SIGHASH_SINGLE/NONE) then we give it the 's' property. Note this breaks the invariant that an 's' fragment always contains at least one key. In the miniscript_smart and miniscript_stable fuzz harnesses we use the message hash for dummy signatures as the template hash. We sometimes create th() fragments with this hash (making them satisfiable), and sometimes not. --- src/script/descriptor.cpp | 10 +++++----- src/script/miniscript.cpp | 4 +++- src/script/miniscript.h | 29 +++++++++++++++++++++++++++++ src/script/sign.cpp | 13 +++++++++++++ src/test/fuzz/miniscript.cpp | 31 ++++++++++++++++++++++++++++--- src/test/miniscript_tests.cpp | 7 +++++++ 6 files changed, 85 insertions(+), 9 deletions(-) diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp index e35582d054db..495f1ef94553 100644 --- a/src/script/descriptor.cpp +++ b/src/script/descriptor.cpp @@ -2132,16 +2132,16 @@ std::vector> ParseScript(uint32_t& key_exp_index } return {}; } - // A signature check is required for a miniscript to be sane. Therefore no sane miniscript - // may have an empty list of public keys. - CHECK_NONFATAL(!parser.m_keys.empty()); + // A signature check is required for a miniscript to be sane. But a templatehash check + // is considered one, so the descriptor may have an empty list of keys. key_exp_index += parser.m_keys.size(); // Make sure all vecs are of the same length, or exactly length 1 // For length 1 vectors, clone subdescs until vector is the same length - size_t num_multipath = std::max_element(parser.m_keys.begin(), parser.m_keys.end(), + auto max_elem{std::max_element(parser.m_keys.begin(), parser.m_keys.end(), [](const std::vector>& a, const std::vector>& b) { return a.size() < b.size(); - })->size(); + })}; + const size_t num_multipath{!parser.m_keys.empty() ? max_elem->size() : 1}; for (auto& vec : parser.m_keys) { if (vec.size() == 1) { diff --git a/src/script/miniscript.cpp b/src/script/miniscript.cpp index b8af8c731e74..88e7fe972d12 100644 --- a/src/script/miniscript.cpp +++ b/src/script/miniscript.cpp @@ -39,7 +39,7 @@ Type SanitizeType(Type e) { Type ComputeType(Fragment fragment, Type x, Type y, Type z, const std::vector& sub_types, uint32_t k, size_t data_size, size_t n_subs, size_t n_keys, MiniscriptContext ms_ctx) { // Sanity check on data - if (fragment == Fragment::SHA256 || fragment == Fragment::HASH256) { + if (fragment == Fragment::SHA256 || fragment == Fragment::HASH256 || fragment == Fragment::TH) { assert(data_size == 32); } else if (fragment == Fragment::RIPEMD160 || fragment == Fragment::HASH160) { assert(data_size == 20); @@ -101,6 +101,7 @@ Type ComputeType(Fragment fragment, Type x, Type y, Type z, const std::vectorops.count + subs[1]->ops.count, subs[0]->ops.sat + subs[1]->ops.sat, {}}; case Fragment::AND_B: { const auto count{1 + subs[0]->ops.count + subs[1]->ops.count}; @@ -1051,6 +1055,8 @@ struct Node { SatInfo::OP_SIZE() + SatInfo::Push() + SatInfo::OP_EQUALVERIFY() + SatInfo::Hash() + SatInfo::Push() + SatInfo::OP_EQUAL(), {} }; + // Push the provided hash, push the actual template hash, then op_equal + case Fragment::TH: return SatInfo::Push() + SatInfo::Push() + SatInfo::OP_EQUAL(); case Fragment::ANDOR: { const auto& x{subs[0]->ss}; const auto& y{subs[1]->ss}; @@ -1165,6 +1171,7 @@ struct Node { case Fragment::RIPEMD160: case Fragment::HASH256: case Fragment::HASH160: return {1 + 32, {}}; + case Fragment::TH: return {0, 0}; case Fragment::ANDOR: { const auto sat{(subs[0]->ws.sat + subs[1]->ws.sat) | (subs[0]->ws.dsat + subs[2]->ws.sat)}; const auto dsat{subs[0]->ws.dsat + subs[2]->ws.dsat}; @@ -1338,6 +1345,13 @@ struct Node { Availability avail = ctx.SatHASH160(node.data, preimage); return {ZERO32, InputStack(std::move(preimage)).SetAvailable(avail)}; } + case Fragment::TH: { + if (ctx.CheckTemplateHash(node.data)) { + return {INVALID, InputStack{}.SetWithSig()}; + } else { + return {EMPTY, INVALID}; + } + } case Fragment::AND_V: { auto& x = subres[0], &y = subres[1]; // As the dissatisfaction here only consist of a single option, it doesn't @@ -1614,6 +1628,7 @@ struct Node { case Fragment::HASH160: case Fragment::SHA256: case Fragment::RIPEMD160: + case Fragment::TH: return bool{fn(node)}; case Fragment::ANDOR: return (subs[0] && subs[1]) || subs[2]; @@ -2009,6 +2024,14 @@ inline NodeRef Parse(Span in, const Ctx& ctx) constructed.push_back(MakeNodeRef(internal::NoDupCheck{}, ctx.MsContext(), Fragment::HASH160, std::move(hash))); in = in.subspan(hash_size + 1); script_size += 26; + } else if (Const("th(", in)) { + if (!IsTapscript(ctx.MsContext())) return {}; + auto res = ParseHexStrEnd(in, 32, ctx); + if (!res) return {}; + auto& [thash, thash_size] = *res; + constructed.push_back(MakeNodeRef(internal::NoDupCheck{}, ctx.MsContext(), Fragment::TH, std::move(thash))); + in = in.subspan(thash_size + 1); + script_size += 32 + 2; } else if (Const("after(", in)) { int arg_size = FindNextChar(in, ')'); if (arg_size < 1) return {}; @@ -2372,6 +2395,12 @@ inline NodeRef DecodeScript(I& in, I last, const Ctx& ctx) break; } } + if (last - in >= 2 && in[0].first == OP_EQUAL && in[1].first == OP_TEMPLATEHASH) { + if (!IsTapscript(ctx.MsContext())) return {}; + constructed.push_back(MakeNodeRef(internal::NoDupCheck{}, ctx.MsContext(), Fragment::TH, in[2].second)); + in += 3; + break; + } // Multi if (last - in >= 3 && in[0].first == OP_CHECKMULTISIG) { if (IsTapscript(ctx.MsContext())) return {}; diff --git a/src/script/sign.cpp b/src/script/sign.cpp index de476ce32816..0791e42f4b84 100644 --- a/src/script/sign.cpp +++ b/src/script/sign.cpp @@ -248,6 +248,19 @@ struct Satisfier { return MsLookupHelper(m_sig_data.hash160_preimages, hash, preimage); } + //! Get the template hash of the spending transaction. Like in regular signing, we only support annex-less for now. + uint256 GetTemplateHash() const { + ScriptExecutionData exec_data; + exec_data.m_annex_init = true; + exec_data.m_annex_present = false; + return m_creator.Checker().GetTemplateHash(exec_data); + } + + //! Template hash satisfaction. + bool CheckTemplateHash(const std::vector& hash) const { + return hash.data() == GetTemplateHash().data(); + } + miniscript::MiniscriptContext MsContext() const { return m_script_ctx; } diff --git a/src/test/fuzz/miniscript.cpp b/src/test/fuzz/miniscript.cpp index 0f86dafa8b1e..b01f937f2305 100644 --- a/src/test/fuzz/miniscript.cpp +++ b/src/test/fuzz/miniscript.cpp @@ -28,6 +28,9 @@ using miniscript::operator""_mst; struct TestData { typedef CPubKey Key; + // All our signatures sign (and are required to sign) this constant message. Also used as the template hash. + static constexpr uint256 MESSAGE_HASH{"0000000000000000f5cd94e18b6fe77dd7aca9e35c2b0c9cbd86356c80a71065"}; + // Precomputed public keys, and a dummy signature for each of them. std::vector dummy_keys; std::map dummy_key_idx_map; @@ -48,8 +51,6 @@ struct TestData { //! Set the precomputed data. void Init() { unsigned char keydata[32] = {1}; - // All our signatures sign (and are required to sign) this constant message. - constexpr uint256 MESSAGE_HASH{"0000000000000000f5cd94e18b6fe77dd7aca9e35c2b0c9cbd86356c80a71065"}; // We don't pass additional randomness when creating a schnorr signature. const auto EMPTY_AUX{uint256::ZERO}; @@ -296,6 +297,10 @@ struct SatisfierContext : ParserContext { miniscript::Availability SatHASH160(const std::vector& hash, std::vector& preimage) const { return LookupHash(hash, preimage, TEST_DATA.hash160_preimages); } + + bool CheckTemplateHash(const std::vector& data) const { + return uint256{data} == TestData::MESSAGE_HASH; + } }; //! Context to check a satisfaction against the pre-computed data. @@ -318,6 +323,8 @@ const struct CheckerContext: BaseSignatureChecker { } bool CheckLockTime(const CScriptNum& nLockTime) const override { return nLockTime.GetInt64() & 1; } bool CheckSequence(const CScriptNum& nSequence) const override { return nSequence.GetInt64() & 1; } + + uint256 GetTemplateHash(ScriptExecutionData&) const override { return TestData::MESSAGE_HASH; } } CHECKER_CTX; //! Context to check for duplicates when instancing a Node. @@ -526,6 +533,12 @@ std::optional ConsumeNodeStable(MsCtx script_ctx, FuzzedDataProvider& } return {{Fragment::PK_I, tr_internal_key.value()}}; } + case 29: { + if (!allow_B || !IsTapscript(script_ctx)) return {}; + // Sometimes make it unsatisfiable. + const auto data{provider.ConsumeBool() ? TestData::MESSAGE_HASH : uint256::ZERO}; + return {{Fragment::TH, std::vector{data.begin(), data.end()}}}; + } default: break; } @@ -605,7 +618,8 @@ struct SmartInfo auto requires_tapscript{[](const Fragment& frag) { switch (frag) { case Fragment::MULTI_A: - case Fragment::PK_I: return true; + case Fragment::PK_I: + case Fragment::TH: return true; default: return false; } }}; @@ -643,6 +657,7 @@ struct SmartInfo break; case Fragment::SHA256: case Fragment::HASH256: + case Fragment::TH: data_size = 32; break; case Fragment::RIPEMD160: @@ -853,6 +868,11 @@ std::optional ConsumeNodeSmart(MsCtx script_ctx, FuzzedDataProvider& p return {{frag, PickValue(provider, TEST_DATA.ripemd160)}}; case Fragment::HASH160: return {{frag, PickValue(provider, TEST_DATA.hash160)}}; + case Fragment::TH: { + // Sometimes make it non-satisfiable. + const auto data{provider.ConsumeBool() ? TestData::MESSAGE_HASH : uint256::ZERO}; + return {{frag, std::vector{data.begin(), data.end()}}}; + } case Fragment::JUST_0: case Fragment::JUST_1: case Fragment::WRAP_A: @@ -944,6 +964,9 @@ NodeRef GenNode(MsCtx script_ctx, F ConsumeNode, Type root_type, std::optional& hash, std::vector& preimage) const { return SatHash(hash, preimage, ChallengeType::RIPEMD160); } miniscript::Availability SatHASH256(const std::vector& hash, std::vector& preimage) const { return SatHash(hash, preimage, ChallengeType::HASH256); } miniscript::Availability SatHASH160(const std::vector& hash, std::vector& preimage) const { return SatHash(hash, preimage, ChallengeType::HASH160); } + + bool CheckTemplateHash(const std::vector& data) const { + return supported.count(Challenge(ChallengeType::TEMPLATEHASH, ChallengeNumber(data))); + } }; /** Mocking signature/timelock checker. @@ -321,6 +326,8 @@ std::set FindChallenges(const NodeRef& ref) { chal.emplace(ChallengeType::HASH256, ChallengeNumber(ref->data)); } else if (ref->fragment == miniscript::Fragment::HASH160) { chal.emplace(ChallengeType::HASH160, ChallengeNumber(ref->data)); + } else if (ref->fragment == miniscript::Fragment::TH) { + chal.emplace(ChallengeType::TEMPLATEHASH, ChallengeNumber(ref->data)); } for (const auto& sub : ref->subs) { auto sub_chal = FindChallenges(sub); From d0505d788d094e439e19c49dab9ebff9d6ba63e1 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Fri, 2 Jan 2026 10:36:43 -0500 Subject: [PATCH 06/16] qa: sanity check new th() Miniscript fragment --- src/test/descriptor_tests.cpp | 3 ++- src/test/miniscript_tests.cpp | 25 ++++++++++++++++++++----- test/functional/wallet_miniscript.py | 2 ++ 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/test/descriptor_tests.cpp b/src/test/descriptor_tests.cpp index 601a489ea981..35bc6f75679c 100644 --- a/src/test/descriptor_tests.cpp +++ b/src/test/descriptor_tests.cpp @@ -1072,7 +1072,7 @@ BOOST_AUTO_TEST_CASE(descriptor_test) // Infer pk() from p2pk with uncompressed key CheckInferDescriptor("4104032540df1d3c7070a8ab3a9cdd304dfc7fd1e6541369c53c4c3310b2537d91059afc8b8e7673eb812a32978dabb78c40f2e423f7757dca61d11838c7aeeb5220ac", "pk(04032540df1d3c7070a8ab3a9cdd304dfc7fd1e6541369c53c4c3310b2537d91059afc8b8e7673eb812a32978dabb78c40f2e423f7757dca61d11838c7aeeb5220)", {}, {{"04032540df1d3c7070a8ab3a9cdd304dfc7fd1e6541369c53c4c3310b2537d91059afc8b8e7673eb812a32978dabb78c40f2e423f7757dca61d11838c7aeeb5220", ""}}); - // OP_INTERNALKEY tests + // OP_INTERNALKEY, OP_TEMPLATEHASH tests CheckMultipath("tr(xprv9yYge4PS54XkYT9KiLfCRwc8Jeuz8DucxQGtuEecJZYhKNiqbPxYHTPzXtskmzWBqdqkRAGsghNmZzNsfU2wstaB3XjDQFPv567aQSSuPyo/<2;3>/*,l:pki())", "tr(xpub6CY33ZvKuS63kwDnpNCCo5YrrgkUXgdUKdCVhd4Dru5gCB3z8wGnqFiUP98Za5pYSYF5KmvBHTY3Ra8FAJGggzBjuHS69WzN8gscPupuZwK/<2;3>/*,l:pki())", { @@ -1098,6 +1098,7 @@ BOOST_AUTO_TEST_CASE(descriptor_test) {{3, 0}, {3, 1}}, } ); + Check("tr(xprv9yYge4PS54XkYT9KiLfCRwc8Jeuz8DucxQGtuEecJZYhKNiqbPxYHTPzXtskmzWBqdqkRAGsghNmZzNsfU2wstaB3XjDQFPv567aQSSuPyo/0/*,th(e8a8c07ee3bfdc31a2b2c79c796346da139ae1810cd456a4d4dda86a9f522937))", "tr(xpub6CY33ZvKuS63kwDnpNCCo5YrrgkUXgdUKdCVhd4Dru5gCB3z8wGnqFiUP98Za5pYSYF5KmvBHTY3Ra8FAJGggzBjuHS69WzN8gscPupuZwK/0/*,th(e8a8c07ee3bfdc31a2b2c79c796346da139ae1810cd456a4d4dda86a9f522937))", "tr(xpub6CY33ZvKuS63kwDnpNCCo5YrrgkUXgdUKdCVhd4Dru5gCB3z8wGnqFiUP98Za5pYSYF5KmvBHTY3Ra8FAJGggzBjuHS69WzN8gscPupuZwK/0/*,th(e8a8c07ee3bfdc31a2b2c79c796346da139ae1810cd456a4d4dda86a9f522937))", XONLY_KEYS | RANGE, {{"51203ea124787c99ae093e931684d25c9acdb665b87089c018086584fa9014880a38"}}, OutputType::BECH32M, /*op_desc_id=*/{}, {{0, 0}}); } BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/miniscript_tests.cpp b/src/test/miniscript_tests.cpp index d4be786d7b01..de5675ff2354 100644 --- a/src/test/miniscript_tests.cpp +++ b/src/test/miniscript_tests.cpp @@ -29,6 +29,9 @@ namespace { /** TestData groups various kinds of precomputed data necessary in this test. */ struct TestData { + // All our signatures sign (and are required to sign) this constant message. Also used as the template hash. + static constexpr uint256 MESSAGE_HASH{"0000000000000000f5cd94e18b6fe77dd7aca9e35c2b0c9cbd86356c80a71065"}; + //! The only public keys used in this test. std::vector pubkeys; //! A map from the public keys to their CKeyIDs (faster than hashing every time). @@ -50,8 +53,6 @@ struct TestData { TestData() { - // All our signatures sign (and are required to sign) this constant message. - constexpr uint256 MESSAGE_HASH{"0000000000000000f5cd94e18b6fe77dd7aca9e35c2b0c9cbd86356c80a71065"}; // We don't pass additional randomness when creating a schnorr signature. const auto EMPTY_AUX{uint256::ZERO}; @@ -260,7 +261,7 @@ struct Satisfier : public KeyConverter { miniscript::Availability SatHASH160(const std::vector& hash, std::vector& preimage) const { return SatHash(hash, preimage, ChallengeType::HASH160); } bool CheckTemplateHash(const std::vector& data) const { - return supported.count(Challenge(ChallengeType::TEMPLATEHASH, ChallengeNumber(data))); + return supported.count(Challenge(ChallengeType::TEMPLATEHASH, ChallengeNumber(data))) && uint256{data} == TestData::MESSAGE_HASH; } }; @@ -300,6 +301,10 @@ class TestSignatureChecker : public BaseSignatureChecker { // Delegate to Satisfier. return ctx.CheckOlder(sequence.GetInt64()); } + + uint256 GetTemplateHash(ScriptExecutionData&) const override { + return TestData::MESSAGE_HASH; + } }; using Fragment = miniscript::Fragment; @@ -437,7 +442,12 @@ void TestSatisfy(const KeyConverter& converter, const std::string& testcase, con prev_nonmal_success = nonmal_success; } - bool satisfiable = node->IsSatisfiable([](const Node&) { return true; }); + bool satisfiable = node->IsSatisfiable([](const Node& node) { + if (node.fragment == Fragment::TH && uint256{node.data} != TestData::MESSAGE_HASH) { + return false; + } + return true; + }); // If the miniscript was satisfiable at all, a satisfaction must be found after all conditions are added. BOOST_CHECK_EQUAL(prev_mal_success, satisfiable); // If the miniscript is sane and satisfiable, a nonmalleable satisfaction must eventually be found. @@ -737,12 +747,17 @@ BOOST_AUTO_TEST_CASE(fixed_tests) // This is actually non-malleable in practice, but we cannot detect it in type system. See above rationale Test("thresh(1,c:pk_k(03d30199d74fb5a22d47b6e054e2f378cedacffcb89904a61d75d0dbd407143e65),altv:after(1000000000),altv:after(100))", "?", "?", TESTMODE_VALID); // thresh with k = 1 - // OP_INTERNALKEY tests + // OP_INTERNALKEY, OP_TEMPLATEHASH tests Test("and_b(older(42),sc:pk_i())", "012ab27ccbac9a", "012ab27ccbac9a", TESTMODE_VALID | TESTMODE_NONMAL | TESTMODE_NEEDSIG | TESTMODE_P2WSH_INVALID); Test("and_b(older(42),s:pki())", "012ab27ccbac9a", "012ab27ccbac9a", TESTMODE_VALID | TESTMODE_NONMAL | TESTMODE_NEEDSIG | TESTMODE_P2WSH_INVALID); std::string ms_ik{"and_b(older(42),ac:or_i(pk_i(),pk_h("}; ms_ik += HexStr(g_testdata->pubkeys[21]) + ")))"; Test(ms_ik, "?", "?", TESTMODE_VALID | TESTMODE_NONMAL | TESTMODE_NEEDSIG | TESTMODE_P2WSH_INVALID); + Test("th(8bbd5b2f0852f2b4d3844bbec1628821c3ea6eeb117ded96558b48e36b27e45d)", "", "208bbd5b2f0852f2b4d3844bbec1628821c3ea6eeb117ded96558b48e36b27e45dce87", TESTMODE_VALID | TESTMODE_NONMAL | TESTMODE_NEEDSIG | TESTMODE_P2WSH_INVALID); + Test("or_i(pk(03d30199d74fb5a22d47b6e054e2f378cedacffcb89904a61d75d0dbd407143e65),and_v(v:th(d180e2ecc3e5a2360a5569c1ace901550b9bc6939b96ffa0f1403db8581e56f1),pk(037c04d6fdc6920f2f278f6d479f64f765c0e0421e9d4233325c0ce7e7253088ee)))", "", "?", TESTMODE_VALID | TESTMODE_NONMAL | TESTMODE_NEEDSIG | TESTMODE_P2WSH_INVALID); + std::string ms_th{"and_b(older(42),a:th("}; + ms_th += HexStr(TestData::MESSAGE_HASH) + "))"; + Test(ms_th, "", "?", TESTMODE_VALID | TESTMODE_NONMAL | TESTMODE_NEEDSIG | TESTMODE_P2WSH_INVALID); g_testdata.reset(); } diff --git a/test/functional/wallet_miniscript.py b/test/functional/wallet_miniscript.py index de4ea277f9b3..c0d5db064ed9 100755 --- a/test/functional/wallet_miniscript.py +++ b/test/functional/wallet_miniscript.py @@ -57,6 +57,8 @@ f"tr(4d54bb9928a0683b7e383de72943b214b0716f58aa54c7ba6bcea2328bc9c768,{{{{{P2WSH_MINISCRIPTS[0]},{P2WSH_MINISCRIPTS[1]}}},{{{P2WSH_MINISCRIPTS[2].replace('multi', 'multi_a')},{P2WSH_MINISCRIPTS[3]}}}}})", # A Taproot with an OP_INTERNALKEY in one of the leaves. f"tr({TPUBS[0]}/*,{{thresh(2,pki(),a:multi_a(1,{TPUBS[1]}/*)),pk({TPUBS[2]})}})", + # A Taproot with a leaf that may only be spent by a specific transaction. + f"tr({TPUBS[0]}/*,th(54ab1fa5f9ea585d0f9674163276bbbde113a9f3328034977a3b3170cc3a9234))", ] DESCS_PRIV = [ From 38a856244a5eb1ece5cc9cde107736c14a5e2d51 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Thu, 15 Jan 2026 15:59:20 -0500 Subject: [PATCH 07/16] miniscript: make key fragments aware of signature type required The satisfaction algorithm checks for signatures when going over key fragments, as a signature-checking fragment may have more than one key fragment as "descendant" where not all of them are available or some are more desirable to use than others. The key fragments have therefore no knowledge of the message to be signed to satisfy their signature-checking "ancestor" fragment. This was not an issue up until now since Miniscript only supported transaction signature checking, and therefore signatures had implicitly to be provided for the transaction itself. But we are about to introduce a Miniscript fragment that check signatures for arbitrary messages in the upcoming commits. Therefore in preparation make key fragments aware of the message their "ancestor" signature-checking fragment expect them to be signing. --- src/script/miniscript.h | 39 ++++++++++++++++++++++++++++------- src/script/sign.cpp | 6 ++++-- src/test/fuzz/miniscript.cpp | 3 ++- src/test/miniscript_tests.cpp | 3 ++- 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/script/miniscript.h b/src/script/miniscript.h index 6babd45e2a2a..437f1530ee73 100644 --- a/src/script/miniscript.h +++ b/src/script/miniscript.h @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -243,6 +244,17 @@ enum class Availability { MAYBE, }; +struct NoSig {}; +struct TxSig {}; +struct CustomSig { + std::span msg; +}; +/** The type of message for a signature check. May be NoSig if no parent of a fragment comports a + * signature check, TxSig if the parent (or an ancestor) of a fragment is a regular transaction + * signature check (CHECKSIG and friends), and CustomSig if it is a signature check for an arbitrary + * message. */ +using SigMsgType = std::variant; + enum class MiniscriptContext { P2WSH, TAPSCRIPT, @@ -1215,22 +1227,32 @@ struct Node { internal::InputResult ProduceInput(const Ctx& ctx) const { using namespace internal; + // Forward down the type of message an upper signature (if any) is for. This is because signature satisfaction + // happens at the key fragment level, which may be unaware of the message to provide a signature for. + auto downfn = [](SigMsgType sig_type, const Node& node, size_t child_index) -> SigMsgType { + if (node.fragment == Fragment::WRAP_C) { + return TxSig{}; + } + return sig_type; + }; + // Internal function which is invoked for every tree node, constructing satisfaction/dissatisfactions // given those of its subnodes. - auto helper = [&ctx](const Node& node, Span subres) -> InputResult { + auto helper = [&ctx](SigMsgType sig_type, const Node& node, Span subres) -> InputResult { switch (node.fragment) { case Fragment::PK_K: case Fragment::PK_I: { std::vector sig; - Availability avail = ctx.Sign(node.keys[0], sig); + Availability avail = ctx.Sign(node.keys[0], sig_type, sig); return {ZERO, InputStack(std::move(sig)).SetWithSig().SetAvailable(avail)}; } case Fragment::PK_H: { std::vector key = ctx.ToPKBytes(node.keys[0]), sig; - Availability avail = ctx.Sign(node.keys[0], sig); + Availability avail = ctx.Sign(node.keys[0], sig_type, sig); return {ZERO + InputStack(key), (InputStack(std::move(sig)).SetWithSig() + InputStack(key)).SetAvailable(avail)}; } case Fragment::MULTI_A: { + const SigMsgType sig_type{TxSig{}}; // sats[j] represents the best stack containing j valid signatures (out of the first i keys). // In the loop below, these stacks are built up using a dynamic programming approach. std::vector sats = Vector(EMPTY); @@ -1238,7 +1260,7 @@ struct Node { // Get the signature for the i'th key in reverse order (the signature for the first key needs to // be at the top of the stack, contrary to CHECKMULTISIG's satisfaction). std::vector sig; - Availability avail = ctx.Sign(node.keys[node.keys.size() - 1 - i], sig); + Availability avail = ctx.Sign(node.keys[node.keys.size() - 1 - i], sig_type, sig); // Compute signature stack for just this key. auto sat = InputStack(std::move(sig)).SetWithSig().SetAvailable(avail); // Compute the next sats vector: next_sats[0] is a copy of sats[0] (no signatures). All further @@ -1259,13 +1281,14 @@ struct Node { return {std::move(nsat), std::move(sats[node.k])}; } case Fragment::MULTI: { + const SigMsgType sig_type{TxSig{}}; // sats[j] represents the best stack containing j valid signatures (out of the first i keys). // In the loop below, these stacks are built up using a dynamic programming approach. // sats[0] starts off being {0}, due to the CHECKMULTISIG bug that pops off one element too many. std::vector sats = Vector(ZERO); for (size_t i = 0; i < node.keys.size(); ++i) { std::vector sig; - Availability avail = ctx.Sign(node.keys[i], sig); + Availability avail = ctx.Sign(node.keys[i], sig_type, sig); // Compute signature stack for just the i'th key. auto sat = InputStack(std::move(sig)).SetWithSig().SetAvailable(avail); // Compute the next sats vector: next_sats[0] is a copy of sats[0] (no signatures). All further @@ -1421,8 +1444,8 @@ struct Node { return {INVALID, INVALID}; }; - auto tester = [&helper](const Node& node, Span subres) -> InputResult { - auto ret = helper(node, subres); + auto tester = [&helper](SigMsgType sig_type, const Node& node, Span subres) -> InputResult { + auto ret = helper(sig_type, node, subres); // Do a consistency check between the satisfaction code and the type checker // (the actual satisfaction code in ProduceInputHelper does not use GetType) @@ -1464,7 +1487,7 @@ struct Node { return ret; }; - return TreeEval(tester); + return TreeEval(SigMsgType{}, downfn, tester); } public: diff --git a/src/script/sign.cpp b/src/script/sign.cpp index 0791e42f4b84..a9c59bede08a 100644 --- a/src/script/sign.cpp +++ b/src/script/sign.cpp @@ -291,7 +291,8 @@ struct WshSatisfier: Satisfier { } //! Satisfy an ECDSA signature check. - miniscript::Availability Sign(const CPubKey& key, std::vector& sig) const { + miniscript::Availability Sign(const CPubKey& key, const miniscript::SigMsgType& sig_type, std::vector& sig) const { + CHECK_NONFATAL(std::holds_alternative(sig_type)); if (CreateSig(m_creator, m_sig_data, m_provider, sig, key, m_witness_script, SigVersion::WITNESS_V0)) { return miniscript::Availability::YES; } @@ -330,7 +331,8 @@ struct TapSatisfier: Satisfier { } //! Satisfy a BIP340 signature check. - miniscript::Availability Sign(const XOnlyPubKey& key, std::vector& sig) const { + miniscript::Availability Sign(const XOnlyPubKey& key, const miniscript::SigMsgType& sig_type, std::vector& sig) const { + CHECK_NONFATAL(std::holds_alternative(sig_type)); if (CreateTaprootScriptSig(m_creator, m_sig_data, m_provider, sig, key, m_leaf_hash, SigVersion::TAPSCRIPT)) { return miniscript::Availability::YES; } diff --git a/src/test/fuzz/miniscript.cpp b/src/test/fuzz/miniscript.cpp index b01f937f2305..55cd4f3f2e25 100644 --- a/src/test/fuzz/miniscript.cpp +++ b/src/test/fuzz/miniscript.cpp @@ -268,7 +268,8 @@ struct SatisfierContext : ParserContext { bool CheckOlder(uint32_t value) const { return value % 2; } // Signature challenges fulfilled with a dummy signature, if it was one of our dummy keys. - miniscript::Availability Sign(const CPubKey& key, std::vector& sig) const { + miniscript::Availability Sign(const CPubKey& key, const miniscript::SigMsgType& sig_type, std::vector& sig) const { + Assert(std::holds_alternative(sig_type)); bool sig_available{false}; if (auto res = TEST_DATA.GetSig(script_ctx, key)) { std::tie(sig, sig_available) = *res; diff --git a/src/test/miniscript_tests.cpp b/src/test/miniscript_tests.cpp index de5675ff2354..b5f611e637d2 100644 --- a/src/test/miniscript_tests.cpp +++ b/src/test/miniscript_tests.cpp @@ -224,7 +224,8 @@ struct Satisfier : public KeyConverter { } //! Produce a signature for the given key. - miniscript::Availability Sign(const CPubKey& key, std::vector& sig) const { + miniscript::Availability Sign(const CPubKey& key, const miniscript::SigMsgType& sig_type, std::vector& sig) const { + Assert(std::holds_alternative(sig_type)); if (supported.count(Challenge(ChallengeType::PK, ChallengeNumber(key)))) { if (!miniscript::IsTapscript(m_script_ctx)) { auto it = g_testdata->signatures.find(key); From 32d468d551f497051eb38c288c2dece8fe452d38 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Thu, 15 Jan 2026 17:14:59 -0500 Subject: [PATCH 08/16] miniscript: introduce a fragment to check a signature against an arbitrary message A cms() fragment that takes as inputs a key expression and a message to check the signature against. The message is forwarded to key expressions and the satisfier made aware of a potential custom message to sign in place of the customary sighash. The chosen order of arguments did not require introduce more state to the parser, but did require introducing more to the decoder (where previously it was only necessary for thresh()). --- src/script/miniscript.cpp | 22 +++++++-- src/script/miniscript.h | 95 ++++++++++++++++++++++++++++++++++-- src/script/sign.cpp | 25 +++++++--- src/script/sign.h | 4 +- src/test/fuzz/miniscript.cpp | 60 ++++++++++++++++++++++- 5 files changed, 187 insertions(+), 19 deletions(-) diff --git a/src/script/miniscript.cpp b/src/script/miniscript.cpp index 88e7fe972d12..2c389f0c1f14 100644 --- a/src/script/miniscript.cpp +++ b/src/script/miniscript.cpp @@ -43,7 +43,7 @@ Type ComputeType(Fragment fragment, Type x, Type y, Type z, const std::vector& data) { switch (fragment) { case Fragment::JUST_1: case Fragment::JUST_0: return 1; @@ -291,6 +296,7 @@ size_t ComputeScriptLen(Fragment fragment, Type sub0typ, size_t subsize, uint32_ case Fragment::WRAP_N: case Fragment::AND_B: case Fragment::OR_B: return subsize + 1; + case Fragment::CMS: return subsize + BuildScript(data).size() + 2; case Fragment::WRAP_A: case Fragment::OR_C: return subsize + 2; case Fragment::WRAP_D: @@ -436,5 +442,15 @@ int FindNextChar(Span sp, const char m) return -1; } +std::optional, int>> ParseArbHexStrEnd(Span in) +{ + int size = FindNextChar(in, ')'); + if (size < 1) return {}; + std::string hex{in.begin(), in.begin() + size}; + if (!IsHex(hex)) return {}; + auto data = ParseHex(hex); + return {{std::move(data), size}}; +} + } // namespace internal } // namespace miniscript diff --git a/src/script/miniscript.h b/src/script/miniscript.h index 437f1530ee73..346b077021f8 100644 --- a/src/script/miniscript.h +++ b/src/script/miniscript.h @@ -222,6 +222,7 @@ enum class Fragment { WRAP_V, //!< [X] OP_VERIFY (or -VERIFY version of last opcode in X) WRAP_J, //!< OP_SIZE OP_0NOTEQUAL OP_IF [X] OP_ENDIF WRAP_N, //!< [X] OP_0NOTEQUAL + CMS, //!< [X] OP_SWAP OP_CHECKSIGVERIFY AND_V, //!< [X] [Y] AND_B, //!< [X] [Y] OP_BOOLAND OR_B, //!< [X] [Y] OP_BOOLOR @@ -304,7 +305,7 @@ constexpr uint32_t MaxScriptSize(MiniscriptContext ms_ctx) Type ComputeType(Fragment fragment, Type x, Type y, Type z, const std::vector& sub_types, uint32_t k, size_t data_size, size_t n_subs, size_t n_keys, MiniscriptContext ms_ctx); //! Helper function for Node::CalcScriptLen. -size_t ComputeScriptLen(Fragment fragment, Type sub0typ, size_t subsize, uint32_t k, size_t n_subs, size_t n_keys, MiniscriptContext ms_ctx); +size_t ComputeScriptLen(Fragment fragment, Type sub0typ, size_t subsize, uint32_t k, size_t n_subs, size_t n_keys, MiniscriptContext ms_ctx, const std::vector& data); //! A helper sanitizer/checker for the output of CalcType. Type SanitizeType(Type x); @@ -494,6 +495,7 @@ struct SatInfo { static constexpr SatInfo OP_EQUAL() noexcept { return {1, 1}; } static constexpr SatInfo OP_SIZE() noexcept { return {-1, 0}; } static constexpr SatInfo OP_CHECKSIG() noexcept { return {1, 1}; } + static constexpr SatInfo OP_CSFS() noexcept { return {2, 2}; } static constexpr SatInfo OP_0NOTEQUAL() noexcept { return {0, 0}; } static constexpr SatInfo OP_VERIFY() noexcept { return {1, 1}; } }; @@ -593,7 +595,7 @@ struct Node { } static constexpr auto NONE_MST{""_mst}; Type sub0type = subs.size() > 0 ? subs[0]->GetType() : NONE_MST; - return internal::ComputeScriptLen(fragment, sub0type, subsize, k, subs.size(), keys.size(), m_script_ctx); + return internal::ComputeScriptLen(fragment, sub0type, subsize, k, subs.size(), keys.size(), m_script_ctx, data); } /* Apply a recursive algorithm to a Miniscript tree, without actual recursive calls. @@ -811,6 +813,10 @@ struct Node { } case Fragment::WRAP_J: return BuildScript(OP_SIZE, OP_0NOTEQUAL, OP_IF, subs[0], OP_ENDIF); case Fragment::WRAP_N: return BuildScript(std::move(subs[0]), OP_0NOTEQUAL); + case Fragment::CMS: { + CHECK_NONFATAL(is_tapscript); + return BuildScript(std::move(subs[0]), node.data, OP_SWAP, OP_CHECKSIGFROMSTACK); + } case Fragment::JUST_1: return BuildScript(OP_1); case Fragment::JUST_0: return BuildScript(OP_0); case Fragment::AND_V: return BuildScript(std::move(subs[0]), subs[1]); @@ -928,6 +934,10 @@ struct Node { case Fragment::RIPEMD160: return std::move(ret) + "ripemd160(" + HexStr(node.data) + ")"; case Fragment::JUST_1: return std::move(ret) + "1"; case Fragment::JUST_0: return std::move(ret) + "0"; + case Fragment::CMS: { + CHECK_NONFATAL(is_tapscript); + return std::move(ret) + "cms(" + std::move(subs[0]) + "," + HexStr(node.data) + ")"; + } case Fragment::AND_V: return std::move(ret) + "and_v(" + std::move(subs[0]) + "," + std::move(subs[1]) + ")"; case Fragment::AND_B: return std::move(ret) + "and_b(" + std::move(subs[0]) + "," + std::move(subs[1]) + ")"; case Fragment::OR_B: return std::move(ret) + "or_b(" + std::move(subs[0]) + "," + std::move(subs[1]) + ")"; @@ -1033,6 +1043,7 @@ struct Node { case Fragment::WRAP_D: return {3 + subs[0]->ops.count, subs[0]->ops.sat, 0}; case Fragment::WRAP_J: return {4 + subs[0]->ops.count, subs[0]->ops.sat, 0}; case Fragment::WRAP_V: return {subs[0]->ops.count + (subs[0]->GetType() << "x"_mst), subs[0]->ops.sat, {}}; + case Fragment::CMS: return {2 + subs[0]->ops.count, subs[0]->ops.sat, subs[0]->ops.dsat}; case Fragment::THRESH: { uint32_t count = 0; auto sats = Vector(internal::MaxInt(0)); @@ -1139,6 +1150,11 @@ struct Node { SatInfo::OP_SIZE() + SatInfo::OP_0NOTEQUAL() + SatInfo::If() + subs[0]->ss.sat, SatInfo::OP_SIZE() + SatInfo::OP_0NOTEQUAL() + SatInfo::If() }; + case Fragment::CMS: return { + // message + CSFS + subs[0]->ss.sat + SatInfo::Push() + SatInfo::OP_CSFS(), + subs[0]->ss.dsat + SatInfo::Push() + SatInfo::OP_CSFS(), + }; case Fragment::THRESH: { // sats[j] is the SatInfo corresponding to all traces reaching j satisfactions. auto sats = Vector(SatInfo::Empty()); @@ -1208,6 +1224,10 @@ struct Node { case Fragment::WRAP_D: return {1 + 1 + subs[0]->ws.sat, 1}; case Fragment::WRAP_V: return {subs[0]->ws.sat, {}}; case Fragment::WRAP_J: return {subs[0]->ws.sat, 1}; + case Fragment::CMS: return { + subs[0]->ws.sat + sig_size, + subs[0]->ws.dsat + 1, + }; case Fragment::THRESH: { auto sats = Vector(internal::MaxInt(0)); for (const auto& sub : subs) { @@ -1232,6 +1252,8 @@ struct Node { auto downfn = [](SigMsgType sig_type, const Node& node, size_t child_index) -> SigMsgType { if (node.fragment == Fragment::WRAP_C) { return TxSig{}; + } else if (node.fragment == Fragment::CMS) { + return CustomSig{.msg = std::span{node.data}}; } return sig_type; }; @@ -1415,6 +1437,7 @@ struct Node { auto& x = subres[0], &y = subres[1], &z = subres[2]; return {(y.nsat + x.sat).SetNonCanon() | (z.nsat + x.nsat), (y.sat + x.sat) | (z.sat + x.nsat)}; } + case Fragment::CMS: case Fragment::WRAP_A: case Fragment::WRAP_S: case Fragment::WRAP_C: @@ -1794,6 +1817,9 @@ enum class ParseContext { /** OR_I will construct an or_i node from the last two constructed nodes. */ OR_I, + /** CMS will construct a cms(X, m) from the last constructed node and a given message. */ + CMS, + /** THRESH will read a wrapped expression, and then look for a COMMA. If * no comma follows, it will construct a thresh node from the appropriate * number of constructed children. Otherwise, it will recurse with another @@ -1833,6 +1859,9 @@ std::optional, int>> ParseHexStrEnd(Span, int>> ParseArbHexStrEnd(Span in); + /** BuildBack pops the last two elements off `constructed` and wraps them in the specified Fragment */ template void BuildBack(const MiniscriptContext script_ctx, Fragment nt, std::vector>& constructed, const bool reverse = false) @@ -2071,6 +2100,12 @@ inline NodeRef Parse(Span in, const Ctx& ctx) constructed.push_back(MakeNodeRef(internal::NoDupCheck{}, ctx.MsContext(), Fragment::OLDER, *num)); in = in.subspan(arg_size + 1); script_size += 1 + (*num > 16) + (*num > 0x7f) + (*num > 0x7fff) + (*num > 0x7fffff); + } else if (Const("cms(", in)) { + if (!IsTapscript(ctx.MsContext())) return {}; + to_parse.emplace_back(ParseContext::CMS, -1, -1); + to_parse.emplace_back(ParseContext::COMMA, -1, -1); + to_parse.emplace_back(ParseContext::WRAPPED_EXPR, -1, -1); + // Script size is accounted for after parsing the message in ParseContext::CMS. } else if (Const("multi(", in)) { if (!parse_multi_exp(in, /* is_multi_a = */false)) return {}; } else if (Const("multi_a(", in)) { @@ -2126,6 +2161,15 @@ inline NodeRef Parse(Span in, const Ctx& ctx) } break; } + case ParseContext::CMS: { + auto res = ParseArbHexStrEnd(in); + if (!res) return {}; + auto& [msg, msg_size] = *res; + in = in.subspan(msg_size + 1); + script_size += BuildScript(msg).size() + 2; + constructed.back() = MakeNodeRef(internal::NoDupCheck{}, ctx.MsContext(), Fragment::CMS, Vector(std::move(constructed.back())), std::move(msg)); + break; + } case ParseContext::ALT: { constructed.back() = MakeNodeRef(internal::NoDupCheck{}, ctx.MsContext(), Fragment::WRAP_A, Vector(std::move(constructed.back()))); break; @@ -2287,6 +2331,10 @@ enum class DecodeContext { /** ZERO_NOTEQUAL wraps the top constructed node with n: */ ZERO_NOTEQUAL, + /** Wraps the top of the constructed stack with a CHECKSIGFROMSTACK against + * a previously-read message. */ + CHECK_MSG, + /** MAYBE_AND_V will check if the next part of the script could be a valid * miniscript sub-expression, and if so it will push AND_V and SINGLE_BKV_EXPR * to decode it and construct the and_v node. This is recursive, to deal with @@ -2327,12 +2375,26 @@ enum class DecodeContext { ENDIF_ELSE, }; +struct DecodeCtx { + DecodeContext ctx; + int64_t thresh_n; + int64_t thresh_k; + std::optional> cms_msg; + + DecodeCtx(DecodeContext ctx_, int64_t n, int64_t k): + ctx{ctx_}, thresh_n{n}, thresh_k{k}, cms_msg{std::nullopt} {} + DecodeCtx(DecodeContext ctx_, std::vector msg): + ctx{ctx_}, thresh_n{-1}, thresh_k{-1}, cms_msg{std::move(msg)} {} + DecodeCtx(DecodeContext ctx_): + ctx{ctx_}, thresh_n{-1}, thresh_k{-1}, cms_msg{std::nullopt} {} +}; + //! Parse a miniscript from a bitcoin script template inline NodeRef DecodeScript(I& in, I last, const Ctx& ctx) { // The two integers are used to hold state for thresh() - std::vector> to_parse; + std::vector to_parse; std::vector> constructed; // This is the top level, so we assume the type is B @@ -2344,7 +2406,11 @@ inline NodeRef DecodeScript(I& in, I last, const Ctx& ctx) if (!constructed.empty() && !constructed.back()->IsValid()) return {}; // Get the current context we are decoding within - auto [cur_context, n, k] = to_parse.back(); + const auto &context = to_parse.back(); + DecodeContext cur_context = context.ctx; + int64_t n = context.thresh_n; + int64_t k = context.thresh_k; + std::optional> cms_msg = std::move(context.cms_msg); to_parse.pop_back(); switch(cur_context) { @@ -2418,7 +2484,7 @@ inline NodeRef DecodeScript(I& in, I last, const Ctx& ctx) break; } } - if (last - in >= 2 && in[0].first == OP_EQUAL && in[1].first == OP_TEMPLATEHASH) { + if (last - in >= 3 && in[0].first == OP_EQUAL && in[1].first == OP_TEMPLATEHASH) { if (!IsTapscript(ctx.MsContext())) return {}; constructed.push_back(MakeNodeRef(internal::NoDupCheck{}, ctx.MsContext(), Fragment::TH, in[2].second)); in += 3; @@ -2498,6 +2564,19 @@ inline NodeRef DecodeScript(I& in, I last, const Ctx& ctx) to_parse.emplace_back(DecodeContext::SINGLE_BKV_EXPR, -1, -1); break; } + /** The cms(X,message) commutes with and_v() in the same way the c:X wrapper + * above does. Parsing and_v() as the "outer" fragment is preferable as the + * opposite can lead to parsing some scripts as invalid Miniscripts. For instance + * "1 VERIFY SWAP CSFS NOTIF 1 ELSE 1 ENDIF" would otherwise be + * parsed as the invalid `andor(cms(and_v(v:1,pk(X)),message),1,1)` instead of + * the valid `and_v(v:1,andor(cms(pk(X),message),1,1))`. */ + if (last - in >= 3 && in[0].first == OP_CHECKSIGFROMSTACK && in[1].first == OP_SWAP) { + if (!IsTapscript(ctx.MsContext())) return {}; + to_parse.emplace_back(DecodeContext::CHECK_MSG, in[2].second); + to_parse.emplace_back(DecodeContext::SINGLE_BKV_EXPR); + in += 3; + break; + } // Thresh if (last - in >= 3 && in[0].first == OP_EQUAL && (num = ParseScriptNumber(in[1]))) { if (*num < 1) return {}; @@ -2580,6 +2659,12 @@ inline NodeRef DecodeScript(I& in, I last, const Ctx& ctx) constructed.back() = MakeNodeRef(internal::NoDupCheck{}, ctx.MsContext(), Fragment::WRAP_C, Vector(std::move(constructed.back()))); break; } + case DecodeContext::CHECK_MSG: { + if (constructed.empty()) return {}; + CHECK_NONFATAL(cms_msg.has_value()); + constructed.back() = MakeNodeRef(internal::NoDupCheck{}, ctx.MsContext(), Fragment::CMS, Vector(std::move(constructed.back())), std::move(cms_msg.value())); + break; + } case DecodeContext::DUP_IF: { if (constructed.empty()) return {}; constructed.back() = MakeNodeRef(internal::NoDupCheck{}, ctx.MsContext(), Fragment::WRAP_D, Vector(std::move(constructed.back()))); diff --git a/src/script/sign.cpp b/src/script/sign.cpp index a9c59bede08a..027c8532f362 100644 --- a/src/script/sign.cpp +++ b/src/script/sign.cpp @@ -59,7 +59,7 @@ bool MutableTransactionSignatureCreator::CreateSig(const SigningProvider& provid return true; } -bool MutableTransactionSignatureCreator::CreateSchnorrSig(const SigningProvider& provider, std::vector& sig, const XOnlyPubKey& pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion) const +bool MutableTransactionSignatureCreator::CreateSchnorrSig(const SigningProvider& provider, std::vector& sig, const XOnlyPubKey& pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion, std::optional custom_msg) const { assert(sigversion == SigVersion::TAPROOT || sigversion == SigVersion::TAPSCRIPT); @@ -82,7 +82,11 @@ bool MutableTransactionSignatureCreator::CreateSchnorrSig(const SigningProvider& execdata.m_tapleaf_hash = *leaf_hash; } uint256 hash; - if (!SignatureHashSchnorr(hash, execdata, m_txto, nIn, nHashType, sigversion, KeyVersion::TAPROOT, *m_txdata, MissingDataBehavior::FAIL)) return false; + if (custom_msg.has_value()) { + hash = custom_msg.value(); + } else if (!SignatureHashSchnorr(hash, execdata, m_txto, nIn, nHashType, sigversion, KeyVersion::TAPROOT, *m_txdata, MissingDataBehavior::FAIL)) { + return false; + } sig.resize(64); // Use uint256{} as aux_rnd for now. if (!key.SignSchnorr(hash, sig, merkle_root, {})) return false; @@ -151,7 +155,7 @@ static bool CreateSig(const BaseSignatureCreator& creator, SignatureData& sigdat return false; } -static bool CreateTaprootScriptSig(const BaseSignatureCreator& creator, SignatureData& sigdata, const SigningProvider& provider, std::vector& sig_out, const XOnlyPubKey& pubkey, const uint256& leaf_hash, SigVersion sigversion) +static bool CreateTaprootScriptSig(const BaseSignatureCreator& creator, SignatureData& sigdata, const SigningProvider& provider, std::vector& sig_out, const XOnlyPubKey& pubkey, const uint256& leaf_hash, SigVersion sigversion, std::optional custom_msg) { KeyOriginInfo info; if (provider.GetKeyOriginByXOnly(pubkey, info)) { @@ -169,7 +173,7 @@ static bool CreateTaprootScriptSig(const BaseSignatureCreator& creator, Signatur sig_out = it->second; return true; } - if (creator.CreateSchnorrSig(provider, sig_out, pubkey, &leaf_hash, nullptr, sigversion)) { + if (creator.CreateSchnorrSig(provider, sig_out, pubkey, &leaf_hash, nullptr, sigversion, custom_msg)) { sigdata.taproot_script_sigs[lookup_key] = sig_out; return true; } @@ -332,8 +336,15 @@ struct TapSatisfier: Satisfier { //! Satisfy a BIP340 signature check. miniscript::Availability Sign(const XOnlyPubKey& key, const miniscript::SigMsgType& sig_type, std::vector& sig) const { - CHECK_NONFATAL(std::holds_alternative(sig_type)); - if (CreateTaprootScriptSig(m_creator, m_sig_data, m_provider, sig, key, m_leaf_hash, SigVersion::TAPSCRIPT)) { + std::optional custom_msg{}; + if (const auto* custom_sig = std::get_if(&sig_type)) { + // We only support signing for custom messages that are 32-byte long for now. + if (custom_sig->msg.size() != 32) { + return miniscript::Availability::NO; + } + custom_msg = uint256{custom_sig->msg}; + } + if (CreateTaprootScriptSig(m_creator, m_sig_data, m_provider, sig, key, m_leaf_hash, SigVersion::TAPSCRIPT, custom_msg)) { return miniscript::Availability::YES; } return miniscript::Availability::NO; @@ -756,7 +767,7 @@ class DummySignatureCreator final : public BaseSignatureCreator { vchSig[6 + m_r_len + m_s_len] = SIGHASH_ALL; return true; } - bool CreateSchnorrSig(const SigningProvider& provider, std::vector& sig, const XOnlyPubKey& pubkey, const uint256* leaf_hash, const uint256* tweak, SigVersion sigversion) const override + bool CreateSchnorrSig(const SigningProvider& provider, std::vector& sig, const XOnlyPubKey& pubkey, const uint256* leaf_hash, const uint256* tweak, SigVersion sigversion, std::optional) const override { sig.assign(64, '\000'); return true; diff --git a/src/script/sign.h b/src/script/sign.h index fe2c470bc644..ff5c640b740c 100644 --- a/src/script/sign.h +++ b/src/script/sign.h @@ -32,7 +32,7 @@ class BaseSignatureCreator { /** Create a singular (non-script) signature. */ virtual bool CreateSig(const SigningProvider& provider, std::vector& vchSig, const CKeyID& keyid, const CScript& scriptCode, SigVersion sigversion) const =0; - virtual bool CreateSchnorrSig(const SigningProvider& provider, std::vector& sig, const XOnlyPubKey& pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion) const =0; + virtual bool CreateSchnorrSig(const SigningProvider& provider, std::vector& sig, const XOnlyPubKey& pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion, std::optional custom_msg = std::nullopt) const =0; }; /** A signature creator for transactions. */ @@ -50,7 +50,7 @@ class MutableTransactionSignatureCreator : public BaseSignatureCreator MutableTransactionSignatureCreator(const CMutableTransaction& tx LIFETIMEBOUND, unsigned int input_idx, const CAmount& amount, const PrecomputedTransactionData* txdata, int hash_type); const BaseSignatureChecker& Checker() const override { return checker; } bool CreateSig(const SigningProvider& provider, std::vector& vchSig, const CKeyID& keyid, const CScript& scriptCode, SigVersion sigversion) const override; - bool CreateSchnorrSig(const SigningProvider& provider, std::vector& sig, const XOnlyPubKey& pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion) const override; + bool CreateSchnorrSig(const SigningProvider& provider, std::vector& sig, const XOnlyPubKey& pubkey, const uint256* leaf_hash, const uint256* merkle_root, SigVersion sigversion, std::optional custom_msg = std::nullopt) const override; }; /** A signature checker that accepts all signatures */ diff --git a/src/test/fuzz/miniscript.cpp b/src/test/fuzz/miniscript.cpp index 55cd4f3f2e25..c9929785af4e 100644 --- a/src/test/fuzz/miniscript.cpp +++ b/src/test/fuzz/miniscript.cpp @@ -48,6 +48,10 @@ struct TestData { std::map, std::vector> hash256_preimages; std::map, std::vector> hash160_preimages; + // Precomputed 32-byte messages and a valid signatures for each. + std::vector> custom_messages; + std::map, std::vector>> custom_sigs; + //! Set the precomputed data. void Init() { unsigned char keydata[32] = {1}; @@ -92,6 +96,29 @@ struct TestData { CHash160().Write(keydata).Finalize(hash); hash160.push_back(hash); if (i & 1) hash160_preimages[hash] = std::vector(keydata, keydata + 32); + + // Only 3 different custom messages, since we need a valid signature for each. + if (i < 3) { + custom_messages.emplace_back(sha256[i]); + } + } + + for (size_t i{0}; i < 256; ++i) { + // Like for regular signatures, only odd indexes are available. + if ((i & 1) == 0) { + continue; + } + + CKey privkey; + keydata[31] = i; + privkey.Set(keydata, keydata + 32, true); + XOnlyPubKey pubkey{privkey.GetPubKey()}; + + for (const auto& msg: custom_messages) { + std::vector sig(64); + Assert(privkey.SignSchnorr(uint256{msg}, sig, nullptr, EMPTY_AUX)); + custom_sigs[pubkey][msg] = std::move(sig); + } } } @@ -107,6 +134,15 @@ struct TestData { return &it->second; } } + + const std::vector* GetCustomSig(const CPubKey& pubkey, std::span msg) const { + const auto it{custom_sigs.find(XOnlyPubKey{pubkey})}; + if (it == custom_sigs.end()) return nullptr; + std::vector msg_owned{msg.begin(), msg.end()}; + const auto sec_it{it->second.find(msg_owned)}; + if (sec_it == it->second.end()) return nullptr; + return &sec_it->second; + } } TEST_DATA; /** @@ -269,6 +305,13 @@ struct SatisfierContext : ParserContext { // Signature challenges fulfilled with a dummy signature, if it was one of our dummy keys. miniscript::Availability Sign(const CPubKey& key, const miniscript::SigMsgType& sig_type, std::vector& sig) const { + if (const auto* custom = std::get_if(&sig_type)) { + if (const auto* sig_res = TEST_DATA.GetCustomSig(key, custom->msg)) { + sig = *sig_res; + return miniscript::Availability::YES; + } + return miniscript::Availability::NO; + } Assert(std::holds_alternative(sig_type)); bool sig_available{false}; if (auto res = TEST_DATA.GetSig(script_ctx, key)) { @@ -362,6 +405,7 @@ struct NodeInfo { NodeInfo(Fragment frag, std::vector h): fragment(frag), k(0), hash(std::move(h)) {} NodeInfo(std::vector subt, Fragment frag): fragment(frag), k(0), subtypes(std::move(subt)) {} NodeInfo(std::vector subt, Fragment frag, uint32_t _k): fragment(frag), k(_k), subtypes(std::move(subt)) {} + NodeInfo(std::vector subt, Fragment frag, std::vector data): fragment{frag}, k{0}, hash{data}, subtypes{std::move(subt)} {} NodeInfo(Fragment frag, uint32_t _k, std::vector _keys): fragment(frag), k(_k), keys(std::move(_keys)) {} }; @@ -620,7 +664,8 @@ struct SmartInfo switch (frag) { case Fragment::MULTI_A: case Fragment::PK_I: - case Fragment::TH: return true; + case Fragment::TH: + case Fragment::CMS: return true; default: return false; } }}; @@ -656,6 +701,10 @@ struct SmartInfo case Fragment::AFTER: k = 1; break; + case Fragment::CMS: + sub_count = 1; + data_size = 32; + break; case Fragment::SHA256: case Fragment::HASH256: case Fragment::TH: @@ -858,6 +907,10 @@ std::optional ConsumeNodeSmart(MsCtx script_ctx, FuzzedDataProvider& p for (auto& key: keys) key = ConsumePubKey(provider); return {{frag, k, std::move(keys)}}; } + case Fragment::CMS: { + Assert(IsTapscript(script_ctx)); + return {{subt, frag, PickValue(provider, TEST_DATA.custom_messages)}}; + } case Fragment::OLDER: case Fragment::AFTER: return {{frag, provider.ConsumeIntegralInRange(1, 0x7FFFFFF)}}; @@ -941,7 +994,7 @@ NodeRef GenNode(MsCtx script_ctx, F ConsumeNode, Type root_type, std::optionalfragment, ""_mst, node_info->subtypes.size(), node_info->k, node_info->subtypes.size(), - node_info->keys.size(), script_ctx) - 1; + node_info->keys.size(), script_ctx, node_info->hash) - 1; if (scriptsize > MAX_STANDARD_P2WSH_SCRIPT_SIZE) return {}; switch (node_info->fragment) { case Fragment::JUST_0: @@ -986,6 +1039,9 @@ NodeRef GenNode(MsCtx script_ctx, F ConsumeNode, Type root_type, std::optionalsubtypes.size(); break; From 79f420fb1e1653f91a78e39945875c8eb7bb57c9 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Tue, 20 Jan 2026 12:31:22 -0500 Subject: [PATCH 09/16] qa: sanity check cms() fragment usage in descriptors --- src/test/descriptor_tests.cpp | 27 +++++++++- src/test/miniscript_tests.cpp | 73 ++++++++++++++++++++++++++-- test/functional/wallet_miniscript.py | 19 ++++++++ 3 files changed, 114 insertions(+), 5 deletions(-) diff --git a/src/test/descriptor_tests.cpp b/src/test/descriptor_tests.cpp index 35bc6f75679c..641bd3478b19 100644 --- a/src/test/descriptor_tests.cpp +++ b/src/test/descriptor_tests.cpp @@ -1072,7 +1072,7 @@ BOOST_AUTO_TEST_CASE(descriptor_test) // Infer pk() from p2pk with uncompressed key CheckInferDescriptor("4104032540df1d3c7070a8ab3a9cdd304dfc7fd1e6541369c53c4c3310b2537d91059afc8b8e7673eb812a32978dabb78c40f2e423f7757dca61d11838c7aeeb5220ac", "pk(04032540df1d3c7070a8ab3a9cdd304dfc7fd1e6541369c53c4c3310b2537d91059afc8b8e7673eb812a32978dabb78c40f2e423f7757dca61d11838c7aeeb5220)", {}, {{"04032540df1d3c7070a8ab3a9cdd304dfc7fd1e6541369c53c4c3310b2537d91059afc8b8e7673eb812a32978dabb78c40f2e423f7757dca61d11838c7aeeb5220", ""}}); - // OP_INTERNALKEY, OP_TEMPLATEHASH tests + // OP_INTERNALKEY, OP_TEMPLATEHASH and OP_CHECKSIGFROMSTACK tests CheckMultipath("tr(xprv9yYge4PS54XkYT9KiLfCRwc8Jeuz8DucxQGtuEecJZYhKNiqbPxYHTPzXtskmzWBqdqkRAGsghNmZzNsfU2wstaB3XjDQFPv567aQSSuPyo/<2;3>/*,l:pki())", "tr(xpub6CY33ZvKuS63kwDnpNCCo5YrrgkUXgdUKdCVhd4Dru5gCB3z8wGnqFiUP98Za5pYSYF5KmvBHTY3Ra8FAJGggzBjuHS69WzN8gscPupuZwK/<2;3>/*,l:pki())", { @@ -1099,6 +1099,31 @@ BOOST_AUTO_TEST_CASE(descriptor_test) } ); Check("tr(xprv9yYge4PS54XkYT9KiLfCRwc8Jeuz8DucxQGtuEecJZYhKNiqbPxYHTPzXtskmzWBqdqkRAGsghNmZzNsfU2wstaB3XjDQFPv567aQSSuPyo/0/*,th(e8a8c07ee3bfdc31a2b2c79c796346da139ae1810cd456a4d4dda86a9f522937))", "tr(xpub6CY33ZvKuS63kwDnpNCCo5YrrgkUXgdUKdCVhd4Dru5gCB3z8wGnqFiUP98Za5pYSYF5KmvBHTY3Ra8FAJGggzBjuHS69WzN8gscPupuZwK/0/*,th(e8a8c07ee3bfdc31a2b2c79c796346da139ae1810cd456a4d4dda86a9f522937))", "tr(xpub6CY33ZvKuS63kwDnpNCCo5YrrgkUXgdUKdCVhd4Dru5gCB3z8wGnqFiUP98Za5pYSYF5KmvBHTY3Ra8FAJGggzBjuHS69WzN8gscPupuZwK/0/*,th(e8a8c07ee3bfdc31a2b2c79c796346da139ae1810cd456a4d4dda86a9f522937))", XONLY_KEYS | RANGE, {{"51203ea124787c99ae093e931684d25c9acdb665b87089c018086584fa9014880a38"}}, OutputType::BECH32M, /*op_desc_id=*/{}, {{0, 0}}); + CheckMultipath("tr(xprv9yYge4PS54XkYT9KiLfCRwc8Jeuz8DucxQGtuEecJZYhKNiqbPxYHTPzXtskmzWBqdqkRAGsghNmZzNsfU2wstaB3XjDQFPv567aQSSuPyo/<2;3>/*,and_v(v:th(e8a8c07ee3bfdc31a2b2c79c796346da139ae1810cd456a4d4dda86a9f522937),cms(pk_i(),ab21)))", + "tr(xpub6CY33ZvKuS63kwDnpNCCo5YrrgkUXgdUKdCVhd4Dru5gCB3z8wGnqFiUP98Za5pYSYF5KmvBHTY3Ra8FAJGggzBjuHS69WzN8gscPupuZwK/<2;3>/*,and_v(v:th(e8a8c07ee3bfdc31a2b2c79c796346da139ae1810cd456a4d4dda86a9f522937),cms(pk_i(),ab21)))", + { + "tr(xprv9yYge4PS54XkYT9KiLfCRwc8Jeuz8DucxQGtuEecJZYhKNiqbPxYHTPzXtskmzWBqdqkRAGsghNmZzNsfU2wstaB3XjDQFPv567aQSSuPyo/2/*,and_v(v:th(e8a8c07ee3bfdc31a2b2c79c796346da139ae1810cd456a4d4dda86a9f522937),cms(pk_i(),ab21)))", + "tr(xprv9yYge4PS54XkYT9KiLfCRwc8Jeuz8DucxQGtuEecJZYhKNiqbPxYHTPzXtskmzWBqdqkRAGsghNmZzNsfU2wstaB3XjDQFPv567aQSSuPyo/3/*,and_v(v:th(e8a8c07ee3bfdc31a2b2c79c796346da139ae1810cd456a4d4dda86a9f522937),cms(pk_i(),ab21)))", + }, + { + "tr(xpub6CY33ZvKuS63kwDnpNCCo5YrrgkUXgdUKdCVhd4Dru5gCB3z8wGnqFiUP98Za5pYSYF5KmvBHTY3Ra8FAJGggzBjuHS69WzN8gscPupuZwK/2/*,and_v(v:th(e8a8c07ee3bfdc31a2b2c79c796346da139ae1810cd456a4d4dda86a9f522937),cms(pk_i(),ab21)))", + "tr(xpub6CY33ZvKuS63kwDnpNCCo5YrrgkUXgdUKdCVhd4Dru5gCB3z8wGnqFiUP98Za5pYSYF5KmvBHTY3Ra8FAJGggzBjuHS69WzN8gscPupuZwK/3/*,and_v(v:th(e8a8c07ee3bfdc31a2b2c79c796346da139ae1810cd456a4d4dda86a9f522937),cms(pk_i(),ab21)))", + }, + { + "tr(xpub6CY33ZvKuS63kwDnpNCCo5YrrgkUXgdUKdCVhd4Dru5gCB3z8wGnqFiUP98Za5pYSYF5KmvBHTY3Ra8FAJGggzBjuHS69WzN8gscPupuZwK/2/*,and_v(v:th(e8a8c07ee3bfdc31a2b2c79c796346da139ae1810cd456a4d4dda86a9f522937),cms(pk_i(),ab21)))", + "tr(xpub6CY33ZvKuS63kwDnpNCCo5YrrgkUXgdUKdCVhd4Dru5gCB3z8wGnqFiUP98Za5pYSYF5KmvBHTY3Ra8FAJGggzBjuHS69WzN8gscPupuZwK/3/*,and_v(v:th(e8a8c07ee3bfdc31a2b2c79c796346da139ae1810cd456a4d4dda86a9f522937),cms(pk_i(),ab21)))", + }, + XONLY_KEYS | RANGE, + { + {{"51209fb3202ec1b492f6d72c5154736f9fb2dc3f6122f6f59649c018553b5a7ae796"}, {"51206e46c6c0cc6b8dd0649658e6876cf02c015f8ea94508d0628bc8bde77be9e00f"}}, + {{"5120bd7ef4a4b68935d86b8fc5a94e1bab4dee38262fa424bcf2691df05c9852dcb4"}, {"5120121bde8c7182731d7a9216f779c8a87dc6207070b82a208e73e5b2f73b0de6a0"}}, + }, + OutputType::BECH32M, + { + {{2, 0}, {2, 1}}, + {{3, 0}, {3, 1}}, + } + ); } BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/miniscript_tests.cpp b/src/test/miniscript_tests.cpp index b5f611e637d2..a685d351fb3f 100644 --- a/src/test/miniscript_tests.cpp +++ b/src/test/miniscript_tests.cpp @@ -51,15 +51,19 @@ struct TestData { std::map, std::vector> hash256_preimages; std::map, std::vector> hash160_preimages; + // Precomputed 32-byte messages and a valid signatures for each. + std::vector> custom_messages; + std::map, std::vector>> custom_sigs; + TestData() { // We don't pass additional randomness when creating a schnorr signature. const auto EMPTY_AUX{uint256::ZERO}; + // This 32-byte array functions as both private key data and hash preimage (31 zero bytes plus any nonzero byte). + unsigned char keydata[32] = {0}; // We generate 255 public keys and 255 hashes of each type. for (int i = 1; i <= 255; ++i) { - // This 32-byte array functions as both private key data and hash preimage (31 zero bytes plus any nonzero byte). - unsigned char keydata[32] = {0}; keydata[31] = i; // Compute CPubkey and CKeyID @@ -100,6 +104,24 @@ struct TestData { CHash160().Write(keydata).Finalize(hash); hash160.push_back(hash); hash160_preimages[hash] = std::vector(keydata, keydata + 32); + + // Only 3 different custom messages, since we need a valid signature for each. + if (i < 4) { + custom_messages.emplace_back(sha256.back()); + } + } + + for (size_t i{1}; i <= 255; ++i) { + CKey privkey; + keydata[31] = i; + privkey.Set(keydata, keydata + 32, true); + XOnlyPubKey pubkey{privkey.GetPubKey()}; + + for (const auto& msg: custom_messages) { + std::vector sig(64); + Assert(privkey.SignSchnorr(uint256{msg}, sig, nullptr, EMPTY_AUX)); + custom_sigs[pubkey][msg] = std::move(sig); + } } } }; @@ -225,8 +247,18 @@ struct Satisfier : public KeyConverter { //! Produce a signature for the given key. miniscript::Availability Sign(const CPubKey& key, const miniscript::SigMsgType& sig_type, std::vector& sig) const { - Assert(std::holds_alternative(sig_type)); if (supported.count(Challenge(ChallengeType::PK, ChallengeNumber(key)))) { + if (const auto* custom = std::get_if(&sig_type)) { + const auto it{g_testdata->custom_sigs.find(XOnlyPubKey{key})}; + if (it == g_testdata->custom_sigs.end()) return miniscript::Availability::NO; + std::vector msg_owned{custom->msg.begin(), custom->msg.end()}; + const auto sec_it{it->second.find(msg_owned)}; + if (sec_it == it->second.end()) return miniscript::Availability::NO; + sig = sec_it->second; + return miniscript::Availability::YES; + } + + Assert(std::holds_alternative(sig_type)); if (!miniscript::IsTapscript(m_script_ctx)) { auto it = g_testdata->signatures.find(key); if (it == g_testdata->signatures.end()) return miniscript::Availability::NO; @@ -748,7 +780,7 @@ BOOST_AUTO_TEST_CASE(fixed_tests) // This is actually non-malleable in practice, but we cannot detect it in type system. See above rationale Test("thresh(1,c:pk_k(03d30199d74fb5a22d47b6e054e2f378cedacffcb89904a61d75d0dbd407143e65),altv:after(1000000000),altv:after(100))", "?", "?", TESTMODE_VALID); // thresh with k = 1 - // OP_INTERNALKEY, OP_TEMPLATEHASH tests + // OP_INTERNALKEY, OP_TEMPLATEHASH and OP_CHECKSIGFROMSTACK tests Test("and_b(older(42),sc:pk_i())", "012ab27ccbac9a", "012ab27ccbac9a", TESTMODE_VALID | TESTMODE_NONMAL | TESTMODE_NEEDSIG | TESTMODE_P2WSH_INVALID); Test("and_b(older(42),s:pki())", "012ab27ccbac9a", "012ab27ccbac9a", TESTMODE_VALID | TESTMODE_NONMAL | TESTMODE_NEEDSIG | TESTMODE_P2WSH_INVALID); std::string ms_ik{"and_b(older(42),ac:or_i(pk_i(),pk_h("}; @@ -759,6 +791,39 @@ BOOST_AUTO_TEST_CASE(fixed_tests) std::string ms_th{"and_b(older(42),a:th("}; ms_th += HexStr(TestData::MESSAGE_HASH) + "))"; Test(ms_th, "", "?", TESTMODE_VALID | TESTMODE_NONMAL | TESTMODE_NEEDSIG | TESTMODE_P2WSH_INVALID); + Test(ms_th, "?", "?", TESTMODE_VALID | TESTMODE_NONMAL | TESTMODE_NEEDSIG | TESTMODE_P2WSH_INVALID); + Test("cms(pk_k(02e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13),ec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc5)", "20e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd1320ec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc57ccc", "20e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd1320ec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc57ccc", TESTMODE_VALID | TESTMODE_NONMAL | TESTMODE_P2WSH_INVALID); + Test("or_i(and_b(hash160(20195b5a3d650c17f0f29f91c33f8f6335193d07),a:cms(pk_k(02e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13),ec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc5)),and_v(v:older(42),pkh(025cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc)))", "6382012088a91420195b5a3d650c17f0f29f91c33f8f6335193d07876b20e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd1320ec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc57ccc6c9a67012ab26976a9141a7ac36cfa8431ab2395d701b0050045ae4a37d188ac68", "6382012088a91420195b5a3d650c17f0f29f91c33f8f6335193d07876b20e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd1320ec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc57ccc6c9a67012ab26976a9141a7ac36cfa8431ab2395d701b0050045ae4a37d188ac68", TESTMODE_VALID | TESTMODE_NONMAL | TESTMODE_P2WSH_INVALID); + Test("or_i(and_b(hash160(20195b5a3d650c17f0f29f91c33f8f6335193d07),a:cms(pk_k(02e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13),424242babaffec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc5)),and_v(v:older(42),pkh(025cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc)))", "6382012088a91420195b5a3d650c17f0f29f91c33f8f6335193d07876b20e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd1326424242babaffec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc57ccc6c9a67012ab26976a9141a7ac36cfa8431ab2395d701b0050045ae4a37d188ac68", "6382012088a91420195b5a3d650c17f0f29f91c33f8f6335193d07876b20e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd1326424242babaffec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc57ccc6c9a67012ab26976a9141a7ac36cfa8431ab2395d701b0050045ae4a37d188ac68", TESTMODE_VALID | TESTMODE_NONMAL | TESTMODE_P2WSH_INVALID); + Test("or_i(and_b(hash160(20195b5a3d650c17f0f29f91c33f8f6335193d07),a:cms(pk_k(02e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13),abab42)),and_v(v:older(42),pkh(025cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc)))", "6382012088a91420195b5a3d650c17f0f29f91c33f8f6335193d07876b20e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd1303abab427ccc6c9a67012ab26976a9141a7ac36cfa8431ab2395d701b0050045ae4a37d188ac68", "6382012088a91420195b5a3d650c17f0f29f91c33f8f6335193d07876b20e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd1303abab427ccc6c9a67012ab26976a9141a7ac36cfa8431ab2395d701b0050045ae4a37d188ac68", TESTMODE_VALID | TESTMODE_NONMAL | TESTMODE_P2WSH_INVALID); + Test("or_i(and_b(hash160(20195b5a3d650c17f0f29f91c33f8f6335193d07),a:cms(pk_i(),00)),and_v(v:older(42),pkh(025cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc)))", "6382012088a91420195b5a3d650c17f0f29f91c33f8f6335193d07876bcb01007ccc6c9a67012ab26976a9141a7ac36cfa8431ab2395d701b0050045ae4a37d188ac68", "6382012088a91420195b5a3d650c17f0f29f91c33f8f6335193d07876bcb01007ccc6c9a67012ab26976a9141a7ac36cfa8431ab2395d701b0050045ae4a37d188ac68", TESTMODE_VALID | TESTMODE_NONMAL | TESTMODE_P2WSH_INVALID); + + // Parsing from Script a cms() inside an and_v() will roundtrip to Script + constexpr std::array cms_andv_roundtrip{{ + "and_v(v:1,andor(cms(pk_k(02f817639b4f4d093dfac1d140c66900b334dac12b5ee3c55f55848e99707e9982),01d0fabd251fcbbe2b93b4b927b26ad2a1a99077152e45ded1e678afa45dbec5),1,1))", + "cms(or_i(and_v(andor(cms(or_i(and_v(and_v(v:older(32),andor(cms(pk_k(02f817639b4f4d093dfac1d140c66900b334dac12b5ee3c55f55848e99707e9982),01d0fabd251fcbbe2b93b4b927b26ad2a1a99077152e45ded1e678afa45dbec5),andor(cms(or_i(and_v(andor(cms(or_i(and_v(andor(cms(or_i(and_v(andor(cms(pk_k(02bf32caadf45c2cdc2bb13dab3feae2dcba15a2f48706317049bfda995aa68053),01d0fabd251fcbbe2b93b4b927b26ad2a1a99077152e45ded1e678afa45dbec5),v:0,v:0),pk_k(02f1167c91ea41c69c07a40cdc0044ec2b0bd1b62407fda4b0ab9b293b447688a4)),pk_k(021fe2829d5372a8dcac7aedbc730cd39bc908ce79347b607eb201b6991f327b31)),01d0fabd251fcbbe2b93b4b927b26ad2a1a99077152e45ded1e678afa45dbec5),v:0,v:0),pk_k(0202fa3aca94d4483d83038cdfbbb74f562776a06850f6171a59fa4d678c6850bf)),pk_k(02d18f9cee54aeff1a0096efa173caed11aa458c28564f125c0c5b623c43594727)),01d0fabd251fcbbe2b93b4b927b26ad2a1a99077152e45ded1e678afa45dbec5),v:0,v:0),pk_k(02c733f2397bf6912a68706b8fb700e8446004f5af59c81493b819857a9560ec42)),pk_k(02c28099397961072fb8e41004922a4a9cdd49c1d40ce403b4095b866c2519a232)),01d0fabd251fcbbe2b93b4b927b26ad2a1a99077152e45ded1e678afa45dbec5),v:0,v:0),v:0)),pk_k(020c91cd4bee354b7bf1332b05a6958c513bdc267b3e79f3f47e1735d132b76de5)),pk_k(020b78192fd2aa32166f9bfd48e46db37c0da8d58b4c0946230028b369e7745076)),01d0fabd251fcbbe2b93b4b927b26ad2a1a99077152e45ded1e678afa45dbec5),v:0,v:0),pk_k(029d8f8bf5a4036afbcec9fd79b1f5be61a9e3519973ad529ccac5d896e3076ce3)),pk_k(02fbe7a86aefec0dc6d70251c8ae5c85be58684b8eded6225e1027ea6069fb24a7)),01d0fabd251fcbbe2b93b4b927b26ad2a1a99077152e45ded1e678afa45dbec5)", + }}; + for (const auto ms_str: cms_andv_roundtrip) { + const auto ms{miniscript::FromString(std::string{ms_str}, tap_converter)}; + Assert(ms); + const auto script{ms->ToScript(tap_converter)}; + const auto decoded{miniscript::FromScript(script, tap_converter)}; + Assert(decoded); + BOOST_CHECK(*ms == *decoded); + } + + // Parsing from Script an and_v() inside a key expression in a cms() does + // not round trip but does yield a top fragment with the same type. + { + std::string_view andv_in_cms{"cms(and_v(v:0,pk_k(03f817639b4f4d093dfac1d140c66900b334dac12b5ee3c55f55848e99707e9982)),01d0fabd251fcbbe2b93b4b927b26ad2a1a99077152e45ded1e678afa45dbec5)"}; + const auto ms{miniscript::FromString(std::string{andv_in_cms}, tap_converter)}; + Assert(ms); + const auto script{ms->ToScript(tap_converter)}; + const auto decoded{miniscript::FromScript(script, tap_converter)}; + Assert(decoded); + BOOST_CHECK(*ms != *decoded); + BOOST_CHECK(decoded->GetType() == ms->GetType()); + } g_testdata.reset(); } diff --git a/test/functional/wallet_miniscript.py b/test/functional/wallet_miniscript.py index c0d5db064ed9..8b088ee4eb62 100755 --- a/test/functional/wallet_miniscript.py +++ b/test/functional/wallet_miniscript.py @@ -61,6 +61,9 @@ f"tr({TPUBS[0]}/*,th(54ab1fa5f9ea585d0f9674163276bbbde113a9f3328034977a3b3170cc3a9234))", ] +# As a separate variable, because as a literal inside an f-string it triggers linter false-positives. +DUMMY_32B_MSG_HEX = "ab" * 32 + DESCS_PRIV = [ # One of two keys, of which one private key is known { @@ -203,6 +206,22 @@ "sigs_count": 2, "stack_size": 8, }, + # A signature for an arbitrary message in a timelocked leaf, the immediately-available alternatives being unavailable. + { + "desc": f"tr({TPUBS[0]}/*,{{and_v(v:pk({TPRVS[1]}/*),and_b(dv:after(42),a:cms(pk_h({TPRVS[2]}/*),{DUMMY_32B_MSG_HEX}))),pk({TPUBS[3]}/*)}})", + "sequence": None, + "locktime": 42, + "sigs_count": 2, + "stack_size": 6, + }, + # Very same descriptor as above, but with a 31-byte arbitrary message. Fails as signing is only implemented for 32-byte messages. + { + "desc": f"tr({TPUBS[0]}/*,{{and_v(v:pk({TPRVS[1]}/*),and_b(dv:after(42),a:cms(pk_h({TPRVS[2]}/*),{DUMMY_32B_MSG_HEX[:62]}))),pk({TPUBS[3]}/*)}})", + "sequence": None, + "locktime": 42, + "sigs_count": 1, + "stack_size": None, + } ] From b5a46a549c6f260c7cfdc15860669bed9c872af1 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Mon, 2 Feb 2026 20:05:19 -0500 Subject: [PATCH 10/16] miniscript: introduce a new 'r:' wrapper fragment This fragment is the equivalent of 'c:' but for rebindable signatures. It is a specialization of the 'cms()' fragment for a specific message that is the TEMPLATEHASH of the spending transaction. --- src/script/miniscript.cpp | 8 +++++- src/script/miniscript.h | 55 +++++++++++++++++++++++++++++++++--- src/script/sign.cpp | 3 ++ src/test/fuzz/miniscript.cpp | 20 +++++++++++-- 4 files changed, 79 insertions(+), 7 deletions(-) diff --git a/src/script/miniscript.cpp b/src/script/miniscript.cpp index 2c389f0c1f14..eb4dbf58e4b5 100644 --- a/src/script/miniscript.cpp +++ b/src/script/miniscript.cpp @@ -64,7 +64,7 @@ Type ComputeType(Fragment fragment, Type x, Type y, Type z, const std::vector OP_SWAP OP_CHECKSIGVERIFY @@ -247,14 +248,15 @@ enum class Availability { struct NoSig {}; struct TxSig {}; +struct TxRebSig {}; struct CustomSig { std::span msg; }; /** The type of message for a signature check. May be NoSig if no parent of a fragment comports a * signature check, TxSig if the parent (or an ancestor) of a fragment is a regular transaction - * signature check (CHECKSIG and friends), and CustomSig if it is a signature check for an arbitrary - * message. */ -using SigMsgType = std::variant; + * signature check (CHECKSIG and friends), TxRebSig if it is a rebindable signature check, and CustomSig + * if it is a signature check for an arbitrary message. */ +using SigMsgType = std::variant; enum class MiniscriptContext { P2WSH, @@ -811,6 +813,10 @@ struct Node { return std::move(subs[0]); } } + case Fragment::WRAP_R: { + CHECK_NONFATAL(is_tapscript); + return BuildScript(std::move(subs[0]), OP_TEMPLATEHASH, OP_SWAP, OP_CHECKSIGFROMSTACK); + } case Fragment::WRAP_J: return BuildScript(OP_SIZE, OP_0NOTEQUAL, OP_IF, subs[0], OP_ENDIF); case Fragment::WRAP_N: return BuildScript(std::move(subs[0]), OP_0NOTEQUAL); case Fragment::CMS: { @@ -864,7 +870,7 @@ struct Node { return (node.fragment == Fragment::WRAP_A || node.fragment == Fragment::WRAP_S || node.fragment == Fragment::WRAP_D || node.fragment == Fragment::WRAP_V || node.fragment == Fragment::WRAP_J || node.fragment == Fragment::WRAP_N || - node.fragment == Fragment::WRAP_C || + node.fragment == Fragment::WRAP_C || node.fragment == Fragment::WRAP_R || (node.fragment == Fragment::AND_V && node.subs[1]->fragment == Fragment::JUST_1) || (node.fragment == Fragment::OR_I && node.subs[0]->fragment == Fragment::JUST_0) || (node.fragment == Fragment::OR_I && node.subs[1]->fragment == Fragment::JUST_0)); @@ -900,6 +906,10 @@ struct Node { case Fragment::WRAP_V: return "v" + std::move(subs[0]); case Fragment::WRAP_J: return "j" + std::move(subs[0]); case Fragment::WRAP_N: return "n" + std::move(subs[0]); + case Fragment::WRAP_R: { + CHECK_NONFATAL(is_tapscript); + return "r" + std::move(subs[0]); + } case Fragment::AND_V: // t:X is syntactic sugar for and_v(X,1). if (node.subs[1]->fragment == Fragment::JUST_1) return "t" + std::move(subs[0]); @@ -1041,6 +1051,7 @@ struct Node { case Fragment::WRAP_N: return {1 + subs[0]->ops.count, subs[0]->ops.sat, subs[0]->ops.dsat}; case Fragment::WRAP_A: return {2 + subs[0]->ops.count, subs[0]->ops.sat, subs[0]->ops.dsat}; case Fragment::WRAP_D: return {3 + subs[0]->ops.count, subs[0]->ops.sat, 0}; + case Fragment::WRAP_R: return {3 + subs[0]->ops.count, subs[0]->ops.sat, subs[0]->ops.dsat}; case Fragment::WRAP_J: return {4 + subs[0]->ops.count, subs[0]->ops.sat, 0}; case Fragment::WRAP_V: return {subs[0]->ops.count + (subs[0]->GetType() << "x"_mst), subs[0]->ops.sat, {}}; case Fragment::CMS: return {2 + subs[0]->ops.count, subs[0]->ops.sat, subs[0]->ops.dsat}; @@ -1145,6 +1156,11 @@ struct Node { SatInfo::OP_DUP() + SatInfo::If() + subs[0]->ss.sat, SatInfo::OP_DUP() + SatInfo::If() }; + case Fragment::WRAP_R: return { + // Get the key on the stack, then push the template hash, then CSFS + subs[0]->ss.sat + SatInfo::Push() + SatInfo::OP_CSFS(), + subs[0]->ss.dsat + SatInfo::Push() + SatInfo::OP_CSFS(), + }; case Fragment::WRAP_V: return {subs[0]->ss.sat + SatInfo::OP_VERIFY(), {}}; case Fragment::WRAP_J: return { SatInfo::OP_SIZE() + SatInfo::OP_0NOTEQUAL() + SatInfo::If() + subs[0]->ss.sat, @@ -1185,6 +1201,8 @@ struct Node { } internal::WitnessSize CalcWitnessSize() const { + // NOTE: this is a 1-byte overestimation for 'cms()' / 'r:' fragments since CSFS signatures + // must always be 64-byte long. const uint32_t sig_size = IsTapscript(m_script_ctx) ? 1 + 65 : 1 + 72; const uint32_t pubkey_size = IsTapscript(m_script_ctx) ? 1 + 32 : 1 + 33; switch (fragment) { @@ -1220,6 +1238,7 @@ struct Node { case Fragment::WRAP_A: case Fragment::WRAP_N: case Fragment::WRAP_S: + case Fragment::WRAP_R: case Fragment::WRAP_C: return subs[0]->ws; case Fragment::WRAP_D: return {1 + 1 + subs[0]->ws.sat, 1}; case Fragment::WRAP_V: return {subs[0]->ws.sat, {}}; @@ -1254,6 +1273,8 @@ struct Node { return TxSig{}; } else if (node.fragment == Fragment::CMS) { return CustomSig{.msg = std::span{node.data}}; + } else if (node.fragment == Fragment::WRAP_R) { + return TxRebSig{}; } return sig_type; }; @@ -1442,6 +1463,7 @@ struct Node { case Fragment::WRAP_S: case Fragment::WRAP_C: case Fragment::WRAP_N: + case Fragment::WRAP_R: return std::move(subres[0]); case Fragment::WRAP_D: { auto &x = subres[0]; @@ -1787,6 +1809,8 @@ enum class ParseContext { ALT, /** CHECK wraps the top constructed node with c: */ CHECK, + /** REBCHECK wraps the top constructed node with r: */ + REBCHECK, /** DUP_IF wraps the top constructed node with d: */ DUP_IF, /** VERIFY wraps the top constructed node with v: */ @@ -1995,6 +2019,10 @@ inline NodeRef Parse(Span in, const Ctx& ctx) script_size += 4; constructed.push_back(MakeNodeRef(internal::NoDupCheck{}, ctx.MsContext(), Fragment::JUST_0)); to_parse.emplace_back(ParseContext::OR_I, -1, -1); + } else if (in[j] == 'r') { + if (!IsTapscript(ctx.MsContext())) return {}; + script_size += 3; + to_parse.emplace_back(ParseContext::REBCHECK, -1, -1); } else { return {}; } @@ -2182,6 +2210,10 @@ inline NodeRef Parse(Span in, const Ctx& ctx) constructed.back() = MakeNodeRef(internal::NoDupCheck{}, ctx.MsContext(), Fragment::WRAP_C, Vector(std::move(constructed.back()))); break; } + case ParseContext::REBCHECK: { + constructed.back() = MakeNodeRef(internal::NoDupCheck{}, ctx.MsContext(), Fragment::WRAP_R, Vector(std::move(constructed.back()))); + break; + } case ParseContext::DUP_IF: { constructed.back() = MakeNodeRef(internal::NoDupCheck{}, ctx.MsContext(), Fragment::WRAP_D, Vector(std::move(constructed.back()))); break; @@ -2322,6 +2354,8 @@ enum class DecodeContext { ALT, /** CHECK wraps the top constructed node with c: */ CHECK, + /** REBCHECK wraps the top constructed node with c: */ + REBCHECK, /** DUP_IF wraps the top constructed node with d: */ DUP_IF, /** VERIFY wraps the top constructed node with v: */ @@ -2550,6 +2584,14 @@ inline NodeRef DecodeScript(I& in, I last, const Ctx& ctx) to_parse.emplace_back(DecodeContext::SINGLE_BKV_EXPR, -1, -1); break; } + // r: wrapper + if (last - in >= 3 && in[0].first == OP_CHECKSIGFROMSTACK && in[1].first == OP_SWAP && in[2].first == OP_TEMPLATEHASH) { + if (!IsTapscript(ctx.MsContext())) return {}; + in += 3; + to_parse.emplace_back(DecodeContext::REBCHECK, -1, -1); + to_parse.emplace_back(DecodeContext::SINGLE_BKV_EXPR, -1, -1); + break; + } // v: wrapper if (in[0].first == OP_VERIFY) { ++in; @@ -2654,6 +2696,11 @@ inline NodeRef DecodeScript(I& in, I last, const Ctx& ctx) constructed.back() = MakeNodeRef(internal::NoDupCheck{}, ctx.MsContext(), Fragment::WRAP_A, Vector(std::move(constructed.back()))); break; } + case DecodeContext::REBCHECK: { + if (constructed.empty()) return {}; + constructed.back() = MakeNodeRef(internal::NoDupCheck{}, ctx.MsContext(), Fragment::WRAP_R, Vector(std::move(constructed.back()))); + break; + } case DecodeContext::CHECK: { if (constructed.empty()) return {}; constructed.back() = MakeNodeRef(internal::NoDupCheck{}, ctx.MsContext(), Fragment::WRAP_C, Vector(std::move(constructed.back()))); diff --git a/src/script/sign.cpp b/src/script/sign.cpp index 027c8532f362..dec9560d23c1 100644 --- a/src/script/sign.cpp +++ b/src/script/sign.cpp @@ -344,6 +344,9 @@ struct TapSatisfier: Satisfier { } custom_msg = uint256{custom_sig->msg}; } + if (std::holds_alternative(sig_type)) { + custom_msg = GetTemplateHash(); + } if (CreateTaprootScriptSig(m_creator, m_sig_data, m_provider, sig, key, m_leaf_hash, SigVersion::TAPSCRIPT, custom_msg)) { return miniscript::Availability::YES; } diff --git a/src/test/fuzz/miniscript.cpp b/src/test/fuzz/miniscript.cpp index c9929785af4e..78b12f1fb65b 100644 --- a/src/test/fuzz/miniscript.cpp +++ b/src/test/fuzz/miniscript.cpp @@ -312,11 +312,21 @@ struct SatisfierContext : ParserContext { } return miniscript::Availability::NO; } - Assert(std::holds_alternative(sig_type)); bool sig_available{false}; if (auto res = TEST_DATA.GetSig(script_ctx, key)) { std::tie(sig, sig_available) = *res; } + if (script_ctx == MsCtx::TAPSCRIPT) { + // Since all dummy sigs sign TestData::MESSAGE_HASH, and it is also used as the template hash, + // then dummy signatures are valid for both regular sig checks and rebindable sig checks with + // the exception that rebindable sigs should not have a sighash type byte. + if (std::holds_alternative(sig_type)) { + sig.pop_back(); + Assert(sig.size() == 64); + } + } else { + Assert(std::holds_alternative(sig_type)); + } return sig_available ? miniscript::Availability::YES : miniscript::Availability::NO; } @@ -665,7 +675,8 @@ struct SmartInfo case Fragment::MULTI_A: case Fragment::PK_I: case Fragment::TH: - case Fragment::CMS: return true; + case Fragment::CMS: + case Fragment::WRAP_R: return true; default: return false; } }}; @@ -724,6 +735,7 @@ struct SmartInfo case Fragment::WRAP_V: case Fragment::WRAP_J: case Fragment::WRAP_N: + case Fragment::WRAP_R: sub_count = 1; break; case Fragment::AND_V: @@ -936,6 +948,7 @@ std::optional ConsumeNodeSmart(MsCtx script_ctx, FuzzedDataProvider& p case Fragment::WRAP_V: case Fragment::WRAP_J: case Fragment::WRAP_N: + case Fragment::WRAP_R: case Fragment::AND_V: case Fragment::AND_B: case Fragment::OR_B: @@ -1073,6 +1086,9 @@ NodeRef GenNode(MsCtx script_ctx, F ConsumeNode, Type root_type, std::optional MAX_OPS_PER_SCRIPT) return {}; auto subtypes = node_info->subtypes; From ac363b6cb3bed6f28c0e7911d5a58e3986e6d8da Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Tue, 3 Feb 2026 16:16:08 -0500 Subject: [PATCH 11/16] qa: sanity check new 'r:' fragment --- src/test/descriptor_tests.cpp | 27 +++++++++++++++++++++++++++ src/test/miniscript_tests.cpp | 14 +++++++++++++- test/functional/wallet_miniscript.py | 9 +++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/test/descriptor_tests.cpp b/src/test/descriptor_tests.cpp index 641bd3478b19..ea3f80169d2b 100644 --- a/src/test/descriptor_tests.cpp +++ b/src/test/descriptor_tests.cpp @@ -1124,6 +1124,33 @@ BOOST_AUTO_TEST_CASE(descriptor_test) {{3, 0}, {3, 1}}, } ); + Check("tr(a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd,r:pk_h(L1NKM8dVA1h52mwDrmk1YreTWkAZZTu2vmKLpmLEbFRqGQYjHeEV))", "tr(a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd,r:pk_h(30a6069f344fb784a2b4c99540a91ee727c91e3a25ef6aae867d9c65b5f23529))", "tr(a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd,r:pk_h(30a6069f344fb784a2b4c99540a91ee727c91e3a25ef6aae867d9c65b5f23529))", MISSING_PRIVKEYS | XONLY_KEYS | SIGNABLE, {{"512051d06208097a41d7325077b8965739e31b4ab150e0568572abe98ea12d4de2de"}}, OutputType::BECH32M); + Check("tr(a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd,{{r:pk_h(KykUPmR5967F4URzMUeCv9kNMU9CNRWycrPmx3ZvfkWoQLabbimL),r:pk_k(L3Enys1jFgTq4E24b8Uom1kAz6cNkz3Z82XZpBKCE2ztErq9fqvJ)},thresh(1,r:pk_k(L1NKM8dVA1h52mwDrmk1YreTWkAZZTu2vmKLpmLEbFRqGQYjHeEV),sr:pk_k(Kz3iCBy3HNGP5CZWDsAMmnCMFNwqdDohudVN9fvkrN7tAkzKNtM7))})", "tr(a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd,{{r:pk_h(1c9bc926084382e76da33b5a52d17b1fa153c072aae5fb5228ecc2ccf89d79d5),r:pk_k(0dd6b52b192ab195558d22dd8437a9ec4519ee5ded496c0d55bc9b1a8b0e8c2b)},thresh(1,r:pk_k(30a6069f344fb784a2b4c99540a91ee727c91e3a25ef6aae867d9c65b5f23529),sr:pk_k(9918d400c1b8c3c478340a40117ced4054b6b58f48cdb3c89b836bdfee1f5766))})", "tr(a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd,{{r:pk_h(1c9bc926084382e76da33b5a52d17b1fa153c072aae5fb5228ecc2ccf89d79d5),r:pk_k(0dd6b52b192ab195558d22dd8437a9ec4519ee5ded496c0d55bc9b1a8b0e8c2b)},thresh(1,r:pk_k(30a6069f344fb784a2b4c99540a91ee727c91e3a25ef6aae867d9c65b5f23529),sr:pk_k(9918d400c1b8c3c478340a40117ced4054b6b58f48cdb3c89b836bdfee1f5766))})", MISSING_PRIVKEYS | XONLY_KEYS, {{"512017e314db6f41afaf3e308aa43648b331d1d6a1d78fb2d2cf8d68d9e37acb833f"}}, OutputType::BECH32M); + CheckMultipath("tr(xprv9yYge4PS54XkYT9KiLfCRwc8Jeuz8DucxQGtuEecJZYhKNiqbPxYHTPzXtskmzWBqdqkRAGsghNmZzNsfU2wstaB3XjDQFPv567aQSSuPyo,lr:pk_k(xprvA1ADjaN8H3HGnZSmt4VF7YdWoV9oNq8jhqhurxsrYycBAFK555cECoaY22KWt6BTRNLuvobW5VQTF89PN3iA485LAg7epazevPyjCa4xTzd/<2;3>))", + "tr(xpub6CY33ZvKuS63kwDnpNCCo5YrrgkUXgdUKdCVhd4Dru5gCB3z8wGnqFiUP98Za5pYSYF5KmvBHTY3Ra8FAJGggzBjuHS69WzN8gscPupuZwK,lr:pk_k(xpub6E9a95u27Qqa13XEz62FUgaFMWzHnHrb54dWfMHU7K9A33eDccvUkbu1sHYoByHAgJdR326rWqn9pGZgZHz1afDprW5gGwS4gUX8Ri6aGPZ/<2;3>))", + { + "tr(xprv9yYge4PS54XkYT9KiLfCRwc8Jeuz8DucxQGtuEecJZYhKNiqbPxYHTPzXtskmzWBqdqkRAGsghNmZzNsfU2wstaB3XjDQFPv567aQSSuPyo,lr:pk_k(xprvA1ADjaN8H3HGnZSmt4VF7YdWoV9oNq8jhqhurxsrYycBAFK555cECoaY22KWt6BTRNLuvobW5VQTF89PN3iA485LAg7epazevPyjCa4xTzd/2))", + "tr(xprv9yYge4PS54XkYT9KiLfCRwc8Jeuz8DucxQGtuEecJZYhKNiqbPxYHTPzXtskmzWBqdqkRAGsghNmZzNsfU2wstaB3XjDQFPv567aQSSuPyo,lr:pk_k(xprvA1ADjaN8H3HGnZSmt4VF7YdWoV9oNq8jhqhurxsrYycBAFK555cECoaY22KWt6BTRNLuvobW5VQTF89PN3iA485LAg7epazevPyjCa4xTzd/3))", + }, + { + "tr(xpub6CY33ZvKuS63kwDnpNCCo5YrrgkUXgdUKdCVhd4Dru5gCB3z8wGnqFiUP98Za5pYSYF5KmvBHTY3Ra8FAJGggzBjuHS69WzN8gscPupuZwK,lr:pk_k(xpub6E9a95u27Qqa13XEz62FUgaFMWzHnHrb54dWfMHU7K9A33eDccvUkbu1sHYoByHAgJdR326rWqn9pGZgZHz1afDprW5gGwS4gUX8Ri6aGPZ/2))", + "tr(xpub6CY33ZvKuS63kwDnpNCCo5YrrgkUXgdUKdCVhd4Dru5gCB3z8wGnqFiUP98Za5pYSYF5KmvBHTY3Ra8FAJGggzBjuHS69WzN8gscPupuZwK,lr:pk_k(xpub6E9a95u27Qqa13XEz62FUgaFMWzHnHrb54dWfMHU7K9A33eDccvUkbu1sHYoByHAgJdR326rWqn9pGZgZHz1afDprW5gGwS4gUX8Ri6aGPZ/3))", + }, + { + "tr(xpub6CY33ZvKuS63kwDnpNCCo5YrrgkUXgdUKdCVhd4Dru5gCB3z8wGnqFiUP98Za5pYSYF5KmvBHTY3Ra8FAJGggzBjuHS69WzN8gscPupuZwK,lr:pk_k(xpub6E9a95u27Qqa13XEz62FUgaFMWzHnHrb54dWfMHU7K9A33eDccvUkbu1sHYoByHAgJdR326rWqn9pGZgZHz1afDprW5gGwS4gUX8Ri6aGPZ/2))", + "tr(xpub6CY33ZvKuS63kwDnpNCCo5YrrgkUXgdUKdCVhd4Dru5gCB3z8wGnqFiUP98Za5pYSYF5KmvBHTY3Ra8FAJGggzBjuHS69WzN8gscPupuZwK,lr:pk_k(xpub6E9a95u27Qqa13XEz62FUgaFMWzHnHrb54dWfMHU7K9A33eDccvUkbu1sHYoByHAgJdR326rWqn9pGZgZHz1afDprW5gGwS4gUX8Ri6aGPZ/3))", + }, + XONLY_KEYS, + { + {{"51203fde91b760f160e557bd8f45c3feb1cec8262964935fa5ae2b1b036a492cdef5"}}, + {{"51207d2514851c111ecae82a126479cdd9594db40526caebda777b3f67d7f79608ed"}}, + }, + OutputType::BECH32M, + { + {{2}, {}}, + {{3}, {}}, + } + ); } BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/miniscript_tests.cpp b/src/test/miniscript_tests.cpp index a685d351fb3f..60cd148bb37f 100644 --- a/src/test/miniscript_tests.cpp +++ b/src/test/miniscript_tests.cpp @@ -258,8 +258,8 @@ struct Satisfier : public KeyConverter { return miniscript::Availability::YES; } - Assert(std::holds_alternative(sig_type)); if (!miniscript::IsTapscript(m_script_ctx)) { + Assert(std::holds_alternative(sig_type)); auto it = g_testdata->signatures.find(key); if (it == g_testdata->signatures.end()) return miniscript::Availability::NO; sig = it->second; @@ -267,6 +267,13 @@ struct Satisfier : public KeyConverter { auto it = g_testdata->schnorr_signatures.find(XOnlyPubKey{key}); if (it == g_testdata->schnorr_signatures.end()) return miniscript::Availability::NO; sig = it->second; + // Since all dummy sigs sign TestData::MESSAGE_HASH, and it is also used as the template hash, + // then dummy signatures are valid for both regular sig checks and rebindable sig checks with + // the exception that rebindable sigs should not have a sighash type byte. + if (std::holds_alternative(sig_type)) { + sig.pop_back(); + Assert(sig.size() == 64); + } } return miniscript::Availability::YES; } @@ -797,6 +804,11 @@ BOOST_AUTO_TEST_CASE(fixed_tests) Test("or_i(and_b(hash160(20195b5a3d650c17f0f29f91c33f8f6335193d07),a:cms(pk_k(02e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13),424242babaffec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc5)),and_v(v:older(42),pkh(025cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc)))", "6382012088a91420195b5a3d650c17f0f29f91c33f8f6335193d07876b20e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd1326424242babaffec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc57ccc6c9a67012ab26976a9141a7ac36cfa8431ab2395d701b0050045ae4a37d188ac68", "6382012088a91420195b5a3d650c17f0f29f91c33f8f6335193d07876b20e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd1326424242babaffec4916dd28fc4c10d78e287ca5d9cc51ee1ae73cbfde08c6b37324cbfaac8bc57ccc6c9a67012ab26976a9141a7ac36cfa8431ab2395d701b0050045ae4a37d188ac68", TESTMODE_VALID | TESTMODE_NONMAL | TESTMODE_P2WSH_INVALID); Test("or_i(and_b(hash160(20195b5a3d650c17f0f29f91c33f8f6335193d07),a:cms(pk_k(02e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13),abab42)),and_v(v:older(42),pkh(025cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc)))", "6382012088a91420195b5a3d650c17f0f29f91c33f8f6335193d07876b20e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd1303abab427ccc6c9a67012ab26976a9141a7ac36cfa8431ab2395d701b0050045ae4a37d188ac68", "6382012088a91420195b5a3d650c17f0f29f91c33f8f6335193d07876b20e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd1303abab427ccc6c9a67012ab26976a9141a7ac36cfa8431ab2395d701b0050045ae4a37d188ac68", TESTMODE_VALID | TESTMODE_NONMAL | TESTMODE_P2WSH_INVALID); Test("or_i(and_b(hash160(20195b5a3d650c17f0f29f91c33f8f6335193d07),a:cms(pk_i(),00)),and_v(v:older(42),pkh(025cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc)))", "6382012088a91420195b5a3d650c17f0f29f91c33f8f6335193d07876bcb01007ccc6c9a67012ab26976a9141a7ac36cfa8431ab2395d701b0050045ae4a37d188ac68", "6382012088a91420195b5a3d650c17f0f29f91c33f8f6335193d07876bcb01007ccc6c9a67012ab26976a9141a7ac36cfa8431ab2395d701b0050045ae4a37d188ac68", TESTMODE_VALID | TESTMODE_NONMAL | TESTMODE_P2WSH_INVALID); + Test("or_i(r:and_v(v:after(500000),pk_k(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)),sha256(d9147961436944f43cd99d28b2bbddbf452ef872b30c8279e255e7daafc7f946))", "", "630320a107b16920c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5ce7ccc6782012088a820d9147961436944f43cd99d28b2bbddbf452ef872b30c8279e255e7daafc7f9468768", TESTMODE_VALID | TESTMODE_NONMAL | TESTMODE_P2WSH_INVALID, 12, 2, -1, 2 + 66, 3); + Test("and_n(sha256(9267d3dbed802941483f1afa2a6bc68de5f653128aca9bf1461c5d0a3ad36ed2),ur:and_v(v:older(144),pk_k(03fe72c435413d33d48ac09c9161ba8b09683215439d62b7940502bda8b202e6ce)))", "", "82012088a8209267d3dbed802941483f1afa2a6bc68de5f653128aca9bf1461c5d0a3ad36ed28764006763029000b26920fe72c435413d33d48ac09c9161ba8b09683215439d62b7940502bda8b202e6cece7ccc67006868", TESTMODE_VALID | TESTMODE_NEEDSIG | TESTMODE_P2WSH_INVALID, 15, 3, -1, 33 + 2 + 66, 5); + Test("r:or_i(and_v(v:older(16),pk_h(02d7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080e)),pk_h(026a245bf6dc698504c89a20cfded60853152b695336c28063b61c65cbd269e6b4))", "", "6360b26976a9144d4421361c3289bdad06441ffaee8be8e786f1ad886776a91460d4a7bcbd08f58e58bd208d1069837d7adb16ae8868ce7ccc", TESTMODE_VALID | TESTMODE_NONMAL | TESTMODE_NEEDSIG | TESTMODE_P2WSH_INVALID, 14, 3, -1, 2 + 33 + 66, 4); + Test("or_d(r:pk_h(02e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13),andor(r:pk_k(024ce119c96e2fa357200b559b2f7dd5a5f02d5290aff74b03f3e471b273211c97),older(2016),after(1567547623)))", "", "76a91421ab1a140d0d305b8ff62bdb887d9fef82c9899e88ce7ccc7364204ce119c96e2fa357200b559b2f7dd5a5f02d5290aff74b03f3e471b273211c97ce7ccc6404e7e06e5db16702e007b26868", TESTMODE_VALID | TESTMODE_NONMAL | TESTMODE_P2WSH_INVALID, 17, 3, -1, 1 + 33 + 66, 5); + Test("thresh(1,r:pk_k(03d30199d74fb5a22d47b6e054e2f378cedacffcb89904a61d75d0dbd407143e65),altv:after(1000000000),altv:after(100))", "", "20d30199d74fb5a22d47b6e054e2f378cedacffcb89904a61d75d0dbd407143e65ce7ccc6b6300670400ca9a3bb16951686c936b6300670164b16951686c935187", TESTMODE_VALID | TESTMODE_P2WSH_INVALID, 20, 3, -1, 66 + 2 + 2, 5); // Parsing from Script a cms() inside an and_v() will roundtrip to Script constexpr std::array cms_andv_roundtrip{{ diff --git a/test/functional/wallet_miniscript.py b/test/functional/wallet_miniscript.py index 8b088ee4eb62..1c1dd2459f8f 100755 --- a/test/functional/wallet_miniscript.py +++ b/test/functional/wallet_miniscript.py @@ -221,6 +221,15 @@ "locktime": 42, "sigs_count": 1, "stack_size": None, + }, + # LN-Symmetry update transaction output script with a regular pk_k() as the rebindable signature key instead of OP_IK because + # using the latter would lead to always finalizing through the key spend path, and with settlement tx template hash 424242.. + { + "desc": f"tr({TPUBS[0]}/*,{{and_v(vr:pk_k({TPRVS[1]}),after(21)),th(4242424242424242424242424242424242424242424242424242424242424242)}})", + "sequence": None, + "locktime": 21, + "sigs_count": 1, + "stack_size": 3, } ] From 8254a3685a8da584515898055bf50fd26da7142f Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Fri, 13 Feb 2026 16:24:24 -0500 Subject: [PATCH 12/16] psbt: introduce new PSBT_OUT_COMMITTED_TXS field This field allows a verifier to validate the transaction template(s) committed to in a transaction output. One caveat is that transaction are Bitcoin-serialized, which includes some field not committed to in a template hash. --- src/psbt.cpp | 1 + src/psbt.h | 20 ++++++++++++++++++++ src/rpc/rawtransaction.cpp | 17 +++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/src/psbt.cpp b/src/psbt.cpp index 19d855e4c78b..ada8afc4ab93 100644 --- a/src/psbt.cpp +++ b/src/psbt.cpp @@ -283,6 +283,7 @@ void PSBTOutput::Merge(const PSBTOutput& output) hd_keypaths.insert(output.hd_keypaths.begin(), output.hd_keypaths.end()); unknown.insert(output.unknown.begin(), output.unknown.end()); m_tap_bip32_paths.insert(output.m_tap_bip32_paths.begin(), output.m_tap_bip32_paths.end()); + m_committed_txs.insert(output.m_committed_txs.begin(), output.m_committed_txs.end()); if (redeem_script.empty() && !output.redeem_script.empty()) redeem_script = output.redeem_script; if (witness_script.empty() && !output.witness_script.empty()) witness_script = output.witness_script; diff --git a/src/psbt.h b/src/psbt.h index 6d49864b3cdb..46e7cd30eb1f 100644 --- a/src/psbt.h +++ b/src/psbt.h @@ -59,6 +59,7 @@ static constexpr uint8_t PSBT_OUT_BIP32_DERIVATION = 0x02; static constexpr uint8_t PSBT_OUT_TAP_INTERNAL_KEY = 0x05; static constexpr uint8_t PSBT_OUT_TAP_TREE = 0x06; static constexpr uint8_t PSBT_OUT_TAP_BIP32_DERIVATION = 0x07; +static constexpr uint8_t PSBT_OUT_COMMITTED_TXS = 0x0b; static constexpr uint8_t PSBT_OUT_PROPRIETARY = 0xFC; // The separator is 0x00. Reading this in means that the unserializer can interpret it @@ -719,6 +720,7 @@ struct PSBTOutput XOnlyPubKey m_tap_internal_key; std::vector>> m_tap_tree; std::map, KeyOriginInfo>> m_tap_bip32_paths; + std::map m_committed_txs; std::map, std::vector> unknown; std::set m_proprietary; @@ -781,6 +783,12 @@ struct PSBTOutput s << value; } + // Write the OP_TEMPLATEHASH-committed transactions + for (const auto& [hash, tx]: m_committed_txs) { + SerializeToVector(s, PSBT_OUT_COMMITTED_TXS, hash); + s << TX_WITH_WITNESS(tx); + } + // Write unknown things for (auto& entry : unknown) { s << entry.first; @@ -907,6 +915,18 @@ struct PSBTOutput m_tap_bip32_paths.emplace(xonly, std::make_pair(leaf_hashes, DeserializeKeyOrigin(s, origin_len))); break; } + case PSBT_OUT_COMMITTED_TXS: + { + if (!key_lookup.emplace(key).second) { + throw std::ios_base::failure("Duplicate Key, output committed transaction already provided"); + } else if (key.size() != 33) { + throw std::ios_base::failure("Output committed transaction key is not 33 bytes"); + } + uint256 hash{std::span{key.begin() + 1, key.end()}}; + CMutableTransaction tx{deserialize, TX_WITH_WITNESS, s}; + m_committed_txs.emplace(std::move(hash), std::move(tx)); + break; + } case PSBT_OUT_PROPRIETARY: { PSBTProprietary this_prop; diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index d6dd4f78a6aa..5d3c052afa97 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -987,6 +987,13 @@ const RPCResult decodepsbt_outputs{ }}, }}, }}, + {RPCResult::Type::OBJ_DYN, "committed_transactions", /*optional=*/true, "Map from template hash to corresponding transaction details", + { + {RPCResult::Type::OBJ, "xxxx", "Committed transaction details. Not all listed fields are committed to in the template hash, and may be different in a spending transaction.", + { + {RPCResult::Type::ELISION, "", "The layout is the same as the output of decoderawtransaction."}, + }}, + }}, {RPCResult::Type::OBJ_DYN, "unknown", /*optional=*/true, "The unknown output fields", { {RPCResult::Type::STR_HEX, "key", "(key-value pair) An unknown key-value pair"}, @@ -1403,6 +1410,16 @@ static RPCHelpMan decodepsbt() out.pushKV("taproot_bip32_derivs", std::move(keypaths)); } + if (!output.m_committed_txs.empty()) { + UniValue tx_map{UniValue::VOBJ}; + for (const auto& [template_hash, tx]: output.m_committed_txs) { + UniValue tx_details{UniValue::VOBJ}; + TxToUniv(CTransaction{tx}, /*block_hash=*/uint256{}, /*entry=*/tx_details, /*include_hex=*/false); + tx_map.pushKV(HexStr(template_hash), std::move(tx_details)); + } + out.pushKV("committed_transactions", tx_map); + } + // Proprietary if (!output.m_proprietary.empty()) { UniValue proprietary(UniValue::VARR); From f2bdd1cd137735a7a3db02e54afde86df43f1e15 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Mon, 16 Feb 2026 16:40:54 -0500 Subject: [PATCH 13/16] psbt: introduce field for additional Taproot internal keys The existing PSBT output field for a Taproot internal key is not keyed, which makes it so only a single Taproot internal key may be specified. This makes sense since there may be at most a single one per Taproot output, but since we introduce the capability of committing to the template of a spending transaction, it is often useful for a verifier to validate the outputs of the spending transaction. --- src/psbt.cpp | 1 + src/psbt.h | 21 +++++++++++++++++++++ src/rpc/rawtransaction.cpp | 12 ++++++++++++ 3 files changed, 34 insertions(+) diff --git a/src/psbt.cpp b/src/psbt.cpp index ada8afc4ab93..1fae865d6cc9 100644 --- a/src/psbt.cpp +++ b/src/psbt.cpp @@ -284,6 +284,7 @@ void PSBTOutput::Merge(const PSBTOutput& output) unknown.insert(output.unknown.begin(), output.unknown.end()); m_tap_bip32_paths.insert(output.m_tap_bip32_paths.begin(), output.m_tap_bip32_paths.end()); m_committed_txs.insert(output.m_committed_txs.begin(), output.m_committed_txs.end()); + m_tap_internal_keys.insert(output.m_tap_internal_keys.begin(), output.m_tap_internal_keys.end()); if (redeem_script.empty() && !output.redeem_script.empty()) redeem_script = output.redeem_script; if (witness_script.empty() && !output.witness_script.empty()) witness_script = output.witness_script; diff --git a/src/psbt.h b/src/psbt.h index 46e7cd30eb1f..e7f4ee11a34a 100644 --- a/src/psbt.h +++ b/src/psbt.h @@ -60,6 +60,7 @@ static constexpr uint8_t PSBT_OUT_TAP_INTERNAL_KEY = 0x05; static constexpr uint8_t PSBT_OUT_TAP_TREE = 0x06; static constexpr uint8_t PSBT_OUT_TAP_BIP32_DERIVATION = 0x07; static constexpr uint8_t PSBT_OUT_COMMITTED_TXS = 0x0b; +static constexpr uint8_t PSBT_OUT_TAP_INTERNAL_KEYS = 0x0c; static constexpr uint8_t PSBT_OUT_PROPRIETARY = 0xFC; // The separator is 0x00. Reading this in means that the unserializer can interpret it @@ -721,6 +722,8 @@ struct PSBTOutput std::vector>> m_tap_tree; std::map, KeyOriginInfo>> m_tap_bip32_paths; std::map m_committed_txs; + //! Map from output key to internal key for output of transactions committed in this output. + std::map m_tap_internal_keys; std::map, std::vector> unknown; std::set m_proprietary; @@ -789,6 +792,12 @@ struct PSBTOutput s << TX_WITH_WITNESS(tx); } + // Write the additional Taproot internal keys + for (const auto& [output_key, internal_key]: m_tap_internal_keys) { + SerializeToVector(s, PSBT_OUT_TAP_INTERNAL_KEYS, output_key); + s << internal_key; + } + // Write unknown things for (auto& entry : unknown) { s << entry.first; @@ -927,6 +936,18 @@ struct PSBTOutput m_committed_txs.emplace(std::move(hash), std::move(tx)); break; } + case PSBT_OUT_TAP_INTERNAL_KEYS: + { + if (!key_lookup.emplace(key).second) { + throw std::ios_base::failure("Duplicate Key, additional output Taproot internal key already provided"); + } else if (key.size() != 33) { + throw std::ios_base::failure("Additional output Taproot internal key key is not 33 bytes"); + } + XOnlyPubKey output_key{std::span(key).last(32)}, internal_key; + s >> internal_key; + m_tap_internal_keys.emplace(std::move(output_key), std::move(internal_key)); + break; + } case PSBT_OUT_PROPRIETARY: { PSBTProprietary this_prop; diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 5d3c052afa97..cfbdb0e8c692 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -994,6 +994,10 @@ const RPCResult decodepsbt_outputs{ {RPCResult::Type::ELISION, "", "The layout is the same as the output of decoderawtransaction."}, }}, }}, + {RPCResult::Type::OBJ_DYN, "internal_keys", /*optional=*/true, "Map from output key to internal key for Taproot outputs of committed transactions", + { + {RPCResult::Type::STR_HEX, "xxxx", "Taproot internal key, keyed by Taproot output key"}, + }}, {RPCResult::Type::OBJ_DYN, "unknown", /*optional=*/true, "The unknown output fields", { {RPCResult::Type::STR_HEX, "key", "(key-value pair) An unknown key-value pair"}, @@ -1420,6 +1424,14 @@ static RPCHelpMan decodepsbt() out.pushKV("committed_transactions", tx_map); } + if (!output.m_tap_internal_keys.empty()) { + UniValue keys_map{UniValue::VOBJ}; + for (const auto& [output_key, internal_key]: output.m_tap_internal_keys) { + keys_map.pushKV(HexStr(output_key), HexStr(internal_key)); + } + out.pushKV("internal_keys", std::move(keys_map)); + } + // Proprietary if (!output.m_proprietary.empty()) { UniValue proprietary(UniValue::VARR); From 7c39b7573d9c6788290053842ba68b5b5c405399 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Mon, 16 Feb 2026 16:49:24 -0500 Subject: [PATCH 14/16] psbt: move Tap tree (un)serialization into helpers We are going to reuse them in the following commit. --- src/psbt.h | 80 ++++++++++++++++++++++---------------- src/rpc/rawtransaction.cpp | 4 +- 2 files changed, 48 insertions(+), 36 deletions(-) diff --git a/src/psbt.h b/src/psbt.h index e7f4ee11a34a..fbc4681ef1f9 100644 --- a/src/psbt.h +++ b/src/psbt.h @@ -712,6 +712,50 @@ struct PSBTInput } }; +template +void SerTapTree(Stream& s, const std::vector>>& tap_tree) +{ + std::vector value; + VectorWriter s_value{value, 0}; + for (const auto& [depth, leaf_ver, script] : tap_tree) { + s_value << depth; + s_value << leaf_ver; + s_value << script; + } + s << value; +} + +template +void UnserTapTree(Stream& s, std::vector>>& tap_tree) +{ + std::vector tree_v; + s >> tree_v; + SpanReader s_tree{tree_v}; + if (s_tree.empty()) { + throw std::ios_base::failure("Output Taproot tree must not be empty"); + } + TaprootBuilder builder; + while (!s_tree.empty()) { + uint8_t depth; + uint8_t leaf_ver; + std::vector script; + s_tree >> depth; + s_tree >> leaf_ver; + s_tree >> script; + if (depth > TAPROOT_CONTROL_MAX_NODE_COUNT) { + throw std::ios_base::failure("Output Taproot tree has as leaf greater than Taproot maximum depth"); + } + if ((leaf_ver & ~TAPROOT_LEAF_MASK) != 0) { + throw std::ios_base::failure("Output Taproot tree has a leaf with an invalid leaf version"); + } + tap_tree.emplace_back(depth, leaf_ver, script); + builder.Add((int)depth, script, (int)leaf_ver, /*track=*/true); + } + if (!builder.IsComplete()) { + throw std::ios_base::failure("Output Taproot tree is malformed"); + } +} + /** A structure for PSBTs which contains per output information */ struct PSBTOutput { @@ -765,14 +809,7 @@ struct PSBTOutput // Write taproot tree if (!m_tap_tree.empty()) { SerializeToVector(s, PSBT_OUT_TAP_TREE); - std::vector value; - VectorWriter s_value{value, 0}; - for (const auto& [depth, leaf_ver, script] : m_tap_tree) { - s_value << depth; - s_value << leaf_ver; - s_value << script; - } - s << value; + SerTapTree(s, m_tap_tree); } // Write taproot bip32 keypaths @@ -875,32 +912,7 @@ struct PSBTOutput } else if (key.size() != 1) { throw std::ios_base::failure("Output Taproot tree key is more than one byte type"); } - std::vector tree_v; - s >> tree_v; - SpanReader s_tree{tree_v}; - if (s_tree.empty()) { - throw std::ios_base::failure("Output Taproot tree must not be empty"); - } - TaprootBuilder builder; - while (!s_tree.empty()) { - uint8_t depth; - uint8_t leaf_ver; - std::vector script; - s_tree >> depth; - s_tree >> leaf_ver; - s_tree >> script; - if (depth > TAPROOT_CONTROL_MAX_NODE_COUNT) { - throw std::ios_base::failure("Output Taproot tree has as leaf greater than Taproot maximum depth"); - } - if ((leaf_ver & ~TAPROOT_LEAF_MASK) != 0) { - throw std::ios_base::failure("Output Taproot tree has a leaf with an invalid leaf version"); - } - m_tap_tree.emplace_back(depth, leaf_ver, script); - builder.Add((int)depth, script, (int)leaf_ver, /*track=*/true); - } - if (!builder.IsComplete()) { - throw std::ios_base::failure("Output Taproot tree is malformed"); - } + UnserTapTree(s, m_tap_tree); break; } case PSBT_OUT_TAP_BIP32_DERIVATION: diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index cfbdb0e8c692..575f30e0eccc 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -994,7 +994,7 @@ const RPCResult decodepsbt_outputs{ {RPCResult::Type::ELISION, "", "The layout is the same as the output of decoderawtransaction."}, }}, }}, - {RPCResult::Type::OBJ_DYN, "internal_keys", /*optional=*/true, "Map from output key to internal key for Taproot outputs of committed transactions", + {RPCResult::Type::OBJ_DYN, "taproot_internal_keys", /*optional=*/true, "Map from output key to internal key for Taproot outputs of committed transactions", { {RPCResult::Type::STR_HEX, "xxxx", "Taproot internal key, keyed by Taproot output key"}, }}, @@ -1429,7 +1429,7 @@ static RPCHelpMan decodepsbt() for (const auto& [output_key, internal_key]: output.m_tap_internal_keys) { keys_map.pushKV(HexStr(output_key), HexStr(internal_key)); } - out.pushKV("internal_keys", std::move(keys_map)); + out.pushKV("taproot_internal_keys", std::move(keys_map)); } // Proprietary From 8e6cf4037a69fba9c83d0f60e8ec98f6990bd879 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Mon, 16 Feb 2026 17:26:39 -0500 Subject: [PATCH 15/16] psbt: introduce PSBT output field for additional Tap trees The rationale here is the same as for the additional Taproot internal keys, be able to inspect the outputs of transaction templates committed to in this output. --- src/psbt.cpp | 1 + src/psbt.h | 28 +++++++++++++++++++++++++--- src/rpc/rawtransaction.cpp | 28 ++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/psbt.cpp b/src/psbt.cpp index 1fae865d6cc9..f378d4a85823 100644 --- a/src/psbt.cpp +++ b/src/psbt.cpp @@ -285,6 +285,7 @@ void PSBTOutput::Merge(const PSBTOutput& output) m_tap_bip32_paths.insert(output.m_tap_bip32_paths.begin(), output.m_tap_bip32_paths.end()); m_committed_txs.insert(output.m_committed_txs.begin(), output.m_committed_txs.end()); m_tap_internal_keys.insert(output.m_tap_internal_keys.begin(), output.m_tap_internal_keys.end()); + m_tap_trees.insert(output.m_tap_trees.begin(), output.m_tap_trees.end()); if (redeem_script.empty() && !output.redeem_script.empty()) redeem_script = output.redeem_script; if (witness_script.empty() && !output.witness_script.empty()) witness_script = output.witness_script; diff --git a/src/psbt.h b/src/psbt.h index fbc4681ef1f9..b84eaaf62aad 100644 --- a/src/psbt.h +++ b/src/psbt.h @@ -61,6 +61,7 @@ static constexpr uint8_t PSBT_OUT_TAP_TREE = 0x06; static constexpr uint8_t PSBT_OUT_TAP_BIP32_DERIVATION = 0x07; static constexpr uint8_t PSBT_OUT_COMMITTED_TXS = 0x0b; static constexpr uint8_t PSBT_OUT_TAP_INTERNAL_KEYS = 0x0c; +static constexpr uint8_t PSBT_OUT_TAP_TREES = 0x0d; static constexpr uint8_t PSBT_OUT_PROPRIETARY = 0xFC; // The separator is 0x00. Reading this in means that the unserializer can interpret it @@ -712,8 +713,10 @@ struct PSBTInput } }; +using TapTreeList = std::vector>>; + template -void SerTapTree(Stream& s, const std::vector>>& tap_tree) +void SerTapTree(Stream& s, const TapTreeList& tap_tree) { std::vector value; VectorWriter s_value{value, 0}; @@ -726,7 +729,7 @@ void SerTapTree(Stream& s, const std::vector -void UnserTapTree(Stream& s, std::vector>>& tap_tree) +void UnserTapTree(Stream& s, TapTreeList& tap_tree) { std::vector tree_v; s >> tree_v; @@ -763,11 +766,13 @@ struct PSBTOutput CScript witness_script; std::map hd_keypaths; XOnlyPubKey m_tap_internal_key; - std::vector>> m_tap_tree; + TapTreeList m_tap_tree; std::map, KeyOriginInfo>> m_tap_bip32_paths; std::map m_committed_txs; //! Map from output key to internal key for output of transactions committed in this output. std::map m_tap_internal_keys; + //! Map from output key to Tap tree for output of transactions committed in this output. + std::map m_tap_trees; std::map, std::vector> unknown; std::set m_proprietary; @@ -835,6 +840,12 @@ struct PSBTOutput s << internal_key; } + // Write the additional Taproot trees + for (const auto& [output_key, tree]: m_tap_trees) { + SerializeToVector(s, PSBT_OUT_TAP_TREES, output_key); + SerTapTree(s, tree); + } + // Write unknown things for (auto& entry : unknown) { s << entry.first; @@ -960,6 +971,17 @@ struct PSBTOutput m_tap_internal_keys.emplace(std::move(output_key), std::move(internal_key)); break; } + case PSBT_OUT_TAP_TREES: + { + if (!key_lookup.emplace(key).second) { + throw std::ios_base::failure("Duplicate Key, additional output Taproot internal key already provided"); + } else if (key.size() != 33) { + throw std::ios_base::failure("Additional output Taproot internal key key is not 33 bytes"); + } + XOnlyPubKey output_key{std::span(key).last(32)}; + UnserTapTree(s, m_tap_trees[output_key]); + break; + } case PSBT_OUT_PROPRIETARY: { PSBTProprietary this_prop; diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 575f30e0eccc..26ae5af784b9 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -998,6 +998,18 @@ const RPCResult decodepsbt_outputs{ { {RPCResult::Type::STR_HEX, "xxxx", "Taproot internal key, keyed by Taproot output key"}, }}, + {RPCResult::Type::OBJ_DYN, "taproot_trees", /*optional=*/true, "Map from output key to Taproot tree for outputs of committed transactions", + { + {RPCResult::Type::ARR, "xxxx", "List of tuples that make up the Taproot tree, in depth first search order, keyed by Taproot output key", + { + {RPCResult::Type::OBJ, "tuple", /*optional=*/ true, "A single leaf script in the taproot tree", + { + {RPCResult::Type::NUM, "depth", "The depth of this element in the tree"}, + {RPCResult::Type::NUM, "leaf_ver", "The version of this leaf"}, + {RPCResult::Type::STR, "script", "The hex-encoded script itself"}, + }}, + }}, + }}, {RPCResult::Type::OBJ_DYN, "unknown", /*optional=*/true, "The unknown output fields", { {RPCResult::Type::STR_HEX, "key", "(key-value pair) An unknown key-value pair"}, @@ -1432,6 +1444,22 @@ static RPCHelpMan decodepsbt() out.pushKV("taproot_internal_keys", std::move(keys_map)); } + if (!output.m_tap_trees.empty()) { + UniValue trees_map{UniValue::VOBJ}; + for (const auto& [output_key, tap_tree]: output.m_tap_trees) { + UniValue tree(UniValue::VARR); + for (const auto& [depth, leaf_ver, script] : tap_tree) { + UniValue elem(UniValue::VOBJ); + elem.pushKV("depth", depth); + elem.pushKV("leaf_ver", leaf_ver); + elem.pushKV("script", HexStr(script)); + tree.push_back(std::move(elem)); + } + trees_map.pushKV(HexStr(output_key), std::move(tree)); + } + out.pushKV("taproot_trees", std::move(trees_map)); + } + // Proprietary if (!output.m_proprietary.empty()) { UniValue proprietary(UniValue::VARR); From 4430066984d0af7f283598a6a4afdf537aaecd16 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Tue, 17 Feb 2026 16:30:38 -0500 Subject: [PATCH 16/16] qa: sanity check new TEMPLATEHASH related fields This is a specially crafted PSBT of a transaction that pays to a Taproot with a leaf with a TEMPLATEHASH equality check for a transaction that pays to 2 Taproot outputs. This highlights the use of all introduced fields, as well as existing ones (BIP32 derivations). --- test/functional/data/rpc_psbt.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/data/rpc_psbt.json b/test/functional/data/rpc_psbt.json index 1ccc5e0ba0c1..6318c193f33d 100644 --- a/test/functional/data/rpc_psbt.json +++ b/test/functional/data/rpc_psbt.json @@ -72,7 +72,8 @@ "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" ], "creator" : [ {