Merge bitcoin/bitcoin#31713: miniscript refactor: Remove unique_ptr-indirection

964c44cdcd test(miniscript): Prove avoidance of stack overflow (Hodlinator)
198bbaee49 refactor(miniscript): Destroy nodes one full subs-vector at a time (Hodlinator)
50cab8570e refactor(miniscript): Remove NodeRef & MakeNodeRef() (Hodlinator)
15fb34de41 refactor(miniscript): Remove superfluous unique_ptr-indirection (Hodlinator)
e55b23c170 refactor(miniscript): Remove Node::subs mutability (Hodlinator)
c6f798b222 refactor(miniscript): Make fields non-const & private (Hodlinator)
22e4115312 doc(miniscript): Remove mention of shared pointers (Hodlinator)

Pull request description:

  Removes one level of unnecessary indirection, which was a change that originally [aided in finding one issue](https://github.com/bitcoin/bitcoin/pull/30866#pullrequestreview-2434704657) in #30866. Simplifies the code one step further than 09a1875ad8 belonging to aforementioned PR.

  Also adds test which verifies resistance to stack overflow when it comes to `~Node()` and `Node::Clone()`.

  No observed difference when running benchmarks: ExpandDescriptor/WalletIsMineDescriptors/WalletIsMineMigratedDescriptors/WalletLoadingDescriptors.

  Followup to #30866.

ACKs for top commit:
  achow101:
    ACK 964c44cdcd
  darosior:
    Code review ACK 964c44cdcd
  l0rinc:
    ACK 964c44cdcd

Tree-SHA512: 32927e8f0f916fb70372ffd110f7ec7207d9e7a099c21c0a7482a12e96593b673c339719f4ab166ad7c086dc43767315fc1742c5b236a3facc45c4cfeb5872e9
This commit is contained in:
Ava Chow committed 2026-01-30 15:17:59 -08:00
commit 4f85b05131
4 files changed
+387 -335

No files matched your search

+19 -17
View File
@@ -1584,13 +1584,13 @@ public:
class MiniscriptDescriptor final : public DescriptorImpl class MiniscriptDescriptor final : public DescriptorImpl
{ {
private: private:
miniscript::NodeRef<uint32_t> m_node; miniscript::Node<uint32_t> m_node;
protected: protected:
std::vector<CScript> MakeScripts(const std::vector<CPubKey>& keys, std::span<const CScript> scripts, std::vector<CScript> MakeScripts(const std::vector<CPubKey>& keys, std::span<const CScript> scripts,
FlatSigningProvider& provider) const override FlatSigningProvider& provider) const override
{ {
const auto script_ctx{m_node->GetMsCtx()}; const auto script_ctx{m_node.GetMsCtx()};
for (const auto& key : keys) { for (const auto& key : keys) {
if (miniscript::IsTapscript(script_ctx)) { if (miniscript::IsTapscript(script_ctx)) {
provider.pubkeys.emplace(Hash160(XOnlyPubKey{key}), key); provider.pubkeys.emplace(Hash160(XOnlyPubKey{key}), key);
@@ -1598,17 +1598,17 @@ protected:
provider.pubkeys.emplace(key.GetID(), key); provider.pubkeys.emplace(key.GetID(), key);
} }
} }
return Vector(m_node->ToScript(ScriptMaker(keys, script_ctx))); return Vector(m_node.ToScript(ScriptMaker(keys, script_ctx)));
} }
public: public:
MiniscriptDescriptor(std::vector<std::unique_ptr<PubkeyProvider>> providers, miniscript::NodeRef<uint32_t> node) MiniscriptDescriptor(std::vector<std::unique_ptr<PubkeyProvider>> providers, miniscript::Node<uint32_t>&& node)
: DescriptorImpl(std::move(providers), "?"), m_node(std::move(node)) : DescriptorImpl(std::move(providers), "?"), m_node(std::move(node))
{ {
// Traverse miniscript tree for unsafe use of older() // Traverse miniscript tree for unsafe use of older()
miniscript::ForEachNode(*m_node, [&](const miniscript::Node<uint32_t>& node) { miniscript::ForEachNode(m_node, [&](const miniscript::Node<uint32_t>& node) {
if (node.fragment == miniscript::Fragment::OLDER) { if (node.Fragment() == miniscript::Fragment::OLDER) {
const uint32_t raw = node.k; const uint32_t raw = node.K();
const uint32_t value_part = raw & ~CTxIn::SEQUENCE_LOCKTIME_TYPE_FLAG; const uint32_t value_part = raw & ~CTxIn::SEQUENCE_LOCKTIME_TYPE_FLAG;
if (value_part > CTxIn::SEQUENCE_LOCKTIME_MASK) { if (value_part > CTxIn::SEQUENCE_LOCKTIME_MASK) {
const bool is_time_based = (raw & CTxIn::SEQUENCE_LOCKTIME_TYPE_FLAG) != 0; const bool is_time_based = (raw & CTxIn::SEQUENCE_LOCKTIME_TYPE_FLAG) != 0;
@@ -1626,7 +1626,7 @@ public:
const DescriptorCache* cache = nullptr) const override const DescriptorCache* cache = nullptr) const override
{ {
bool has_priv_key{false}; bool has_priv_key{false};
auto res = m_node->ToString(StringMaker(arg, m_pubkey_args, type, cache), has_priv_key); auto res = m_node.ToString(StringMaker(arg, m_pubkey_args, type, cache), has_priv_key);
if (res) out = *res; if (res) out = *res;
if (type == StringType::PRIVATE) { if (type == StringType::PRIVATE) {
Assume(res.has_value()); Assume(res.has_value());
@@ -1639,15 +1639,17 @@ public:
bool IsSolvable() const override { return true; } bool IsSolvable() const override { return true; }
bool IsSingleType() const final { return true; } bool IsSingleType() const final { return true; }
std::optional<int64_t> ScriptSize() const override { return m_node->ScriptSize(); } std::optional<int64_t> ScriptSize() const override { return m_node.ScriptSize(); }
std::optional<int64_t> MaxSatSize(bool) const override { std::optional<int64_t> MaxSatSize(bool) const override
{
// For Miniscript we always assume high-R ECDSA signatures. // For Miniscript we always assume high-R ECDSA signatures.
return m_node->GetWitnessSize(); return m_node.GetWitnessSize();
} }
std::optional<int64_t> MaxSatisfactionElems() const override { std::optional<int64_t> MaxSatisfactionElems() const override
return m_node->GetStackSize(); {
return m_node.GetStackSize();
} }
std::unique_ptr<DescriptorImpl> Clone() const override std::unique_ptr<DescriptorImpl> Clone() const override
@@ -1657,7 +1659,7 @@ public:
for (const auto& arg : m_pubkey_args) { for (const auto& arg : m_pubkey_args) {
providers.push_back(arg->Clone()); providers.push_back(arg->Clone());
} }
return std::make_unique<MiniscriptDescriptor>(std::move(providers), m_node->Clone()); return std::make_unique<MiniscriptDescriptor>(std::move(providers), m_node.Clone());
} }
}; };
@@ -2566,7 +2568,7 @@ std::vector<std::unique_ptr<DescriptorImpl>> ParseScript(uint32_t& key_exp_index
} }
if (!node->IsSane() || node->IsNotSatisfiable()) { if (!node->IsSane() || node->IsNotSatisfiable()) {
// Try to find the first insane sub for better error reporting. // Try to find the first insane sub for better error reporting.
auto insane_node = node.get(); const auto* insane_node = &node.value();
if (const auto sub = node->FindInsaneSub()) insane_node = sub; if (const auto sub = node->FindInsaneSub()) insane_node = sub;
error = *insane_node->ToString(parser); error = *insane_node->ToString(parser);
if (!insane_node->IsValid()) { if (!insane_node->IsValid()) {
@@ -2575,7 +2577,7 @@ std::vector<std::unique_ptr<DescriptorImpl>> ParseScript(uint32_t& key_exp_index
error += " is not sane"; error += " is not sane";
if (!insane_node->IsNonMalleable()) { if (!insane_node->IsNonMalleable()) {
error += ": malleable witnesses exist"; error += ": malleable witnesses exist";
} else if (insane_node == node.get() && !insane_node->NeedsSignature()) { } else if (insane_node == &node.value() && !insane_node->NeedsSignature()) {
error += ": witnesses without signature exist"; error += ": witnesses without signature exist";
} else if (!insane_node->CheckTimeLocksMix()) { } else if (!insane_node->CheckTimeLocksMix()) {
error += ": contains mixes of timelocks expressed in blocks and seconds"; error += ": contains mixes of timelocks expressed in blocks and seconds";
@@ -2775,7 +2777,7 @@ std::unique_ptr<DescriptorImpl> InferScript(const CScript& script, ParseScriptCo
for (auto& key : parser.m_keys) { for (auto& key : parser.m_keys) {
keys.emplace_back(std::move(key.at(0))); keys.emplace_back(std::move(key.at(0)));
} }
return std::make_unique<MiniscriptDescriptor>(std::move(keys), std::move(node)); return std::make_unique<MiniscriptDescriptor>(std::move(keys), std::move(*node));
} }
} }
+279 -246
View File
File diff suppressed because it is too large. Load diff
+33 -37
View File
@@ -15,11 +15,11 @@
#include <util/strencodings.h> #include <util/strencodings.h>
#include <algorithm> #include <algorithm>
#include <optional>
namespace { namespace {
using Fragment = miniscript::Fragment; using Fragment = miniscript::Fragment;
using NodeRef = miniscript::NodeRef<CPubKey>;
using Node = miniscript::Node<CPubKey>; using Node = miniscript::Node<CPubKey>;
using Type = miniscript::Type; using Type = miniscript::Type;
using MsCtx = miniscript::MiniscriptContext; using MsCtx = miniscript::MiniscriptContext;
@@ -316,11 +316,6 @@ const struct KeyComparator {
// A dummy scriptsig to pass to VerifyScript (we always use Segwit v0). // A dummy scriptsig to pass to VerifyScript (we always use Segwit v0).
const CScript DUMMY_SCRIPTSIG; const CScript DUMMY_SCRIPTSIG;
//! Construct a miniscript node as a shared_ptr.
template<typename... Args> NodeRef MakeNodeRef(Args&&... args) {
return miniscript::MakeNodeRef<CPubKey>(miniscript::internal::NoDupCheck{}, std::forward<Args>(args)...);
}
/** Information about a yet to be constructed Miniscript node. */ /** Information about a yet to be constructed Miniscript node. */
struct NodeInfo { struct NodeInfo {
//! The type of this node //! The type of this node
@@ -852,14 +847,15 @@ std::optional<NodeInfo> ConsumeNodeSmart(MsCtx script_ctx, FuzzedDataProvider& p
* Generate a Miniscript node based on the fuzzer's input. * Generate a Miniscript node based on the fuzzer's input.
* *
* - ConsumeNode is a function object taking a Type, and returning an std::optional<NodeInfo>. * - ConsumeNode is a function object taking a Type, and returning an std::optional<NodeInfo>.
* - root_type is the required type properties of the constructed NodeRef. * - root_type is the required type properties of the constructed Node.
* - strict_valid sets whether ConsumeNode is expected to guarantee a NodeInfo that results in * - strict_valid sets whether ConsumeNode is expected to guarantee a NodeInfo that results in
* a NodeRef whose Type() matches the type fed to ConsumeNode. * a Node whose Type() matches the type fed to ConsumeNode.
*/ */
template<typename F> template <typename F>
NodeRef GenNode(MsCtx script_ctx, F ConsumeNode, Type root_type, bool strict_valid = false) { std::optional<Node> GenNode(MsCtx script_ctx, F ConsumeNode, Type root_type, bool strict_valid = false)
{
/** A stack of miniscript Nodes being built up. */ /** A stack of miniscript Nodes being built up. */
std::vector<NodeRef> stack; std::vector<Node> stack;
/** The queue of instructions. */ /** The queue of instructions. */
std::vector<std::pair<Type, std::optional<NodeInfo>>> todo{{root_type, {}}}; std::vector<std::pair<Type, std::optional<NodeInfo>>> todo{{root_type, {}}};
/** Predict the number of (static) script ops. */ /** Predict the number of (static) script ops. */
@@ -962,36 +958,36 @@ NodeRef GenNode(MsCtx script_ctx, F ConsumeNode, Type root_type, bool strict_val
} else { } else {
// The back of todo has fragment and number of children decided, and // The back of todo has fragment and number of children decided, and
// those children have been constructed at the back of stack. Pop // those children have been constructed at the back of stack. Pop
// that entry off todo, and use it to construct a new NodeRef on // that entry off todo, and use it to construct a new Node on
// stack. // stack.
NodeInfo& info = *todo.back().second; NodeInfo& info = *todo.back().second;
// Gather children from the back of stack. // Gather children from the back of stack.
std::vector<NodeRef> sub; std::vector<Node> sub;
sub.reserve(info.subtypes.size()); sub.reserve(info.subtypes.size());
for (size_t i = 0; i < info.subtypes.size(); ++i) { for (size_t i = 0; i < info.subtypes.size(); ++i) {
sub.push_back(std::move(*(stack.end() - info.subtypes.size() + i))); sub.push_back(std::move(*(stack.end() - info.subtypes.size() + i)));
} }
stack.erase(stack.end() - info.subtypes.size(), stack.end()); stack.erase(stack.end() - info.subtypes.size(), stack.end());
// Construct new NodeRef. // Construct new Node.
NodeRef node; Node node{[&] {
if (info.keys.empty()) { if (info.keys.empty()) {
node = MakeNodeRef(script_ctx, info.fragment, std::move(sub), std::move(info.hash), info.k); return Node{miniscript::internal::NoDupCheck{}, script_ctx, info.fragment, std::move(sub), std::move(info.hash), info.k};
} else { }
assert(sub.empty()); assert(sub.empty());
assert(info.hash.empty()); assert(info.hash.empty());
node = MakeNodeRef(script_ctx, info.fragment, std::move(info.keys), info.k); return Node{miniscript::internal::NoDupCheck{}, script_ctx, info.fragment, std::move(info.keys), info.k};
} }()};
// Verify acceptability. // Verify acceptability.
if (!node || (node->GetType() & "KVWB"_mst) == ""_mst) { if ((node.GetType() & "KVWB"_mst) == ""_mst) {
assert(!strict_valid); assert(!strict_valid);
return {}; return {};
} }
if (!(type_needed == ""_mst)) { if (!(type_needed == ""_mst)) {
assert(node->GetType() << type_needed); assert(node.GetType() << type_needed);
} }
if (!node->IsValid()) return {}; if (!node.IsValid()) return {};
// Update resource predictions. // Update resource predictions.
if (node->fragment == Fragment::WRAP_V && node->subs[0]->GetType() << "x"_mst) { if (node.Fragment() == Fragment::WRAP_V && node.Subs()[0].GetType() << "x"_mst) {
ops += 1; ops += 1;
scriptsize += 1; scriptsize += 1;
} }
@@ -1005,9 +1001,9 @@ NodeRef GenNode(MsCtx script_ctx, F ConsumeNode, Type root_type, bool strict_val
} }
} }
assert(stack.size() == 1); assert(stack.size() == 1);
assert(stack[0]->GetStaticOps() == ops); assert(stack[0].GetStaticOps() == ops);
assert(stack[0]->ScriptSize() == scriptsize); assert(stack[0].ScriptSize() == scriptsize);
stack[0]->DuplicateKeyCheck(KEY_COMP); stack[0].DuplicateKeyCheck(KEY_COMP);
return std::move(stack[0]); return std::move(stack[0]);
} }
@@ -1032,7 +1028,7 @@ void SatisfactionToWitness(MsCtx ctx, CScriptWitness& witness, const CScript& sc
} }
/** Perform various applicable tests on a miniscript Node. */ /** 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 std::optional<Node>& node, FuzzedDataProvider& provider)
{ {
if (!node) return; if (!node) return;
@@ -1167,28 +1163,28 @@ void TestNode(const MsCtx script_ctx, const NodeRef& node, FuzzedDataProvider& p
return sig_ptr != nullptr && sig_ptr->second; return sig_ptr != nullptr && sig_ptr->second;
}; };
bool satisfiable = node->IsSatisfiable([&](const Node& node) -> bool { bool satisfiable = node->IsSatisfiable([&](const Node& node) -> bool {
switch (node.fragment) { switch (node.Fragment()) {
case Fragment::PK_K: case Fragment::PK_K:
case Fragment::PK_H: case Fragment::PK_H:
return is_key_satisfiable(node.keys[0]); return is_key_satisfiable(node.Keys()[0]);
case Fragment::MULTI: case Fragment::MULTI:
case Fragment::MULTI_A: { case Fragment::MULTI_A: {
size_t sats = std::count_if(node.keys.begin(), node.keys.end(), [&](const auto& key) { size_t sats = std::ranges::count_if(node.Keys(), [&](const auto& key) {
return size_t(is_key_satisfiable(key)); return size_t(is_key_satisfiable(key));
}); });
return sats >= node.k; return sats >= node.K();
} }
case Fragment::OLDER: case Fragment::OLDER:
case Fragment::AFTER: case Fragment::AFTER:
return node.k & 1; return node.K() & 1;
case Fragment::SHA256: case Fragment::SHA256:
return TEST_DATA.sha256_preimages.contains(node.data); return TEST_DATA.sha256_preimages.contains(node.Data());
case Fragment::HASH256: case Fragment::HASH256:
return TEST_DATA.hash256_preimages.contains(node.data); return TEST_DATA.hash256_preimages.contains(node.Data());
case Fragment::RIPEMD160: case Fragment::RIPEMD160:
return TEST_DATA.ripemd160_preimages.contains(node.data); return TEST_DATA.ripemd160_preimages.contains(node.Data());
case Fragment::HASH160: case Fragment::HASH160:
return TEST_DATA.hash160_preimages.contains(node.data); return TEST_DATA.hash160_preimages.contains(node.Data());
default: default:
assert(false); assert(false);
} }
+56 -35
View File
@@ -292,33 +292,32 @@ public:
}; };
using Fragment = miniscript::Fragment; using Fragment = miniscript::Fragment;
using NodeRef = miniscript::NodeRef<CPubKey>;
using miniscript::operator""_mst; using miniscript::operator""_mst;
using Node = miniscript::Node<CPubKey>; using Node = miniscript::Node<CPubKey>;
/** Compute all challenges (pubkeys, hashes, timelocks) that occur in a given Miniscript. */ /** Compute all challenges (pubkeys, hashes, timelocks) that occur in a given Miniscript. */
std::set<Challenge> FindChallenges(const Node* root) std::set<Challenge> FindChallenges(const Node& root)
{ {
std::set<Challenge> chal; std::set<Challenge> chal;
for (std::vector stack{root}; !stack.empty();) { for (std::vector stack{&root}; !stack.empty();) {
const auto* ref{stack.back()}; const auto* ref{stack.back()};
stack.pop_back(); stack.pop_back();
for (const auto& key : ref->keys) { for (const auto& key : ref->Keys()) {
chal.emplace(ChallengeType::PK, ChallengeNumber(key)); chal.emplace(ChallengeType::PK, ChallengeNumber(key));
} }
switch (ref->fragment) { switch (ref->Fragment()) {
case Fragment::OLDER: chal.emplace(ChallengeType::OLDER, ref->k); break; case Fragment::OLDER: chal.emplace(ChallengeType::OLDER, ref->K()); break;
case Fragment::AFTER: chal.emplace(ChallengeType::AFTER, ref->k); break; case Fragment::AFTER: chal.emplace(ChallengeType::AFTER, ref->K()); break;
case Fragment::SHA256: chal.emplace(ChallengeType::SHA256, ChallengeNumber(ref->data)); break; case Fragment::SHA256: chal.emplace(ChallengeType::SHA256, ChallengeNumber(ref->Data())); break;
case Fragment::RIPEMD160: chal.emplace(ChallengeType::RIPEMD160, ChallengeNumber(ref->data)); break; case Fragment::RIPEMD160: chal.emplace(ChallengeType::RIPEMD160, ChallengeNumber(ref->Data())); break;
case Fragment::HASH256: chal.emplace(ChallengeType::HASH256, ChallengeNumber(ref->data)); break; case Fragment::HASH256: chal.emplace(ChallengeType::HASH256, ChallengeNumber(ref->Data())); break;
case Fragment::HASH160: chal.emplace(ChallengeType::HASH160, ChallengeNumber(ref->data)); break; case Fragment::HASH160: chal.emplace(ChallengeType::HASH160, ChallengeNumber(ref->Data())); break;
default: break; default: break;
} }
for (const auto& sub : ref->subs) { for (const auto& sub : ref->Subs()) {
stack.push_back(sub.get()); stack.push_back(&sub);
} }
} }
return chal; return chal;
@@ -346,9 +345,10 @@ void SatisfactionToWitness(miniscript::MiniscriptContext ctx, CScriptWitness& wi
struct MiniScriptTest : BasicTestingSetup { struct MiniScriptTest : BasicTestingSetup {
/** Run random satisfaction tests. */ /** Run random satisfaction tests. */
void TestSatisfy(const KeyConverter& converter, const std::string& testcase, const NodeRef& node) { void TestSatisfy(const KeyConverter& converter, const Node& node)
auto script = node->ToScript(converter); {
const auto challenges{FindChallenges(node.get())}; // Find all challenges in the generated miniscript. auto script = node.ToScript(converter);
const auto challenges{FindChallenges(node)}; // Find all challenges in the generated miniscript.
std::vector<Challenge> challist(challenges.begin(), challenges.end()); std::vector<Challenge> challist(challenges.begin(), challenges.end());
for (int iter = 0; iter < 3; ++iter) { for (int iter = 0; iter < 3; ++iter) {
std::shuffle(challist.begin(), challist.end(), m_rng); std::shuffle(challist.begin(), challist.end(), m_rng);
@@ -365,12 +365,12 @@ void TestSatisfy(const KeyConverter& converter, const std::string& testcase, con
// Run malleable satisfaction algorithm. // Run malleable satisfaction algorithm.
CScriptWitness witness_mal; CScriptWitness witness_mal;
const bool mal_success = node->Satisfy(satisfier, witness_mal.stack, false) == miniscript::Availability::YES; const bool mal_success = node.Satisfy(satisfier, witness_mal.stack, false) == miniscript::Availability::YES;
SatisfactionToWitness(converter.MsContext(), witness_mal, script, builder); SatisfactionToWitness(converter.MsContext(), witness_mal, script, builder);
// Run non-malleable satisfaction algorithm. // Run non-malleable satisfaction algorithm.
CScriptWitness witness_nonmal; CScriptWitness witness_nonmal;
const bool nonmal_success = node->Satisfy(satisfier, witness_nonmal.stack, true) == miniscript::Availability::YES; const bool nonmal_success = node.Satisfy(satisfier, witness_nonmal.stack, true) == miniscript::Availability::YES;
// Compute witness size (excluding script push, control block, and witness count encoding). // Compute witness size (excluding script push, control block, and witness count encoding).
const uint64_t wit_size{GetSerializeSize(witness_nonmal.stack) - GetSizeOfCompactSize(witness_nonmal.stack.size())}; const uint64_t wit_size{GetSerializeSize(witness_nonmal.stack) - GetSizeOfCompactSize(witness_nonmal.stack.size())};
SatisfactionToWitness(converter.MsContext(), witness_nonmal, script, builder); SatisfactionToWitness(converter.MsContext(), witness_nonmal, script, builder);
@@ -379,23 +379,23 @@ void TestSatisfy(const KeyConverter& converter, const std::string& testcase, con
// Non-malleable satisfactions are bounded by the satisfaction size plus: // Non-malleable satisfactions are bounded by the satisfaction size plus:
// - For P2WSH spends, the witness script // - For P2WSH spends, the witness script
// - For Tapscript spends, both the witness script and the control block // - For Tapscript spends, both the witness script and the control block
const size_t max_stack_size{*node->GetStackSize() + 1 + miniscript::IsTapscript(converter.MsContext())}; const size_t max_stack_size{*node.GetStackSize() + 1 + miniscript::IsTapscript(converter.MsContext())};
BOOST_CHECK(witness_nonmal.stack.size() <= max_stack_size); BOOST_CHECK(witness_nonmal.stack.size() <= max_stack_size);
// If a non-malleable satisfaction exists, the malleable one must also exist, and be identical to it. // If a non-malleable satisfaction exists, the malleable one must also exist, and be identical to it.
BOOST_CHECK(mal_success); BOOST_CHECK(mal_success);
BOOST_CHECK(witness_nonmal.stack == witness_mal.stack); BOOST_CHECK(witness_nonmal.stack == witness_mal.stack);
assert(wit_size <= *node->GetWitnessSize()); assert(wit_size <= *node.GetWitnessSize());
// Test non-malleable satisfaction. // Test non-malleable satisfaction.
ScriptError serror; ScriptError serror;
bool res = VerifyScript(CScript(), script_pubkey, &witness_nonmal, STANDARD_SCRIPT_VERIFY_FLAGS, checker, &serror); bool res = VerifyScript(CScript(), script_pubkey, &witness_nonmal, STANDARD_SCRIPT_VERIFY_FLAGS, checker, &serror);
// Non-malleable satisfactions are guaranteed to be valid if ValidSatisfactions(). // Non-malleable satisfactions are guaranteed to be valid if ValidSatisfactions().
if (node->ValidSatisfactions()) BOOST_CHECK(res); if (node.ValidSatisfactions()) BOOST_CHECK(res);
// More detailed: non-malleable satisfactions must be valid, or could fail with ops count error (if CheckOpsLimit failed), // 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 fails). // or with a stack size error (if CheckStackSize check fails).
BOOST_CHECK(res || BOOST_CHECK(res ||
(!node->CheckOpsLimit() && serror == ScriptError::SCRIPT_ERR_OP_COUNT) || (!node.CheckOpsLimit() && serror == ScriptError::SCRIPT_ERR_OP_COUNT) ||
(!node->CheckStackSize() && serror == ScriptError::SCRIPT_ERR_STACK_SIZE)); (!node.CheckStackSize() && serror == ScriptError::SCRIPT_ERR_STACK_SIZE));
} }
if (mal_success && (!nonmal_success || witness_mal.stack != witness_nonmal.stack)) { if (mal_success && (!nonmal_success || witness_mal.stack != witness_nonmal.stack)) {
@@ -407,7 +407,7 @@ void TestSatisfy(const KeyConverter& converter, const std::string& testcase, con
BOOST_CHECK(res || serror == ScriptError::SCRIPT_ERR_OP_COUNT || serror == ScriptError::SCRIPT_ERR_STACK_SIZE); BOOST_CHECK(res || serror == ScriptError::SCRIPT_ERR_OP_COUNT || serror == ScriptError::SCRIPT_ERR_STACK_SIZE);
} }
if (node->IsSane()) { if (node.IsSane()) {
// For sane nodes, the two algorithms behave identically. // For sane nodes, the two algorithms behave identically.
BOOST_CHECK_EQUAL(mal_success, nonmal_success); BOOST_CHECK_EQUAL(mal_success, nonmal_success);
} }
@@ -417,7 +417,7 @@ void TestSatisfy(const KeyConverter& converter, const std::string& testcase, con
// For nonmalleable solutions this is only true if the added condition is PK; // For nonmalleable solutions this is only true if the added condition is PK;
// for other conditions, adding one may make an valid satisfaction become malleable. If the script // for other conditions, adding one may make an valid satisfaction become malleable. If the script
// is sane, this cannot happen however. // is sane, this cannot happen however.
if (node->IsSane() || add < 0 || challist[add].first == ChallengeType::PK) { if (node.IsSane() || add < 0 || challist[add].first == ChallengeType::PK) {
BOOST_CHECK(nonmal_success >= prev_nonmal_success); BOOST_CHECK(nonmal_success >= prev_nonmal_success);
} }
// Remember results for the next added challenge. // Remember results for the next added challenge.
@@ -425,11 +425,11 @@ void TestSatisfy(const KeyConverter& converter, const std::string& testcase, con
prev_nonmal_success = nonmal_success; prev_nonmal_success = nonmal_success;
} }
bool satisfiable = node->IsSatisfiable([](const Node&) { return true; }); bool satisfiable = node.IsSatisfiable([](const Node&) { return true; });
// If the miniscript was satisfiable at all, a satisfaction must be found after all conditions are added. // If the miniscript was satisfiable at all, a satisfaction must be found after all conditions are added.
BOOST_CHECK_EQUAL(prev_mal_success, satisfiable); BOOST_CHECK_EQUAL(prev_mal_success, satisfiable);
// If the miniscript is sane and satisfiable, a nonmalleable satisfaction must eventually be found. // If the miniscript is sane and satisfiable, a nonmalleable satisfaction must eventually be found.
if (node->IsSane()) BOOST_CHECK_EQUAL(prev_nonmal_success, satisfiable); if (node.IsSane()) BOOST_CHECK_EQUAL(prev_nonmal_success, satisfiable);
} }
} }
@@ -472,7 +472,7 @@ void Test(const std::string& ms, const std::string& hexscript, int mode, const K
if (stacklimit != -1) BOOST_CHECK_MESSAGE((int)*node->GetStackSize() == stacklimit, "Stack limit mismatch: " << ms << " (" << *node->GetStackSize() << " vs " << stacklimit << ")"); if (stacklimit != -1) BOOST_CHECK_MESSAGE((int)*node->GetStackSize() == stacklimit, "Stack limit mismatch: " << ms << " (" << *node->GetStackSize() << " vs " << stacklimit << ")");
if (max_wit_size) BOOST_CHECK_MESSAGE(*node->GetWitnessSize() == *max_wit_size, "Witness size limit mismatch: " << ms << " (" << *node->GetWitnessSize() << " vs " << *max_wit_size << ")"); if (max_wit_size) BOOST_CHECK_MESSAGE(*node->GetWitnessSize() == *max_wit_size, "Witness size limit mismatch: " << ms << " (" << *node->GetWitnessSize() << " vs " << *max_wit_size << ")");
if (stack_exec) BOOST_CHECK_MESSAGE(*node->GetExecStackSize() == *stack_exec, "Stack execution limit mismatch: " << ms << " (" << *node->GetExecStackSize() << " vs " << *stack_exec << ")"); if (stack_exec) BOOST_CHECK_MESSAGE(*node->GetExecStackSize() == *stack_exec, "Stack execution limit mismatch: " << ms << " (" << *node->GetExecStackSize() << " vs " << *stack_exec << ")");
TestSatisfy(converter, ms, node); TestSatisfy(converter, *node);
} }
} }
@@ -600,11 +600,11 @@ BOOST_AUTO_TEST_CASE(fixed_tests)
constexpr KeyConverter tap_converter{miniscript::MiniscriptContext::TAPSCRIPT}; constexpr KeyConverter tap_converter{miniscript::MiniscriptContext::TAPSCRIPT};
constexpr KeyConverter wsh_converter{miniscript::MiniscriptContext::P2WSH}; constexpr KeyConverter wsh_converter{miniscript::MiniscriptContext::P2WSH};
const auto no_pubkey{"ac519c"_hex_u8}; const auto no_pubkey{"ac519c"_hex_u8};
BOOST_CHECK(miniscript::FromScript({no_pubkey.begin(), no_pubkey.end()}, tap_converter) == nullptr); BOOST_CHECK(miniscript::FromScript({no_pubkey.begin(), no_pubkey.end()}, tap_converter) == std::nullopt);
const auto incomplete_multi_a{"ba20c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5ba519c"_hex_u8}; const auto incomplete_multi_a{"ba20c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5ba519c"_hex_u8};
BOOST_CHECK(miniscript::FromScript({incomplete_multi_a.begin(), incomplete_multi_a.end()}, tap_converter) == nullptr); BOOST_CHECK(miniscript::FromScript({incomplete_multi_a.begin(), incomplete_multi_a.end()}, tap_converter) == std::nullopt);
const auto incomplete_multi_a_2{"ac2079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ac20c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5ba519c"_hex_u8}; const auto incomplete_multi_a_2{"ac2079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798ac20c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5ba519c"_hex_u8};
BOOST_CHECK(miniscript::FromScript({incomplete_multi_a_2.begin(), incomplete_multi_a_2.end()}, tap_converter) == nullptr); BOOST_CHECK(miniscript::FromScript({incomplete_multi_a_2.begin(), incomplete_multi_a_2.end()}, tap_converter) == std::nullopt);
// Can use multi_a under Tapscript but not P2WSH. // Can use multi_a under Tapscript but not P2WSH.
Test("and_v(v:multi_a(2,03d01115d548e7561b15c38f004d734633687cf4419620095bc5b0f47070afe85a,025601570cb47f238d2b0286db4a990fa0f3ba28d1a319f5e7cf55c2a2444da7cc),after(1231488000))", "?", "20d01115d548e7561b15c38f004d734633687cf4419620095bc5b0f47070afe85aac205601570cb47f238d2b0286db4a990fa0f3ba28d1a319f5e7cf55c2a2444da7ccba529d0400046749b1", TESTMODE_VALID | TESTMODE_NONMAL | TESTMODE_NEEDSIG | TESTMODE_P2WSH_INVALID, 4, 2, {}, {}, 3); Test("and_v(v:multi_a(2,03d01115d548e7561b15c38f004d734633687cf4419620095bc5b0f47070afe85a,025601570cb47f238d2b0286db4a990fa0f3ba28d1a319f5e7cf55c2a2444da7cc),after(1231488000))", "?", "20d01115d548e7561b15c38f004d734633687cf4419620095bc5b0f47070afe85aac205601570cb47f238d2b0286db4a990fa0f3ba28d1a319f5e7cf55c2a2444da7ccba529d0400046749b1", TESTMODE_VALID | TESTMODE_NONMAL | TESTMODE_NEEDSIG | TESTMODE_P2WSH_INVALID, 4, 2, {}, {}, 3);
// Can use more than 20 keys in a multi_a. // Can use more than 20 keys in a multi_a.
@@ -650,13 +650,13 @@ BOOST_AUTO_TEST_CASE(fixed_tests)
// A Script with a non minimal push is invalid // A Script with a non minimal push is invalid
constexpr auto nonminpush{"0000210232780000feff00ffffffffffff21ff005f00ae21ae00000000060602060406564c2102320000060900fe00005f00ae21ae00100000060606060606000000000000000000000000000000000000000000000000000000000000000000"_hex_u8}; constexpr auto nonminpush{"0000210232780000feff00ffffffffffff21ff005f00ae21ae00000000060602060406564c2102320000060900fe00005f00ae21ae00100000060606060606000000000000000000000000000000000000000000000000000000000000000000"_hex_u8};
const CScript nonminpush_script(nonminpush.begin(), nonminpush.end()); const CScript nonminpush_script(nonminpush.begin(), nonminpush.end());
BOOST_CHECK(miniscript::FromScript(nonminpush_script, wsh_converter) == nullptr); BOOST_CHECK(miniscript::FromScript(nonminpush_script, wsh_converter) == std::nullopt);
BOOST_CHECK(miniscript::FromScript(nonminpush_script, tap_converter) == nullptr); BOOST_CHECK(miniscript::FromScript(nonminpush_script, tap_converter) == std::nullopt);
// A non-minimal VERIFY (<key> CHECKSIG VERIFY 1) // A non-minimal VERIFY (<key> CHECKSIG VERIFY 1)
constexpr auto nonminverify{"2103a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7ac6951"_hex_u8}; constexpr auto nonminverify{"2103a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7ac6951"_hex_u8};
const CScript nonminverify_script(nonminverify.begin(), nonminverify.end()); const CScript nonminverify_script(nonminverify.begin(), nonminverify.end());
BOOST_CHECK(miniscript::FromScript(nonminverify_script, wsh_converter) == nullptr); BOOST_CHECK(miniscript::FromScript(nonminverify_script, wsh_converter) == std::nullopt);
BOOST_CHECK(miniscript::FromScript(nonminverify_script, tap_converter) == nullptr); BOOST_CHECK(miniscript::FromScript(nonminverify_script, tap_converter) == std::nullopt);
// A threshold as large as the number of subs is valid. // A threshold as large as the number of subs is valid.
Test("thresh(2,c:pk_k(03d30199d74fb5a22d47b6e054e2f378cedacffcb89904a61d75d0dbd407143e65),altv:after(100))", "2103d30199d74fb5a22d47b6e054e2f378cedacffcb89904a61d75d0dbd407143e65ac6b6300670164b16951686c935287", "20d30199d74fb5a22d47b6e054e2f378cedacffcb89904a61d75d0dbd407143e65ac6b6300670164b16951686c935287", TESTMODE_VALID | TESTMODE_NEEDSIG | TESTMODE_NONMAL); Test("thresh(2,c:pk_k(03d30199d74fb5a22d47b6e054e2f378cedacffcb89904a61d75d0dbd407143e65),altv:after(100))", "2103d30199d74fb5a22d47b6e054e2f378cedacffcb89904a61d75d0dbd407143e65ac6b6300670164b16951686c935287", "20d30199d74fb5a22d47b6e054e2f378cedacffcb89904a61d75d0dbd407143e65ac6b6300670164b16951686c935287", TESTMODE_VALID | TESTMODE_NEEDSIG | TESTMODE_NONMAL);
// A threshold of 1 is valid. // A threshold of 1 is valid.
@@ -727,4 +727,25 @@ BOOST_AUTO_TEST_CASE(fixed_tests)
g_testdata.reset(); g_testdata.reset();
} }
// Confirm that ~Node(), Node::Clone() and operator=(Node&&) are stack-safe.
BOOST_AUTO_TEST_CASE(node_deep_destruct)
{
using miniscript::internal::NoDupCheck;
using miniscript::Fragment;
using NodeU32 = miniscript::Node<uint32_t>;
constexpr auto ctx{miniscript::MiniscriptContext::P2WSH};
NodeU32 root{NoDupCheck{}, ctx, Fragment::JUST_1};
for (uint32_t i{0}; i < 200'000; ++i) {
root = NodeU32{NoDupCheck{}, ctx, Fragment::WRAP_S, Vector(std::move(root))};
}
BOOST_CHECK_EQUAL(root.ScriptSize(), 200'001);
auto clone{root.Clone()};
BOOST_CHECK_EQUAL(clone.ScriptSize(), root.ScriptSize());
clone = std::move(root);
}
BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE_END()