Skip to content

Commit bc88f3a

Browse files
committed
Merge bitcoin/bitcoin#27997: Descriptors: rule out unspendable miniscript descriptors
c7db88a descriptor: assert we never parse a sane miniscript with no pubkey (Antoine Poinsot) a49402a qa: make sure we don't let unspendable Miniscript descriptors be imported (Antoine Poinsot) 639e3b6 descriptor: refuse to parse unspendable miniscript descriptors (Antoine Poinsot) e3280ea miniscript: make GetStackSize() and GetOps() return optionals (Antoine Poinsot) Pull request description: `IsSane()` in Miniscript does not ensure a Script is actually spendable. This is an issue as we would accept any sane Miniscript when parsing a descriptor. Fix this by explicitly checking a Miniscript descriptor is both sane and spendable when parsing it. This bug was exposed due to a check added in #22838 (bitcoin/bitcoin#22838 (comment)) that triggered a fuzz crash (bitcoin/bitcoin#22838 (comment)). ACKs for top commit: sipa: utACK c7db88a achow101: ACK c7db88a Tree-SHA512: e79bc9f7842e98a4e8f358f05811fca51b15b4b80a171c0d2b17cf4bb1f578a18e4397bc2ece9817d392e0de0196ee6a054b7318441fd3566dd22e1f03eb64a5
2 parents 306157a + c7db88a commit bc88f3a

File tree

5 files changed

+48
-14
lines changed

5 files changed

+48
-14
lines changed

src/script/descriptor.cpp

+8-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include <common/args.h>
1616
#include <span.h>
1717
#include <util/bip32.h>
18+
#include <util/check.h>
1819
#include <util/spanparsing.h>
1920
#include <util/strencodings.h>
2021
#include <util/vector.h>
@@ -1553,14 +1554,14 @@ std::unique_ptr<DescriptorImpl> ParseScript(uint32_t& key_exp_index, Span<const
15531554
error = std::move(parser.m_key_parsing_error);
15541555
return nullptr;
15551556
}
1556-
if (!node->IsSane()) {
1557+
if (!node->IsSane() || node->IsNotSatisfiable()) {
15571558
// Try to find the first insane sub for better error reporting.
15581559
auto insane_node = node.get();
15591560
if (const auto sub = node->FindInsaneSub()) insane_node = sub;
15601561
if (const auto str = insane_node->ToString(parser)) error = *str;
15611562
if (!insane_node->IsValid()) {
15621563
error += " is invalid";
1563-
} else {
1564+
} else if (!node->IsSane()) {
15641565
error += " is not sane";
15651566
if (!insane_node->IsNonMalleable()) {
15661567
error += ": malleable witnesses exist";
@@ -1573,9 +1574,14 @@ std::unique_ptr<DescriptorImpl> ParseScript(uint32_t& key_exp_index, Span<const
15731574
} else if (!insane_node->ValidSatisfactions()) {
15741575
error += ": needs witnesses that may exceed resource limits";
15751576
}
1577+
} else {
1578+
error += " is not satisfiable";
15761579
}
15771580
return nullptr;
15781581
}
1582+
// A signature check is required for a miniscript to be sane. Therefore no sane miniscript
1583+
// may have an empty list of public keys.
1584+
CHECK_NONFATAL(!parser.m_keys.empty());
15791585
return std::make_unique<MiniscriptDescriptor>(std::move(parser.m_keys), std::move(node));
15801586
}
15811587
}

src/script/miniscript.h

