Skip to content

Commit c2d4e40

Browse files
committed
Merge bitcoin/bitcoin#28651: Make miniscript GetWitnessSize accurate for tapscript
b228108 miniscript: make GetWitnessSize accurate for tapscript (Pieter Wuille) 8be9851 test: add tests for miniscript GetWitnessSize (Pieter Wuille) 7ed2b2d test: remove mutable global contexts in miniscript fuzzer/test (Pieter Wuille) Pull request description: So far, the same algorithm is used to compute an (upper bound on) the maximum witness size for both P2WSH and P2TR miniscript. That's unfortunate, because it means fee estimations for P2TR miniscript will miss out on the generic savings brought by P2TR witnesses (smaller signatures and public keys, specifically). Fix this by making the algorithm use script context specification calculations, and add tests for it. Also included is a cleanup for the tests to avoid mutable globals, as I found it hard to reason about what exactly was being tested. ACKs for top commit: achow101: ACK b228108 darosior: ACK b228108 Tree-SHA512: e4bda7376628f3e91cfc74917cefc554ca16eb5f2a0e1adddc33eb8717c4aaa071e56a40f85a2041ae74ec445a7bd0129bba48994c203e0e6e4d25af65954d9e
2 parents 9c30f5e + b228108 commit c2d4e40

File tree

3 files changed

+124
-101
lines changed

3 files changed

+124
-101
lines changed

src/script/miniscript.h

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,13 +1105,15 @@ struct Node {
11051105
}
11061106

