diff --git a/src/psbt.cpp b/src/psbt.cpp index 19d855e4c78b..f378d4a85823 100644 --- a/src/psbt.cpp +++ b/src/psbt.cpp @@ -283,6 +283,9 @@ 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()); + 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 6d49864b3cdb..b84eaaf62aad 100644 --- a/src/psbt.h +++ b/src/psbt.h @@ -59,6 +59,9 @@ 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_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 @@ -710,6 +713,52 @@ struct PSBTInput } }; +using TapTreeList = std::vector>>; + +template +void SerTapTree(Stream& s, const TapTreeList& 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, TapTreeList& 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 { @@ -717,8 +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; @@ -760,14 +814,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 @@ -781,6 +828,24 @@ 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 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 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; @@ -858,32 +923,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: @@ -907,6 +947,41 @@ 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_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_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 d6dd4f78a6aa..26ae5af784b9 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -987,6 +987,29 @@ 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, "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"}, + }}, + {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"}, @@ -1403,6 +1426,40 @@ 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); + } + + 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("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); diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp index c8802d2bf893..495f1ef94553 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); @@ -2113,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) { @@ -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 4b8d3673f95f..eb4dbf58e4b5 100644 --- a/src/script/miniscript.cpp +++ b/src/script/miniscript.cpp @@ -39,11 +39,11 @@ 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); - } else { + } else if (fragment != Fragment::CMS) { assert(data_size == 0); } // Sanity check on k @@ -64,13 +64,13 @@ 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 1 and @@ -256,6 +270,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 } } @@ -263,18 +278,20 @@ 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; case Fragment::PK_K: return IsTapscript(ms_ctx) ? 33 : 34; case Fragment::PK_H: return 3 + 21; + case Fragment::PK_I: return 1; case Fragment::OLDER: case Fragment::AFTER: return 1 + BuildScript(k).size(); case Fragment::HASH256: case Fragment::SHA256: return 4 + 2 + 33; case Fragment::HASH160: case Fragment::RIPEMD160: return 4 + 2 + 21; + case Fragment::TH: return 33 + 2; case Fragment::MULTI: return 1 + BuildScript(n_keys).size() + BuildScript(k).size() + 34 * n_keys; case Fragment::MULTI_A: return (1 + 32 + 1) * n_keys + BuildScript(k).size() + 1; case Fragment::AND_V: return subsize; @@ -284,6 +301,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: @@ -291,6 +309,7 @@ size_t ComputeScriptLen(Fragment fragment, Type sub0typ, size_t subsize, uint32_ case Fragment::OR_I: case Fragment::ANDOR: return subsize + 3; case Fragment::WRAP_J: return subsize + 4; + case Fragment::WRAP_R: return subsize + 1 + 1 + 1; case Fragment::THRESH: return subsize + n_subs + BuildScript(k).size(); } assert(false); @@ -429,5 +448,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 03f0a7c36542..765df39ccee8 100644 --- a/src/script/miniscript.h +++ b/src/script/miniscript.h @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -101,6 +102,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 +184,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) ); } @@ -201,19 +207,23 @@ enum class Fragment { JUST_1, //!< OP_1 PK_K, //!< [key] PK_H, //!< OP_DUP OP_HASH160 [keyhash] OP_EQUALVERIFY + PK_I, //!< OP_INTERNALKEY OLDER, //!< [n] OP_CHECKSEQUENCEVERIFY AFTER, //!< [n] OP_CHECKLOCKTIMEVERIFY SHA256, //!< OP_SIZE 32 OP_EQUALVERIFY OP_SHA256 [hash] OP_EQUAL HASH256, //!< OP_SIZE 32 OP_EQUALVERIFY OP_HASH256 [hash] OP_EQUAL RIPEMD160, //!< OP_SIZE 32 OP_EQUALVERIFY OP_RIPEMD160 [hash] OP_EQUAL HASH160, //!< OP_SIZE 32 OP_EQUALVERIFY OP_HASH160 [hash] OP_EQUAL + TH, //!< [hash] OP_TEMPLATEHASH OP_EQUAL WRAP_A, //!< OP_TOALTSTACK [X] OP_FROMALTSTACK WRAP_S, //!< OP_SWAP [X] WRAP_C, //!< [X] OP_CHECKSIG WRAP_D, //!< OP_DUP OP_IF [X] OP_ENDIF WRAP_V, //!< [X] OP_VERIFY (or -VERIFY version of last opcode in X) + WRAP_R, //!< [X] OP_TEMPLATEHASH OP_SWAP OP_CHECKSIGVERIFY 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 @@ -236,6 +246,18 @@ enum class Availability { MAYBE, }; +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), 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, TAPSCRIPT, @@ -285,7 +307,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); @@ -475,6 +497,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}; } }; @@ -574,7 +597,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. @@ -768,12 +791,17 @@ struct Node { switch (node.fragment) { case Fragment::PK_K: return BuildScript(ctx.ToPKBytes(node.keys[0])); case Fragment::PK_H: return BuildScript(OP_DUP, OP_HASH160, ctx.ToPKHBytes(node.keys[0]), OP_EQUALVERIFY); + case Fragment::PK_I: { + CHECK_NONFATAL(is_tapscript); + return BuildScript(OP_INTERNALKEY); + } case Fragment::OLDER: return BuildScript(node.k, OP_CHECKSEQUENCEVERIFY); case Fragment::AFTER: return BuildScript(node.k, OP_CHECKLOCKTIMEVERIFY); case Fragment::SHA256: return BuildScript(OP_SIZE, 32, OP_EQUALVERIFY, OP_SHA256, node.data, verify ? OP_EQUALVERIFY : OP_EQUAL); case Fragment::RIPEMD160: return BuildScript(OP_SIZE, 32, OP_EQUALVERIFY, OP_RIPEMD160, node.data, verify ? OP_EQUALVERIFY : OP_EQUAL); case Fragment::HASH256: return BuildScript(OP_SIZE, 32, OP_EQUALVERIFY, OP_HASH256, node.data, verify ? OP_EQUALVERIFY : OP_EQUAL); case Fragment::HASH160: return BuildScript(OP_SIZE, 32, OP_EQUALVERIFY, OP_HASH160, node.data, verify ? OP_EQUALVERIFY : OP_EQUAL); + case Fragment::TH: return BuildScript(node.data, OP_TEMPLATEHASH, verify ? OP_EQUALVERIFY : OP_EQUAL); case Fragment::WRAP_A: return BuildScript(OP_TOALTSTACK, subs[0], OP_FROMALTSTACK); case Fragment::WRAP_S: return BuildScript(OP_SWAP, subs[0]); case Fragment::WRAP_C: return BuildScript(std::move(subs[0]), verify ? OP_CHECKSIGVERIFY : OP_CHECKSIG); @@ -785,8 +813,16 @@ 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: { + 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]); @@ -834,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)); @@ -861,11 +897,19 @@ 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]); 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]); @@ -887,14 +931,23 @@ struct Node { if (!key_str) return {}; return std::move(ret) + "pk_h(" + std::move(*key_str) + ")"; } + case Fragment::PK_I: { + CHECK_NONFATAL(is_tapscript); + return std::move(ret) + "pk_i()"; + } case Fragment::AFTER: return std::move(ret) + "after(" + util::ToString(node.k) + ")"; case Fragment::OLDER: return std::move(ret) + "older(" + util::ToString(node.k) + ")"; case Fragment::HASH256: return std::move(ret) + "hash256(" + HexStr(node.data) + ")"; case Fragment::HASH160: return std::move(ret) + "hash160(" + HexStr(node.data) + ")"; case Fragment::SHA256: return std::move(ret) + "sha256(" + HexStr(node.data) + ")"; + case Fragment::TH: return std::move(ret) + "th(" + HexStr(node.data) + ")"; 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]) + ")"; @@ -947,12 +1000,14 @@ struct Node { case Fragment::JUST_0: return {0, {}, 0}; case Fragment::PK_K: return {0, 0, 0}; case Fragment::PK_H: return {3, 0, 0}; + case Fragment::PK_I: return {1, 0, 0}; case Fragment::OLDER: case Fragment::AFTER: return {1, 0, {}}; case Fragment::SHA256: case Fragment::RIPEMD160: case Fragment::HASH256: case Fragment::HASH160: return {4, 0, {}}; + case Fragment::TH: return {2, 0, {}}; case Fragment::AND_V: return {subs[0]->ops.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}; @@ -996,8 +1051,10 @@ 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}; case Fragment::THRESH: { uint32_t count = 0; auto sats = Vector(internal::MaxInt(0)); @@ -1024,6 +1081,7 @@ struct Node { case Fragment::AFTER: return {SatInfo::Push() + SatInfo::Nop(), {}}; case Fragment::PK_K: return {SatInfo::Push()}; case Fragment::PK_H: return {SatInfo::OP_DUP() + SatInfo::Hash() + SatInfo::Push() + SatInfo::OP_EQUALVERIFY()}; + case Fragment::PK_I: return {SatInfo::Push()}; case Fragment::SHA256: case Fragment::RIPEMD160: case Fragment::HASH256: @@ -1031,6 +1089,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}; @@ -1096,11 +1156,21 @@ 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, 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()); @@ -1131,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) { @@ -1140,10 +1212,12 @@ struct Node { case Fragment::AFTER: return {0, {}}; case Fragment::PK_K: return {sig_size, 1}; case Fragment::PK_H: return {sig_size + pubkey_size, 1 + pubkey_size}; + case Fragment::PK_I: return {sig_size, 1}; case Fragment::SHA256: 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}; @@ -1164,10 +1238,15 @@ 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, {}}; 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) { @@ -1187,21 +1266,36 @@ 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{}; + } 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; + }; + // 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_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); @@ -1209,7 +1303,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 @@ -1230,13 +1324,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 @@ -1316,6 +1411,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 @@ -1356,10 +1458,12 @@ 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: case Fragment::WRAP_N: + case Fragment::WRAP_R: return std::move(subres[0]); case Fragment::WRAP_D: { auto &x = subres[0]; @@ -1385,8 +1489,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) @@ -1428,7 +1532,7 @@ struct Node { return ret; }; - return TreeEval(tester); + return TreeEval(SigMsgType{}, downfn, tester); } public: @@ -1466,7 +1570,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) { @@ -1582,6 +1687,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: @@ -1590,6 +1696,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]; @@ -1622,8 +1729,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; } @@ -1702,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: */ @@ -1732,6 +1841,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 @@ -1771,6 +1883,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) @@ -1904,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 {}; } @@ -1932,6 +2051,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 {}; @@ -1946,6 +2071,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 {}; @@ -1974,6 +2104,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 {}; @@ -1990,6 +2128,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)) { @@ -2045,6 +2189,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; @@ -2057,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; @@ -2197,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: */ @@ -2206,6 +2365,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 @@ -2246,12 +2409,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 @@ -2263,7 +2440,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) { @@ -2296,6 +2477,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]))) { @@ -2330,6 +2518,12 @@ inline NodeRef DecodeScript(I& in, I last, const Ctx& ctx) break; } } + 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; + break; + } // Multi if (last - in >= 3 && in[0].first == OP_CHECKMULTISIG) { if (IsTapscript(ctx.MsContext())) return {}; @@ -2390,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; @@ -2404,6 +2606,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 {}; @@ -2481,11 +2696,22 @@ 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()))); 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 61f578769c13..dec9560d23c1 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; } @@ -248,6 +252,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; } @@ -259,6 +276,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 { @@ -274,7 +295,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; } @@ -292,6 +314,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 { @@ -309,8 +335,19 @@ struct TapSatisfier: Satisfier { } //! Satisfy a BIP340 signature check. - miniscript::Availability Sign(const XOnlyPubKey& key, std::vector& sig) const { - if (CreateTaprootScriptSig(m_creator, m_sig_data, m_provider, sig, key, m_leaf_hash, SigVersion::TAPSCRIPT)) { + miniscript::Availability Sign(const XOnlyPubKey& key, const miniscript::SigMsgType& sig_type, std::vector& sig) const { + 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 (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; } return miniscript::Availability::NO; @@ -733,7 +770,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/descriptor_tests.cpp b/src/test/descriptor_tests.cpp index 63c53a842c1f..ea3f80169d2b 100644 --- a/src/test/descriptor_tests.cpp +++ b/src/test/descriptor_tests.cpp @@ -1071,6 +1071,86 @@ 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, OP_TEMPLATEHASH and OP_CHECKSIGFROMSTACK 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}}, + } + ); + 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}}, + } + ); + 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/fuzz/miniscript.cpp b/src/test/fuzz/miniscript.cpp index 18681cf503d0..78b12f1fb65b 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; @@ -45,11 +48,13 @@ 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}; - // 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}; @@ -91,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); + } } } @@ -106,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; /** @@ -117,8 +154,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 +222,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 +282,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. @@ -248,11 +304,29 @@ 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 { + 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; + } 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; } @@ -277,6 +351,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. @@ -299,6 +377,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. @@ -335,6 +415,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)) {} }; @@ -385,7 +466,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 +581,19 @@ 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()}}; + } + 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; } @@ -575,6 +669,18 @@ 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: + case Fragment::TH: + case Fragment::CMS: + case Fragment::WRAP_R: 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 +691,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 +700,7 @@ struct SmartInfo switch (frag) { case Fragment::PK_K: case Fragment::PK_H: + case Fragment::PK_I: n_keys = 1; break; case Fragment::MULTI: @@ -605,8 +712,13 @@ 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: data_size = 32; break; case Fragment::RIPEMD160: @@ -623,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: @@ -773,7 +886,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 +899,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); @@ -800,6 +919,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)}}; @@ -811,6 +934,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: @@ -820,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: @@ -856,7 +985,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,13 +1001,13 @@ 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 // used in the miniscript::internal::Parse function to prevent runaway string parsing. scriptsize += miniscript::internal::ComputeScriptLen(node_info->fragment, ""_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: @@ -889,6 +1018,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; @@ -899,6 +1031,9 @@ NodeRef GenNode(MsCtx script_ctx, F ConsumeNode, Type root_type, bool strict_val case Fragment::HASH256: ops += 4; break; + case Fragment::TH: + ops += 2; + break; case Fragment::ANDOR: ops += 3; break; @@ -917,6 +1052,9 @@ NodeRef GenNode(MsCtx script_ctx, F ConsumeNode, Type root_type, bool strict_val case Fragment::OR_I: ops += 3; break; + case Fragment::CMS: + ops += 2; + break; case Fragment::THRESH: ops += node_info->subtypes.size(); break; @@ -948,6 +1086,9 @@ NodeRef GenNode(MsCtx script_ctx, F ConsumeNode, Type root_type, bool strict_val case Fragment::WRAP_N: ops += 1; break; + case Fragment::WRAP_R: + ops += 3; + break; } if (ops > MAX_OPS_PER_SCRIPT) return {}; auto subtypes = node_info->subtypes; @@ -1011,13 +1152,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 +1172,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 +1241,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 +1274,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 +1312,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: { @@ -1188,6 +1332,8 @@ void TestNode(const MsCtx script_ctx, const NodeRef& node, FuzzedDataProvider& p return TEST_DATA.ripemd160_preimages.count(node.data); case Fragment::HASH160: return TEST_DATA.hash160_preimages.count(node.data); + case Fragment::TH: + return uint256{node.data} == TestData::MESSAGE_HASH; default: assert(false); } @@ -1216,9 +1362,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 +1377,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 +1391,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..60cd148bb37f 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). @@ -48,17 +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() { - // 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}; + // 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 @@ -99,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); + } } } }; @@ -112,6 +135,7 @@ enum class ChallengeType { RIPEMD160, HASH256, HASH160, + TEMPLATEHASH, OLDER, AFTER, PK @@ -131,8 +155,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 +221,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; @@ -216,9 +246,20 @@ 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 { 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; + } + 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; @@ -226,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; } @@ -251,6 +299,10 @@ struct Satisfier : public KeyConverter { miniscript::Availability SatRIPEMD160(const std::vector& 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))) && uint256{data} == TestData::MESSAGE_HASH; + } }; /** Mocking signature/timelock checker. @@ -289,6 +341,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; @@ -315,6 +371,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); @@ -324,13 +382,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()); } @@ -351,7 +409,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. @@ -360,7 +418,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; @@ -424,7 +482,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. @@ -480,9 +543,10 @@ 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); + 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); } @@ -596,8 +660,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}; @@ -723,6 +787,56 @@ 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 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("}; + 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); + 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); + 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{{ + "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/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" : [ { diff --git a/test/functional/wallet_miniscript.py b/test/functional/wallet_miniscript.py index 064eac499b2f..1c1dd2459f8f 100755 --- a/test/functional/wallet_miniscript.py +++ b/test/functional/wallet_miniscript.py @@ -55,8 +55,15 @@ 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]})}})", + # A Taproot with a leaf that may only be spent by a specific transaction. + 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 { @@ -199,6 +206,31 @@ "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, + }, + # 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, + } ]