+19-4
Original file line numberDiff line numberDiff line change
@@ -1134,20 +1134,35 @@ struct Node {
11341134
size_t ScriptSize() const { return scriptlen; }
11351135

11361136
//! Return the maximum number of ops needed to satisfy this script non-malleably.
1137-
uint32_t GetOps() const { return ops.count + ops.sat.value; }
1137+
std::optional<uint32_t> GetOps() const {
1138+
if (!ops.sat.valid) return {};
1139+
return ops.count + ops.sat.value;
1140+
}
11381141

11391142
//! Return the number of ops in the script (not counting the dynamic ones that depend on execution).
11401143
uint32_t GetStaticOps() const { return ops.count; }
11411144

11421145
//! Check the ops limit of this script against the consensus limit.
1143-
bool CheckOpsLimit() const { return GetOps() <= MAX_OPS_PER_SCRIPT; }
1146+
bool CheckOpsLimit() const {
1147+
if (const auto ops = GetOps()) return *ops <= MAX_OPS_PER_SCRIPT;
1148+
return true;
1149+
}
11441150

11451151
/** Return the maximum number of stack elements needed to satisfy this script non-malleably, including
11461152
* the script push. */
1147-
uint32_t GetStackSize() const { return ss.sat.value + 1; }
1153+
std::optional<uint32_t> GetStackSize() const {
1154+
if (!ss.sat.valid) return {};
1155+
return ss.sat.value + 1;
1156+
}
11481157

11491158
//! Check the maximum stack size for this script against the policy limit.
1150-
bool CheckStackSize() const { return GetStackSize() - 1 <= MAX_STANDARD_P2WSH_STACK_ITEMS; }
1159+
bool CheckStackSize() const {
1160+
if (const auto ss = GetStackSize()) return *ss - 1 <= MAX_STANDARD_P2WSH_STACK_ITEMS;
1161+
return true;
1162+
}
1163+
1164+
//! Whether no satisfaction exists for this node.
1165+
bool IsNotSatisfiable() const { return !GetStackSize(); }
11511166

11521167
//! Return the expression type.
11531168
Type GetType() const { return typ; }

src/test/fuzz/miniscript.cpp

+4-3
Original file line numberDiff line numberDiff line change
@@ -943,7 +943,8 @@ void TestNode(const NodeRef& node, FuzzedDataProvider& provider)
943943
assert(decoded->ToScript(PARSER_CTX) == script);
944944
assert(decoded->GetType() == node->GetType());
945945

946-
if (provider.ConsumeBool() && node->GetOps() < MAX_OPS_PER_SCRIPT && node->ScriptSize() < MAX_STANDARD_P2WSH_SCRIPT_SIZE) {
946+
const auto node_ops{node->GetOps()};
947+
if (provider.ConsumeBool() && node_ops && *node_ops < MAX_OPS_PER_SCRIPT && node->ScriptSize() < MAX_STANDARD_P2WSH_SCRIPT_SIZE) {
947948
// Optionally pad the script with OP_NOPs to max op the ops limit of the constructed script.
948949
// This makes the script obviously not actually miniscript-compatible anymore, but the
949950
// signatures constructed in this test don't commit to the script anyway, so the same
@@ -954,7 +955,7 @@ void TestNode(const NodeRef& node, FuzzedDataProvider& provider)
954955
// Do not pad more than what would cause MAX_STANDARD_P2WSH_SCRIPT_SIZE to be reached, however,
955956
// as that also invalidates scripts.
956957
int add = std::min<int>(
957-
MAX_OPS_PER_SCRIPT - node->GetOps(),
958+
MAX_OPS_PER_SCRIPT - *node_ops,
958959
MAX_STANDARD_P2WSH_SCRIPT_SIZE - node->ScriptSize());
959960
for (int i = 0; i < add; ++i) script.push_back(OP_NOP);
960961
}
@@ -972,7 +973,7 @@ void TestNode(const NodeRef& node, FuzzedDataProvider& provider)
972973

973974
if (nonmal_success) {
974975
// Non-malleable satisfactions are bounded by GetStackSize().
975-
assert(witness_nonmal.stack.size() <= node->GetStackSize());
976+
assert(witness_nonmal.stack.size() <= *node->GetStackSize());
976977
// If a non-malleable satisfaction exists, the malleable one must also exist, and be identical to it.
977978
assert(mal_success);
978979
assert(witness_nonmal.stack == witness_mal.stack);

src/test/miniscript_tests.cpp

+5-5
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ void TestSatisfy(const std::string& testcase, const NodeRef& node) {
297297

298298
if (nonmal_success) {
299299
// Non-malleable satisfactions are bounded by GetStackSize().
300-
BOOST_CHECK(witness_nonmal.stack.size() <= node->GetStackSize());
300+
BOOST_CHECK(witness_nonmal.stack.size() <= *node->GetStackSize());
301301
// If a non-malleable satisfaction exists, the malleable one must also exist, and be identical to it.
302302
BOOST_CHECK(mal_success);
303303
BOOST_CHECK(witness_nonmal.stack == witness_mal.stack);
@@ -375,8 +375,8 @@ void Test(const std::string& ms, const std::string& hexscript, int mode, int ops
375375
auto inferred_miniscript = miniscript::FromScript(computed_script, CONVERTER);
376376
BOOST_CHECK_MESSAGE(inferred_miniscript, "Cannot infer miniscript from script: " + ms);
377377
BOOST_CHECK_MESSAGE(inferred_miniscript->ToScript(CONVERTER) == computed_script, "Roundtrip failure: miniscript->script != miniscript->script->miniscript->script: " + ms);
378-
if (opslimit != -1) BOOST_CHECK_MESSAGE((int)node->GetOps() == opslimit, "Ops limit mismatch: " << ms << " (" << node->GetOps() << " vs " << opslimit << ")");
379-
if (stacklimit != -1) BOOST_CHECK_MESSAGE((int)node->GetStackSize() == stacklimit, "Stack limit mismatch: " << ms << " (" << node->GetStackSize() << " vs " << stacklimit << ")");
378+
if (opslimit != -1) BOOST_CHECK_MESSAGE((int)*node->GetOps() == opslimit, "Ops limit mismatch: " << ms << " (" << *node->GetOps() << " vs " << opslimit << ")");
379+
if (stacklimit != -1) BOOST_CHECK_MESSAGE((int)*node->GetStackSize() == stacklimit, "Stack limit mismatch: " << ms << " (" << *node->GetStackSize() << " vs " << stacklimit << ")");
380380
TestSatisfy(ms, node);
381381
}
382382
}
@@ -498,8 +498,8 @@ BOOST_AUTO_TEST_CASE(fixed_tests)
498498
// For CHECKMULTISIG the OP cost is the number of keys, but the stack size is the number of sigs (+1)
499499
const auto ms_multi = miniscript::FromString("multi(1,03d30199d74fb5a22d47b6e054e2f378cedacffcb89904a61d75d0dbd407143e65,03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556,0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)", CONVERTER);
500500
BOOST_CHECK(ms_multi);
501-
BOOST_CHECK_EQUAL(ms_multi->GetOps(), 4); // 3 pubkeys + CMS
502-
BOOST_CHECK_EQUAL(ms_multi->GetStackSize(), 3); // 1 sig + dummy elem + script push
501+
BOOST_CHECK_EQUAL(*ms_multi->GetOps(), 4); // 3 pubkeys + CMS
502+
BOOST_CHECK_EQUAL(*ms_multi->GetStackSize(), 3); // 1 sig + dummy elem + script push
503503
// The 'd:' wrapper leaves on the stack what was DUP'ed at the beginning of its execution.
504504
// Since it contains an OP_IF just after on the same element, we can make sure that the element
505505
// in question must be OP_1 if OP_IF enforces that its argument must only be OP_1 or the empty

test/functional/wallet_miniscript.py

+12
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,18 @@ def run_test(self):
277277
assert not res["success"]
278278
assert "is not sane: witnesses without signature exist" in res["error"]["message"]
279279

280+
# Sanity check we wouldn't let an unspendable Miniscript descriptor in
281+
res = self.ms_wo_wallet.importdescriptors(
282+
[
283+
{
284+
"desc": descsum_create("wsh(0)"),
285+
"active": False,
286+
"timestamp": "now",
287+
}
288+
]
289+
)[0]
290+
assert not res["success"] and "is not satisfiable" in res["error"]["message"]
291+
280292
# Test we can track any type of Miniscript
281293
for ms in MINISCRIPTS:
282294
self.watchonly_test(ms)

0 commit comments

Comments
 (0)