11071107
internal::WitnessSize CalcWitnessSize() const {
1108+
const uint32_t sig_size = IsTapscript(m_script_ctx) ? 1 + 65 : 1 + 72;
1109+
const uint32_t pubkey_size = IsTapscript(m_script_ctx) ? 1 + 32 : 1 + 33;
11081110
switch (fragment) {
11091111
case Fragment::JUST_0: return {{}, 0};
11101112
case Fragment::JUST_1:
11111113
case Fragment::OLDER:
11121114
case Fragment::AFTER: return {0, {}};
1113-
case Fragment::PK_K: return {1 + 72, 1};
1114-
case Fragment::PK_H: return {1 + 72 + 1 + 33, 1 + 1 + 33};
1115+
case Fragment::PK_K: return {sig_size, 1};
1116+
case Fragment::PK_H: return {sig_size + pubkey_size, 1 + pubkey_size};
11151117
case Fragment::SHA256:
11161118
case Fragment::RIPEMD160:
11171119
case Fragment::HASH256:
@@ -1131,8 +1133,8 @@ struct Node {
11311133
case Fragment::OR_C: return {subs[0]->ws.sat | (subs[0]->ws.dsat + subs[1]->ws.sat), {}};
11321134
case Fragment::OR_D: return {subs[0]->ws.sat | (subs[0]->ws.dsat + subs[1]->ws.sat), subs[0]->ws.dsat + subs[1]->ws.dsat};
11331135
case Fragment::OR_I: return {(subs[0]->ws.sat + 1 + 1) | (subs[1]->ws.sat + 1), (subs[0]->ws.dsat + 1 + 1) | (subs[1]->ws.dsat + 1)};
1134-
case Fragment::MULTI: return {k * (1 + 72) + 1, k + 1};
1135-
case Fragment::MULTI_A: return {k * (1 + 65) + static_cast<uint32_t>(keys.size()) - k, static_cast<uint32_t>(keys.size())};
1136+
case Fragment::MULTI: return {k * sig_size + 1, k + 1};
1137+
case Fragment::MULTI_A: return {k * sig_size + static_cast<uint32_t>(keys.size()) - k, static_cast<uint32_t>(keys.size())};
11361138
case Fragment::WRAP_A:
11371139
case Fragment::WRAP_N:
11381140
case Fragment::WRAP_S:

src/test/fuzz/miniscript.cpp

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ struct TestData {
7070
sig.push_back(1); // SIGHASH_ALL
7171
dummy_sigs.insert({pubkey, {sig, i & 1}});
7272
assert(privkey.SignSchnorr(MESSAGE_HASH, schnorr_sig, nullptr, EMPTY_AUX));
73+
schnorr_sig.push_back(1); // Maximally-sized signature has sighash byte
7374
schnorr_sigs.emplace(XOnlyPubKey{pubkey}, std::make_pair(std::move(schnorr_sig), i & 1));
7475

7576
std::vector<unsigned char> hash;
@@ -113,7 +114,9 @@ struct TestData {
113114
struct ParserContext {
114115
typedef CPubKey Key;
115116

116-
MsCtx script_ctx{MsCtx::P2WSH};
117+
const MsCtx script_ctx;
118+
119+
constexpr ParserContext(MsCtx ctx) noexcept : script_ctx(ctx) {}
117120

118121
bool KeyCompare(const Key& a, const Key& b) const {
119122
return a < b;
@@ -178,11 +181,13 @@ struct ParserContext {
178181
MsCtx MsContext() const {
179182
return script_ctx;
180183
}
181-
} PARSER_CTX;
184+
};
182185

183186
//! Context that implements naive conversion from/to script only, for roundtrip testing.
184187
struct ScriptParserContext {
185-
MsCtx script_ctx{MsCtx::P2WSH};
188+
const MsCtx script_ctx;
189+
190+
constexpr ScriptParserContext(MsCtx ctx) noexcept : script_ctx(ctx) {}
186191

187192
//! For Script roundtrip we never need the key from a key hash.
188193
struct Key {
@@ -228,10 +233,13 @@ struct ScriptParserContext {
228233
MsCtx MsContext() const {
229234
return script_ctx;
230235
}
231-
} SCRIPT_PARSER_CONTEXT;
236+
};
232237

233238
//! Context to produce a satisfaction for a Miniscript node using the pre-computed data.
234-
struct SatisfierContext: ParserContext {
239+
struct SatisfierContext : ParserContext {
240+
241+
constexpr SatisfierContext(MsCtx ctx) noexcept : ParserContext(ctx) {}
242+
235243
// Timelock challenges satisfaction. Make the value (deterministically) vary to explore different
236244
// paths.
237245
bool CheckAfter(uint32_t value) const { return value % 2; }
@@ -267,12 +275,10 @@ struct SatisfierContext: ParserContext {
267275
miniscript::Availability SatHASH160(const std::vector<unsigned char>& hash, std::vector<unsigned char>& preimage) const {
268276
return LookupHash(hash, preimage, TEST_DATA.hash160_preimages);
269277
}
270-
} SATISFIER_CTX;
278+
};
271279

272280
//! Context to check a satisfaction against the pre-computed data.
273-
struct CheckerContext: BaseSignatureChecker {
274-
TestData *test_data;
275-
281+
const struct CheckerContext: BaseSignatureChecker {
276282
// Signature checker methods. Checks the right dummy signature is used.
277283
bool CheckECDSASignature(const std::vector<unsigned char>& sig, const std::vector<unsigned char>& vchPubKey,
278284
const CScript& scriptCode, SigVersion sigversion) const override
@@ -294,7 +300,7 @@ struct CheckerContext: BaseSignatureChecker {
294300
} CHECKER_CTX;
295301

296302
//! Context to check for duplicates when instancing a Node.
297-
struct KeyComparator {
303+
const struct KeyComparator {
298304
bool KeyCompare(const CPubKey& a, const CPubKey& b) const {
299305
return a < b;
300306
}
@@ -1027,15 +1033,15 @@ void TestNode(const MsCtx script_ctx, const NodeRef& node, FuzzedDataProvider& p
10271033
if (!node) return;
10281034

10291035
// Check that it roundtrips to text representation
1030-
PARSER_CTX.script_ctx = script_ctx;
1031-
std::optional<std::string> str{node->ToString(PARSER_CTX)};
1036+
const ParserContext parser_ctx{script_ctx};
1037+
std::optional<std::string> str{node->ToString(parser_ctx)};
10321038
assert(str);
1033-
auto parsed = miniscript::FromString(*str, PARSER_CTX);
1039+
auto parsed = miniscript::FromString(*str, parser_ctx);
10341040
assert(parsed);
10351041
assert(*parsed == *node);
10361042

10371043
// Check consistency between script size estimation and real size.
1038-
auto script = node->ToScript(PARSER_CTX);
1044+
auto script = node->ToScript(parser_ctx);
10391045
assert(node->ScriptSize() == script.size());
10401046

10411047
// Check consistency of "x" property with the script (type K is excluded, because it can end
@@ -1049,12 +1055,12 @@ void TestNode(const MsCtx script_ctx, const NodeRef& node, FuzzedDataProvider& p
10491055
if (!node->IsValidTopLevel()) return;
10501056

10511057
// Check roundtrip to script
1052-
auto decoded = miniscript::FromScript(script, PARSER_CTX);
1058+
auto decoded = miniscript::FromScript(script, parser_ctx);
10531059
assert(decoded);
10541060
// Note we can't use *decoded == *node because the miniscript representation may differ, so we check that:
10551061
// - The script corresponding to that decoded form matches exactly
10561062
// - The type matches exactly
1057-
assert(decoded->ToScript(PARSER_CTX) == script);
1063+
assert(decoded->ToScript(parser_ctx) == script);
10581064
assert(decoded->GetType() == node->GetType());
10591065

10601066
// Optionally pad the script or the witness in order to increase the sensitivity of the tests of
@@ -1091,19 +1097,19 @@ void TestNode(const MsCtx script_ctx, const NodeRef& node, FuzzedDataProvider& p
10911097
}
10921098
}
10931099

1094-
SATISFIER_CTX.script_ctx = script_ctx;
1100+
const SatisfierContext satisfier_ctx{script_ctx};
10951101

10961102
// Get the ScriptPubKey for this script, filling spend data if it's Taproot.
10971103
TaprootBuilder builder;
10981104
const CScript script_pubkey{ScriptPubKey(script_ctx, script, builder)};
10991105

11001106
// Run malleable satisfaction algorithm.
11011107
std::vector<std::vector<unsigned char>> stack_mal;
1102-
const bool mal_success = node->Satisfy(SATISFIER_CTX, stack_mal, false) == miniscript::Availability::YES;
1108+
const bool mal_success = node->Satisfy(satisfier_ctx, stack_mal, false) == miniscript::Availability::YES;
11031109

11041110
// Run non-malleable satisfaction algorithm.
11051111
std::vector<std::vector<unsigned char>> stack_nonmal;
1106-
const bool nonmal_success = node->Satisfy(SATISFIER_CTX, stack_nonmal, true) == miniscript::Availability::YES;
1112+
const bool nonmal_success = node->Satisfy(satisfier_ctx, stack_nonmal, true) == miniscript::Availability::YES;
11071113

11081114
if (nonmal_success) {
11091115
// Non-malleable satisfactions are bounded by the satisfaction size plus:
@@ -1114,6 +1120,9 @@ void TestNode(const MsCtx script_ctx, const NodeRef& node, FuzzedDataProvider& p
11141120
// If a non-malleable satisfaction exists, the malleable one must also exist, and be identical to it.
11151121
assert(mal_success);
11161122
assert(stack_nonmal == stack_mal);
1123+
// Compute witness size (excluding script push, control block, and witness count encoding).
1124+
const size_t wit_size = GetSerializeSize(stack_nonmal, PROTOCOL_VERSION) - GetSizeOfCompactSize(stack_nonmal.size());
1125+
assert(wit_size <= *node->GetWitnessSize());
11171126

11181127
// Test non-malleable satisfaction.
11191128
witness_nonmal.stack.insert(witness_nonmal.stack.end(), std::make_move_iterator(stack_nonmal.begin()), std::make_move_iterator(stack_nonmal.end()));
@@ -1229,13 +1238,13 @@ FUZZ_TARGET(miniscript_string, .init = FuzzInit)
12291238
if (buffer.empty()) return;
12301239
FuzzedDataProvider provider(buffer.data(), buffer.size());
12311240
auto str = provider.ConsumeBytesAsString(provider.remaining_bytes() - 1);
1232-
PARSER_CTX.script_ctx = (MsCtx)provider.ConsumeBool();
1233-
auto parsed = miniscript::FromString(str, PARSER_CTX);
1241+
const ParserContext parser_ctx{(MsCtx)provider.ConsumeBool()};
1242+
auto parsed = miniscript::FromString(str, parser_ctx);
12341243
if (!parsed) return;
12351244

1236-
const auto str2 = parsed->ToString(PARSER_CTX);
1245+
const auto str2 = parsed->ToString(parser_ctx);
12371246
assert(str2);
1238-
auto parsed2 = miniscript::FromString(*str2, PARSER_CTX);
1247+
auto parsed2 = miniscript::FromString(*str2, parser_ctx);
12391248
assert(parsed2);
12401249
assert(*parsed == *parsed2);
12411250
}
@@ -1247,9 +1256,9 @@ FUZZ_TARGET(miniscript_script)
12471256
const std::optional<CScript> script = ConsumeDeserializable<CScript>(fuzzed_data_provider);
12481257
if (!script) return;
12491258

1250-
SCRIPT_PARSER_CONTEXT.script_ctx = (MsCtx)fuzzed_data_provider.ConsumeBool();
1251-
const auto ms = miniscript::FromScript(*script, SCRIPT_PARSER_CONTEXT);
1259+
const ScriptParserContext script_parser_ctx{(MsCtx)fuzzed_data_provider.ConsumeBool()};
1260+
const auto ms = miniscript::FromScript(*script, script_parser_ctx);
12521261
if (!ms) return;
12531262

1254-
assert(ms->ToScript(SCRIPT_PARSER_CONTEXT) == *script);
1263+
assert(ms->ToScript(script_parser_ctx) == *script);
12551264
}

0 commit comments

Comments
 (0)