Skip to content

Commit 687a0b0

Browse files
committed
miniscript: introduce a multi_a fragment
It is the equivalent of multi() but for Tapscript, using CHECKSIGADD instead of CHECKMULTISIG. It shares the same properties as multi() but for 'n', since a threshold multi_a() may have an empty vector as the top element of its satisfaction. It could also have the 'o' property when it only has a single key, but in this case a 'pk()' is always preferable anyways.
1 parent 9164c2e commit 687a0b0

File tree

3 files changed

+147
-29
lines changed

3 files changed

+147
-29
lines changed

src/script/miniscript.cpp

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ Type ComputeType(Fragment fragment, Type x, Type y, Type z, const std::vector<Ty
4545
// Sanity check on k
4646
if (fragment == Fragment::OLDER || fragment == Fragment::AFTER) {
4747
assert(k >= 1 && k < 0x80000000UL);
48-
} else if (fragment == Fragment::MULTI) {
48+
} else if (fragment == Fragment::MULTI || fragment == Fragment::MULTI_A) {
4949
assert(k >= 1 && k <= n_keys);
5050
} else if (fragment == Fragment::THRESH) {
5151
assert(k >= 1 && k <= n_subs);
@@ -69,7 +69,9 @@ Type ComputeType(Fragment fragment, Type x, Type y, Type z, const std::vector<Ty
6969
if (fragment == Fragment::PK_K || fragment == Fragment::PK_H) {
7070
assert(n_keys == 1);
7171
} else if (fragment == Fragment::MULTI) {
72-
assert(n_keys >= 1 && n_keys <= 20);
72+
assert(n_keys >= 1 && n_keys <= MAX_PUBKEYS_PER_MULTISIG);
73+
} else if (fragment == Fragment::MULTI_A) {
74+
assert(n_keys >= 1 && n_keys <= MAX_PUBKEYS_PER_MULTI_A);
7375
} else {
7476
assert(n_keys == 0);
7577
}
@@ -212,6 +214,7 @@ Type ComputeType(Fragment fragment, Type x, Type y, Type z, const std::vector<Ty
212214
((x << "i"_mst) && (y << "j"_mst)) ||
213215
((x << "j"_mst) && (y << "i"_mst)))); // k=k_x*k_y*k_z* !(g_x*h_y + h_x*g_y + i_x*j_y + j_x*i_y)
214216
case Fragment::MULTI: return "Bnudemsk"_mst;
217+
case Fragment::MULTI_A: return "Budemsk"_mst;
215218
case Fragment::THRESH: {
216219
bool all_e = true;
217220
bool all_m = true;
@@ -260,6 +263,7 @@ size_t ComputeScriptLen(Fragment fragment, Type sub0typ, size_t subsize, uint32_
260263
case Fragment::HASH160:
261264
case Fragment::RIPEMD160: return 4 + 2 + 21;
262265
case Fragment::MULTI: return 1 + BuildScript(n_keys).size() + BuildScript(k).size() + 34 * n_keys;
266+
case Fragment::MULTI_A: return (1 + 32 + 1) * n_keys + BuildScript(k).size() + 1;
263267
case Fragment::AND_V: return subsize;
264268
case Fragment::WRAP_V: return subsize + (sub0typ << "x"_mst);
265269
case Fragment::WRAP_S:
@@ -373,9 +377,13 @@ std::optional<std::vector<Opcode>> DecomposeScript(const CScript& script)
373377
// Decompose OP_EQUALVERIFY into OP_EQUAL OP_VERIFY
374378
out.emplace_back(OP_EQUAL, std::vector<unsigned char>());
375379
opcode = OP_VERIFY;
380+
} else if (opcode == OP_NUMEQUALVERIFY) {
381+
// Decompose OP_NUMEQUALVERIFY into OP_NUMEQUAL OP_VERIFY
382+
out.emplace_back(OP_NUMEQUAL, std::vector<unsigned char>());
383+
opcode = OP_VERIFY;
376384
} else if (IsPushdataOp(opcode)) {
377385
if (!CheckMinimalPush(push_data, opcode)) return {};
378-
} else if (it != itend && (opcode == OP_CHECKSIG || opcode == OP_CHECKMULTISIG || opcode == OP_EQUAL) && (*it == OP_VERIFY)) {
386+
} else if (it != itend && (opcode == OP_CHECKSIG || opcode == OP_CHECKMULTISIG || opcode == OP_EQUAL || opcode == OP_NUMEQUAL) && (*it == OP_VERIFY)) {
379387
// Rule out non minimal VERIFY sequences
380388
return {};
381389
}

src/script/miniscript.h

Lines changed: 124 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ namespace miniscript {
4545
* - When satisfied, pushes nothing.
4646
* - Cannot be dissatisfied.
4747
* - This can be obtained by adding an OP_VERIFY to a B, modifying the last opcode
48-
* of a B to its -VERIFY version (only for OP_CHECKSIG, OP_CHECKSIGVERIFY
49-
* and OP_EQUAL), or by combining a V fragment under some conditions.
48+
* of a B to its -VERIFY version (only for OP_CHECKSIG, OP_CHECKSIGVERIFY,
49+
* OP_NUMEQUAL and OP_EQUAL), or by combining a V fragment under some conditions.
5050
* - For example vc:pk_k(key) = <key> OP_CHECKSIGVERIFY
5151
* - "K" Key:
5252
* - Takes its inputs from the top of the stack.
@@ -217,7 +217,8 @@ enum class Fragment {
217217
OR_I, //!< OP_IF [X] OP_ELSE [Y] OP_ENDIF
218218
ANDOR, //!< [X] OP_NOTIF [Z] OP_ELSE [Y] OP_ENDIF
219219
THRESH, //!< [X1] ([Xn] OP_ADD)* [k] OP_EQUAL
220-
MULTI, //!< [k] [key_n]* [n] OP_CHECKMULTISIG
220+
MULTI, //!< [k] [key_n]* [n] OP_CHECKMULTISIG (only available within P2WSH context)
221+
MULTI_A, //!< [key_0] OP_CHECKSIG ([key_n] OP_CHECKSIGADD)* [k] OP_NUMEQUAL (only within Tapscript ctx)
221222
// AND_N(X,Y) is represented as ANDOR(X,Y,0)
222223
// WRAP_T(X) is represented as AND_V(X,1)
223224
// WRAP_L(X) is represented as OR_I(0,X)
@@ -637,6 +638,14 @@ struct Node {
637638
}
638639
return BuildScript(std::move(script), node.keys.size(), verify ? OP_CHECKMULTISIGVERIFY : OP_CHECKMULTISIG);
639640
}
641+
case Fragment::MULTI_A: {
642+
CHECK_NONFATAL(is_tapscript);
643+
CScript script = BuildScript(ctx.ToPKBytes(*node.keys.begin()), OP_CHECKSIG);
644+
for (auto it = node.keys.begin() + 1; it != node.keys.end(); ++it) {
645+
script = BuildScript(std::move(script), ctx.ToPKBytes(*it), OP_CHECKSIGADD);
646+
}
647+
return BuildScript(std::move(script), node.k, verify ? OP_NUMEQUALVERIFY : OP_NUMEQUAL);
648+
}
640649
case Fragment::THRESH: {
641650
CScript script = std::move(subs[0]);
642651
for (size_t i = 1; i < subs.size(); ++i) {
@@ -740,6 +749,16 @@ struct Node {
740749
}
741750
return std::move(str) + ")";
742751
}
752+
case Fragment::MULTI_A: {
753+
CHECK_NONFATAL(is_tapscript);
754+
auto str = std::move(ret) + "multi_a(" + ::ToString(node.k);
755+
for (const auto& key : node.keys) {
756+
auto key_str = ctx.ToString(key);
757+
if (!key_str) return {};
758+
str += "," + std::move(*key_str);
759+
}
760+
return std::move(str) + ")";
761+
}
743762
case Fragment::THRESH: {
744763
auto str = std::move(ret) + "thresh(" + ::ToString(node.k);
745764
for (auto& sub : subs) {
@@ -805,6 +824,7 @@ struct Node {
805824
return {count, sat, dsat};
806825
}
807826
case Fragment::MULTI: return {1, (uint32_t)keys.size(), (uint32_t)keys.size()};
827+
case Fragment::MULTI_A: return {(uint32_t)keys.size() + 1, 0, 0};
808828
case Fragment::WRAP_S:
809829
case Fragment::WRAP_C:
810830
case Fragment::WRAP_N: return {1 + subs[0]->ops.count, subs[0]->ops.sat, subs[0]->ops.dsat};
@@ -857,6 +877,7 @@ struct Node {
857877
case Fragment::OR_D: return {subs[0]->ss.sat | (subs[0]->ss.dsat + subs[1]->ss.sat), subs[0]->ss.dsat + subs[1]->ss.dsat};
858878
case Fragment::OR_I: return {(subs[0]->ss.sat + 1) | (subs[1]->ss.sat + 1), (subs[0]->ss.dsat + 1) | (subs[1]->ss.dsat + 1)};
859879
case Fragment::MULTI: return {k + 1, k + 1};
880+
case Fragment::MULTI_A: return {keys.size(), keys.size()};
860881
case Fragment::WRAP_A:
861882
case Fragment::WRAP_N:
862883
case Fragment::WRAP_S: return subs[0]->ss;
@@ -907,6 +928,7 @@ struct Node {
907928
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};
908929
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)};
909930
case Fragment::MULTI: return {k * (1 + 72) + 1, k + 1};
931+
case Fragment::MULTI_A: return {k * (1 + 65) + static_cast<uint32_t>(keys.size()) - k, static_cast<uint32_t>(keys.size())};
910932
case Fragment::WRAP_A:
911933
case Fragment::WRAP_N:
912934
case Fragment::WRAP_S:
@@ -947,6 +969,34 @@ struct Node {
947969
Availability avail = ctx.Sign(node.keys[0], sig);
948970
return {ZERO + InputStack(key), (InputStack(std::move(sig)).SetWithSig() + InputStack(key)).SetAvailable(avail)};
949971
}
972+
case Fragment::MULTI_A: {
973+
// sats[j] represents the best stack containing j valid signatures (out of the first i keys).
974+
// In the loop below, these stacks are built up using a dynamic programming approach.
975+
std::vector<InputStack> sats = Vector(EMPTY);
976+
for (size_t i = 0; i < node.keys.size(); ++i) {
977+
// Get the signature for the i'th key in reverse order (the signature for the first key needs to
978+
// be at the top of the stack, contrary to CHECKMULTISIG's satisfaction).
979+
std::vector<unsigned char> sig;
980+
Availability avail = ctx.Sign(node.keys[node.keys.size() - 1 - i], sig);
981+
// Compute signature stack for just this key.
982+
auto sat = InputStack(std::move(sig)).SetWithSig().SetAvailable(avail);
983+
// Compute the next sats vector: next_sats[0] is a copy of sats[0] (no signatures). All further
984+
// next_sats[j] are equal to either the existing sats[j] + ZERO, or sats[j-1] plus a signature
985+
// for the current (i'th) key. The very last element needs all signatures filled.
986+
std::vector<InputStack> next_sats;
987+
next_sats.push_back(sats[0] + ZERO);
988+
for (size_t j = 1; j < sats.size(); ++j) next_sats.push_back((sats[j] + ZERO) | (std::move(sats[j - 1]) + sat));
989+
next_sats.push_back(std::move(sats[sats.size() - 1]) + std::move(sat));
990+
// Switch over.
991+
sats = std::move(next_sats);
992+
}
993+
// The dissatisfaction consists of as many empty vectors as there are keys, which is the same as
994+
// satisfying 0 keys.
995+
auto& nsat{sats[0]};
996+
assert(node.k != 0);
997+
assert(node.k <= sats.size());
998+
return {std::move(nsat), std::move(sats[node.k])};
999+
}
9501000
case Fragment::MULTI: {
9511001
// sats[j] represents the best stack containing j valid signatures (out of the first i keys).
9521002
// In the loop below, these stacks are built up using a dynamic programming approach.
@@ -1281,6 +1331,7 @@ struct Node {
12811331
case Fragment::PK_K:
12821332
case Fragment::PK_H:
12831333
case Fragment::MULTI:
1334+
case Fragment::MULTI_A:
12841335
case Fragment::AFTER:
12851336
case Fragment::OLDER:
12861337
case Fragment::HASH256:
@@ -1502,6 +1553,41 @@ inline NodeRef<Key> Parse(Span<const char> in, const Ctx& ctx)
15021553

15031554
to_parse.emplace_back(ParseContext::WRAPPED_EXPR, -1, -1);
15041555

1556+
// Parses a multi() or multi_a() from its string representation. Returns false on parsing error.
1557+
const auto parse_multi_exp = [&](Span<const char>& in, const bool is_multi_a) -> bool {
1558+
const auto max_keys{is_multi_a ? MAX_PUBKEYS_PER_MULTI_A : MAX_PUBKEYS_PER_MULTISIG};
1559+
const auto required_ctx{is_multi_a ? MiniscriptContext::TAPSCRIPT : MiniscriptContext::P2WSH};
1560+
if (ctx.MsContext() != required_ctx) return false;
1561+
// Get threshold
1562+
int next_comma = FindNextChar(in, ',');
1563+
if (next_comma < 1) return false;
1564+
int64_t k;
1565+
if (!ParseInt64(std::string(in.begin(), in.begin() + next_comma), &k)) return false;
1566+
in = in.subspan(next_comma + 1);
1567+
// Get keys. It is compatible for both compressed and x-only keys.
1568+
std::vector<Key> keys;
1569+
while (next_comma != -1) {
1570+
next_comma = FindNextChar(in, ',');
1571+
int key_length = (next_comma == -1) ? FindNextChar(in, ')') : next_comma;
1572+
if (key_length < 1) return false;
1573+
auto key = ctx.FromString(in.begin(), in.begin() + key_length);
1574+
if (!key) return false;
1575+
keys.push_back(std::move(*key));
1576+
in = in.subspan(key_length + 1);
1577+
}
1578+
if (keys.size() < 1 || keys.size() > max_keys) return false;
1579+
if (k < 1 || k > (int64_t)keys.size()) return false;
1580+
if (is_multi_a) {
1581+
// (push + xonly-key + CHECKSIG[ADD]) * n + k + OP_NUMEQUAL(VERIFY), minus one.
1582+
script_size += (1 + 32 + 1) * keys.size() + BuildScript(k).size();
1583+
constructed.push_back(MakeNodeRef<Key>(internal::NoDupCheck{}, ctx.MsContext(), Fragment::MULTI_A, std::move(keys), k));
1584+
} else {
1585+
script_size += 2 + (keys.size() > 16) + (k > 16) + 34 * keys.size();
1586+
constructed.push_back(MakeNodeRef<Key>(internal::NoDupCheck{}, ctx.MsContext(), Fragment::MULTI, std::move(keys), k));
1587+
}
1588+
return true;
1589+
};
1590+
15051591
while (!to_parse.empty()) {
15061592
if (script_size > MAX_STANDARD_P2WSH_SCRIPT_SIZE) return {};
15071593

@@ -1646,27 +1732,9 @@ inline NodeRef<Key> Parse(Span<const char> in, const Ctx& ctx)
16461732
in = in.subspan(arg_size + 1);
16471733
script_size += 1 + (num > 16) + (num > 0x7f) + (num > 0x7fff) + (num > 0x7fffff);
16481734
} else if (Const("multi(", in)) {
1649-
if (IsTapscript(ctx.MsContext())) return {};
1650-
// Get threshold
1651-
int next_comma = FindNextChar(in, ',');
1652-
if (next_comma < 1) return {};
1653-
if (!ParseInt64(std::string(in.begin(), in.begin() + next_comma), &k)) return {};
1654-
in = in.subspan(next_comma + 1);
1655-
// Get keys
1656-
std::vector<Key> keys;
1657-
while (next_comma != -1) {
1658-
next_comma = FindNextChar(in, ',');
1659-
int key_length = (next_comma == -1) ? FindNextChar(in, ')') : next_comma;
1660-
if (key_length < 1) return {};
1661-
auto key = ctx.FromString(in.begin(), in.begin() + key_length);
1662-
if (!key) return {};
1663-
keys.push_back(std::move(*key));
1664-
in = in.subspan(key_length + 1);
1665-
}
1666-
if (keys.size() < 1 || keys.size() > 20) return {};
1667-
if (k < 1 || k > (int64_t)keys.size()) return {};
1668-
script_size += 2 + (keys.size() > 16) + (k > 16) + 34 * keys.size();
1669-
constructed.push_back(MakeNodeRef<Key>(internal::NoDupCheck{}, ctx.MsContext(), Fragment::MULTI, std::move(keys), k));
1735+
if (!parse_multi_exp(in, /* is_multi_a = */false)) return {};
1736+
} else if (Const("multi_a(", in)) {
1737+
if (!parse_multi_exp(in, /* is_multi_a = */true)) return {};
16701738
} else if (Const("thresh(", in)) {
16711739
int next_comma = FindNextChar(in, ',');
16721740
if (next_comma < 1) return {};
@@ -1843,8 +1911,8 @@ inline NodeRef<Key> Parse(Span<const char> in, const Ctx& ctx)
18431911
* Construct a vector with one element per opcode in the script, in reverse order.
18441912
* Each element is a pair consisting of the opcode, as well as the data pushed by
18451913
* the opcode (including OP_n), if any. OP_CHECKSIGVERIFY, OP_CHECKMULTISIGVERIFY,
1846-
* and OP_EQUALVERIFY are decomposed into OP_CHECKSIG, OP_CHECKMULTISIG, OP_EQUAL
1847-
* respectively, plus OP_VERIFY.
1914+
* OP_NUMEQUALVERIFY and OP_EQUALVERIFY are decomposed into OP_CHECKSIG, OP_CHECKMULTISIG,
1915+
* OP_EQUAL and OP_NUMEQUAL respectively, plus OP_VERIFY.
18481916
*/
18491917
std::optional<std::vector<Opcode>> DecomposeScript(const CScript& script);
18501918

@@ -2023,6 +2091,36 @@ inline NodeRef<Key> DecodeScript(I& in, I last, const Ctx& ctx)
20232091
constructed.push_back(MakeNodeRef<Key>(internal::NoDupCheck{}, ctx.MsContext(), Fragment::MULTI, std::move(keys), *k));
20242092
break;
20252093
}
2094+
// Tapscript's equivalent of multi
2095+
if (last - in >= 4 && in[0].first == OP_NUMEQUAL) {
2096+
if (!IsTapscript(ctx.MsContext())) return {};
2097+
// The necessary threshold of signatures.
2098+
const auto k = ParseScriptNumber(in[1]);
2099+
if (!k) return {};
2100+
if (*k < 1 || *k > MAX_PUBKEYS_PER_MULTI_A) return {};
2101+
if (last - in < 2 + *k * 2) return {};
2102+
std::vector<Key> keys;
2103+
keys.reserve(*k);
2104+
// Walk through the expected (pubkey, CHECKSIG[ADD]) pairs.
2105+
for (int pos = 2;; pos += 2) {
2106+
if (last - in < pos + 2) return {};
2107+
// Make sure it's indeed an x-only pubkey and a CHECKSIG[ADD], then parse the key.
2108+
if (in[pos].first != OP_CHECKSIGADD && in[pos].first != OP_CHECKSIG) return {};
2109+
if (in[pos + 1].second.size() != 32) return {};
2110+
auto key = ctx.FromPKBytes(in[pos + 1].second.begin(), in[pos + 1].second.end());
2111+
if (!key) return {};
2112+
keys.push_back(std::move(*key));
2113+
// Make sure early we don't parse an arbitrary large expression.
2114+
if (keys.size() > MAX_PUBKEYS_PER_MULTI_A) return {};
2115+
// OP_CHECKSIG means it was the last one to parse.
2116+
if (in[pos].first == OP_CHECKSIG) break;
2117+
}
2118+
if (keys.size() < (size_t)*k) return {};
2119+
in += 2 + keys.size() * 2;
2120+
std::reverse(keys.begin(), keys.end());
2121+
constructed.push_back(MakeNodeRef<Key>(internal::NoDupCheck{}, ctx.MsContext(), Fragment::MULTI_A, std::move(keys), *k));
2122+
break;
2123+
}
20262124
/** In the following wrappers, we only need to push SINGLE_BKV_EXPR rather
20272125
* than BKV_EXPR, because and_v commutes with these wrappers. For example,
20282126
* c:and_v(X,Y) produces the same script as and_v(X,c:Y). */

src/test/fuzz/miniscript.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,9 @@ struct SmartInfo
514514

515515
// Based on the fragment, determine #subs/data/k/keys to pass to ComputeType. */
516516
switch (frag) {
517+
case Fragment::MULTI_A:
518+
// TODO: Tapscript support.
519+
assert(false);
517520
case Fragment::PK_K:
518521
case Fragment::PK_H:
519522
n_keys = 1;
@@ -703,6 +706,9 @@ std::optional<NodeInfo> ConsumeNodeSmart(FuzzedDataProvider& provider, Type type
703706

704707
// Based on the fragment the recipe uses, fill in other data (k, keys, data).
705708
switch (frag) {
709+
case Fragment::MULTI_A:
710+
// TODO: Tapscript support.
711+
assert(false);
706712
case Fragment::PK_K:
707713
case Fragment::PK_H:
708714
return {{frag, ConsumePubKey(provider)}};
@@ -793,6 +799,9 @@ NodeRef GenNode(F ConsumeNode, Type root_type, bool strict_valid = false) {
793799
scriptsize += miniscript::internal::ComputeScriptLen(node_info->fragment, ""_mst, node_info->subtypes.size(), node_info->k, node_info->subtypes.size(), node_info->keys.size()) - 1;
794800
if (scriptsize > MAX_STANDARD_P2WSH_SCRIPT_SIZE) return {};
795801
switch (node_info->fragment) {
802+
case Fragment::MULTI_A:
803+
// TODO: Tapscript support.
804+
assert(false);
796805
case Fragment::JUST_0:
797806
case Fragment::JUST_1:
798807
break;
@@ -1019,6 +1028,9 @@ void TestNode(const NodeRef& node, FuzzedDataProvider& provider)
10191028
// satisfaction will also match the expected policy.
10201029
bool satisfiable = node->IsSatisfiable([](const Node& node) -> bool {
10211030
switch (node.fragment) {
1031+
case Fragment::MULTI_A:
1032+
// TODO: Tapscript support.
1033+
assert(false);
10221034
case Fragment::PK_K:
10231035
case Fragment::PK_H: {
10241036
auto it = TEST_DATA.dummy_sigs.find(node.keys[0]);

0 commit comments

Comments
 (0)