Implements parsing of BIP 390 musig()
descriptors.
Split from #29675
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/31244.
See the guideline for information on the review process.
If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.
Reviewers, this pull request conflicts with the following ones:
exportwatchonlywallet
RPC to export a watchonly version of a wallet by achow101)XOnlyPubKey::GetKeyIDs()
to return a pair of pubkeys by w0xlt)If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.
0$ echo "cGsobXVzaWcoZGR9dXVlLzAwLylrKA==" | base64 --decode > mocked_descriptor_parse_1.crash
1$ FUZZ=mocked_descriptor_parse fuzz mocked_descriptor_parse_1.crash
2script/descriptor.cpp:1838 ParsePubkey: Assertion `Func("musig", expr)' failed.
3
4$ echo "dHIobXVzaWcoICAgICB0dXVzKG9sZGVwayhnZylnZ2dnZmdnKTwseigoKCgoKCgoKCgoKCgoKCgoKCgoKHN0KQ==" | base64 --decode > mocked_descriptor_parse_2.crash
5$ FUZZ=mocked_descriptor_parse fuzz mocked_descriptor_parse_2.crash
6script/descriptor.cpp:1833 ParsePubkey: Assertion `split.size() <= 2' failed.
0$ echo "dHIobXVzaWcoJTIyLzMzMmgpSigoKChhZGRyKEJjdXUp" | base64 --decode > mocked_descriptor_parse_3.crash
1$ FUZZ=mocked_descriptor_parse fuzz mocked_descriptor_parse_3.crash
2script/descriptor.cpp:632 GetPubKey: Assertion `pubkey.has_value()' failed.
0$ echo "dHIobXVzaWcoKS8wMDAwMTEp" | base64 --decode > descriptor_parse_1.crash
1$ FUZZ=descriptor_parse fuzz descriptor_parse_1.crash
2[libsecp256k1] illegal argument: pubkeys != NULL
0$ echo "dHIobXVzaWcoJWU5LzwwOzAwMDAwMzU7MDYwOzM7MDY+KS82Nik=" | base64 --decode > mocked_descriptor_parse_4.crash
1$ FUZZ=mocked_descriptor_parse fuzz mocked_descriptor_parse_4.crash
2fuzz: script/descriptor.cpp:1942: std::vector<std::unique_ptr<PubkeyProvider>> (anonymous namespace)::ParsePubkey(uint32_t &, const Span<const char> &, ParseScriptContext, FlatSigningProvider &, std::string &): Assertion `pub.size() == 1' failed.
🚧 At least one of the CI tasks failed. Debug: https://github.com/bitcoin/bitcoin/runs/34992034826
Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:
Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.
A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.
An intermittent issue.
Leave a comment here, if you need help tracking down a confusing failure.
🚧 At least one of the CI tasks failed. Debug: https://github.com/bitcoin/bitcoin/runs/40357481289
Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:
Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.
A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.
An intermittent issue.
Leave a comment here, if you need help tracking down a confusing failure.
86@@ -82,13 +87,21 @@ bool FlatSigningProvider::GetTaprootBuilder(const XOnlyPubKey& output_key, Tapro
87 return LookupHelper(tr_trees, output_key, builder);
88 }
89
90+std::vector<CPubKey> FlatSigningProvider::GetAggregateParticipantPubkeys(const CPubKey& pubkey) const
91+{
92+ const auto& it = aggregate_pubkeys.find(pubkey);
93+ if (it == aggregate_pubkeys.end()) return {};
94+ return it->second;
nit: could use LookupHelper
here (for slightly better readability imho)
0 std::vector<CPubKey> participant_pubkeys;
1 LookupHelper(aggregate_pubkeys, pubkey, participant_pubkeys);
2 return participant_pubkeys;
5+#ifndef BITCOIN_MUSIG_H
6+#define BITCOIN_MUSIG_H
7+
8+#include <pubkey.h>
9+
10+#include <vector>
nit: should also include <optional>
, since it’s used for the return types below
0#include <optional>
1#include <vector>
598+ bool IsRangedParticipants() const
599+ {
600+ for (const auto& pubkey : m_participants) {
601+ if (pubkey->IsRange()) return true;
602+ }
603+ return false;
std::any_of
788+ // musig() can only be a BIP 32 key if all participants are bip32 too
789+ bool all_bip32 = true;
790+ for (const auto& pk : m_participants) {
791+ all_bip32 &= pk->IsBIP32();
792+ }
793+ return all_bip32;
std::all_of
Concept ACK
Seems like the commit message for 2e6dcdbc8055660a2e20ba81b62b7d26ae0ccb05 (“Add MuSig2 Keyagg Cache class and functions”) is out-of-sync, as there is no such class added and also the mentioned MuSig2KeyAggCacheImpl
doesn’t exist.
For the newly introduced parameters of the Const
and Split
functions (commits fe02d7cb237c00de6abe1776b3101342ffddf757 and 4a1eeee27a64c4be7293740f8fff839879b88d86), it would be nice to have unit test coverage, but this can be also done in a follow-up.
638+ // Make the synthetic xpub and construct the BIP32PubkeyProvider
639+ CExtPubKey extpub;
640+ extpub.nDepth = 0;
641+ std::memset(extpub.vchFingerprint, 0, 4);
642+ extpub.nChild = 0;
643+ extpub.chaincode.FromHex("6589e367712c6200e367717145cb322d76576bc3248959c474f9a602ca878086");
musig.h
.
11+#include <vector>
12+
13+struct secp256k1_musig_keyagg_cache;
14+
15+bool GetMuSig2KeyAggCache(const std::vector<CPubKey>& pubkeys, secp256k1_musig_keyagg_cache& keyagg_cache);
16+std::optional<CPubKey> GetCPubKeyFromMuSig2KeyAggCache(secp256k1_musig_keyagg_cache& cache);
285@@ -286,6 +286,7 @@ class XOnlyPubKey
286 * This is needed for key lookups since keys are indexed by CKeyID.
287 */
288 std::vector<CKeyID> GetKeyIDs() const;
289+ std::vector<CPubKey> GetCPubKeys() const;
In 225b3adbf53e4dfde32a1f798cde30cc41998e3c “XOnlyPubKey: Add GetCPubKeys”: maybe document:
0// Return both a version prefixed with 0x02, and one with 0x03.
The comment inside GetKeyIDs
could also be moved to the header.
12@@ -13,10 +13,10 @@ namespace script {
13
14 /** Parse a constant.
15 *
16- * If sp's initial part matches str, sp is updated to skip that part, and true is returned.
17+ * If sp's initial part matches str, sp is optionally updated to skip that part, and true is returned.
99@@ -100,18 +100,27 @@ void ReplaceAll(std::string& in_out, const std::string& search, const std::strin
100 *
101 * If sep does not occur in sp, a singleton with the entirety of sp is returned.
102 *
103+ * @param[in] include_sep Whether to include the separator at the end of the left side of the splits.
104+ *
105 * Note that this function does not care about braces, so splitting
106 * "foo(bar(1),2),3) on ',' will return {"foo(bar(1)", "2)", "3)"}.
107+ *
108+ * If include_sep == true, splitting "foo(bar(1),2),3) on ','
109+ * will return {"foo(bar(1),", "2),", "3)"}.
Also in https://github.com/bitcoin/bitcoin/commit/4a1eeee27a64c4be7293740f8fff839879b88d86, this would be a bit easier to read:
0* will return the following strings:
1* - foo(bar(1),
2* - 2),
3* - 3)
220@@ -221,6 +221,9 @@ struct PubkeyProvider
221
222 /** Make a deep copy of this PubkeyProvider */
223 virtual std::unique_ptr<PubkeyProvider> Clone() const = 0;
224+
225+ /** Whether this PubkeyProvider can be a BIP 32 extended key that can be derived from */
12+
13+struct secp256k1_musig_keyagg_cache;
14+
15+bool GetMuSig2KeyAggCache(const std::vector<CPubKey>& pubkeys, secp256k1_musig_keyagg_cache& keyagg_cache);
16+std::optional<CPubKey> GetCPubKeyFromMuSig2KeyAggCache(secp256k1_musig_keyagg_cache& cache);
17+std::optional<CPubKey> MuSig2AggregatePubkeys(const std::vector<CPubKey>& pubkeys);
MuSig2DeriveAggregatePubkey()
10+#include <optional>
11+#include <vector>
12+
13+struct secp256k1_musig_keyagg_cache;
14+
15+bool GetMuSig2KeyAggCache(const std::vector<CPubKey>& pubkeys, secp256k1_musig_keyagg_cache& keyagg_cache);
761+ // If there is not, then the participant privkeys will be included directly
762+ for (const auto& prov : m_participants) {
763+ prov->GetPrivKey(pos, arg, out);
764+ }
765+ }
766+ std::optional<CPubKey> GetRootPubKey() const override
754+ return true;
755+ }
756+
757+ void GetPrivKey(int pos, const SigningProvider& arg, FlatSigningProvider& out) const override
758+ {
759+ // Get the private keys for all participants
725+ } else {
726+ out += pubkey->ToString();
727+ }
728+ }
729+ out += ")";
730+ out += FormatHDKeypath(m_path, /*apostrophe=*/true);
Concept ACK
Reviewed up to 6b634754d2783ffe16570ad37dbcf9251a7efc24, mostly happy.
I didn’t thoroughly check that all test vectors from BIP390 are in the test, but did check a few.
Can you add an example to
doc/descriptors.md
?
This still seems useful (and this document should probably link back to the bip numbers, but that’s orthogonal).
Manual testing hint for other reviewers: you can use the deriveaddresses
RPC with some of the test vectors. Just use 00000000
as the checksum, it will tell you the right one, and then call it gain to see an address. Pass that address into validateaddress
and compare the scriptPubKey
to the one in the bip.
664+ // Either way, we can passthrough to it's GetPubKey
665+ std::optional<CPubKey> pub = m_aggregate_provider->GetPubKey(pos, arg, out, read_cache, write_cache);
666+ if (!pub) return std::nullopt;
667+ pubout = *pub;
668+ out.aggregate_pubkeys.emplace(m_aggregate_pubkey.value(), pubkeys);
669+ } else if (IsRangedParticipants()) {
in commit 048c95ea557cf81a8e55c5ffe9994cc9d8f3e039: could Assert
this condition instead, as otherwise the logic suggests that it’s possible that neither of the if-branches are executed (it’s not, IIUC; the only reason to not have a cached aggregate pubkey at this point is if we have ranged participants, so this condition is always true)
0 } else {
1 Assert(IsRangedParticipants());
Assume
. It’s occurred to me that Assert
in descriptor code is probably not the way to go, so I’ve changed the couple other Assert
to Assume
.
11@@ -12,6 +12,10 @@
12
13 struct secp256k1_musig_keyagg_cache;
14
15+//! MuSig2 chaincode as defined by BIP 328
16+//! uint256 will byteswap the hex
17+constexpr uint256 MUSIG_CHAINCODE{"6589e367712c6200e367717145cb322d76576bc3248959c474f9a602ca878086"};
in commit 048c95ea557cf81a8e55c5ffe9994cc9d8f3e039: not too fond of “reverse-hexstrings of byte-strings” creeping in, could do instead
0using namespace util::hex_literals;
1constexpr std::array<uint8_t, 32> MUSIG_CHAINCODE{"868087ca02a6f974c4598924c36b57762d32cb45717167e300622c7167e38965"_hex_u8};
and assign the constant to the uint256 instance via ... = uint256(MUSIG_CHAINCODE)
.
(but shouldn’t be a blocker, can be improved in a follow-up as well, if others are even as annoyed by that as me 😅 )
_hex_u8
can be used directly in uin256’s constructor with normal byte order, so used that.
We need to retrieve the even and odd compressed pubkeys for xonly
pubkeys, so add a function to do that. Also reuse it in GetKeyIDs.
When splitting a string, sometimes the separator needs to be included.
Split will now optionally include the separator at the end of the left
side of the splits, i.e. it appears at the end of the splits, except
for the last one.
secp256k1 provides us secp256k1_musig_keyagg_cache objects which we are
used as part of session info and to get the aggregate pubkey. These
helper functions help us convert to/from the secp256k1 C objects into
the Bitcoin Core C++ objects.
15+//! MuSig2 chaincode as defined by BIP 328
16+//! uint256 will byteswap the hex
17+using namespace util::hex_literals;
18+constexpr uint256 MUSIG_CHAINCODE{"868087ca02a6f974c4598924c36b57762d32cb45717167e300622c7167e38965"_hex_u8};
19+
20+//! Create a secp56k1_musig_keyagg_cache from the pubkeys in their current order. This is necessary for most MuSig2 operations
782+ std::unique_ptr<PubkeyProvider> Clone() const override
783+ {
784+ std::vector<std::unique_ptr<PubkeyProvider>> providers;
785+ providers.reserve(m_participants.size());
786+ for (const std::unique_ptr<PubkeyProvider>& p : m_participants) {
787+ providers.emplace_back(p->Clone());
Clone()
? Maybe Check()
in descriptor_tests.cpp
should do this for all valid descriptors?
DescriptorImpl
and PubkeyProvider
classes; all of the unit tests start with a descriptor string that has to be parsed. The parsing is implemented in the next commit (because the PubkeyProvider
it uses needs to exist before parsing can construct the object), and tests for everything in the following commit.
715+ bool ToPrivateString(const SigningProvider& arg, std::string& out) const override
716+ {
717+ bool any_privkeys = false;
718+ out = "musig(";
719+ for (size_t i = 0; i < m_participants.size(); ++i) {
720+ const auto& pubkey = m_participants.at(i);
In 35db4f2dcfc3435e10935581ffa447ffe219cc1e “descriptor: Add MuSigPubkeyProvider”: could avoid old school for i
, make it slightly shorter and have the intention be more clear:
0--- a/src/script/descriptor.cpp
1+++ b/src/script/descriptor.cpp
2@@ -691,9 +691,9 @@ public:
3 std::string ToString(StringType type=StringType::PUBLIC) const override
4 {
5 std::string out = "musig(";
6- for (size_t i = 0; i < m_participants.size(); ++i) {
7- const auto& pubkey = m_participants.at(i);
8- if (i) out += ",";
9+ size_t pos{0};
10+ for (const auto& pubkey : m_participants) {
11+ if (pos++) out += ",";
12 std::string tmp;
Though it’s a not a huge improvement.
for i
is used here because we need the position.
613+ m_derive(derive)
614+ {}
615+
616+ std::optional<CPubKey> GetPubKey(int pos, const SigningProvider& arg, FlatSigningProvider& out, const DescriptorCache* read_cache = nullptr, DescriptorCache* write_cache = nullptr) const override
617+ {
618+ // If the participants are not ranged, we can compute and cache the aggregate pubkey by creating a PubkeyProvider for it
In 35db4f2dcfc3435e10935581ffa447ffe219cc1e “descriptor: Add MuSigPubkeyProvider”: just to check my own understanding, by ranged participants you mean the case of musig(alice/*, bob)
, i.e. derivation before aggregation?
I assume that if the participants are ranged, then you can’t also have a range after derivation, e.g. musig(alice/*, bob)/*
isn’t allowed. Where do we check this?
I assume that if the participants are ranged, then you can’t also have a range after derivation, e.g. musig(alice/*, bob)/* isn’t allowed. Where do we check this?
In the next commit which implements parsing. Enforcement of the descriptor constraints is usually at the parsing level as it is not possible create these classes outside of the descriptor module.
As an additional belt-and-suspenders and documentation, would a corresponding assertion (or Assume
) in the MuSig2PubkeyProvider ctor make sense? E.g. something like
0diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp
1index 04ba832ffc..92dcf73755 100644
2--- a/src/script/descriptor.cpp
3+++ b/src/script/descriptor.cpp
4@@ -611,7 +611,10 @@ public:
5 m_participants(std::move(providers)),
6 m_path(std::move(path)),
7 m_derive(derive)
8- {}
9+ {
10+ /* if participants are already ranged, further derivation after pubkey aggregation isn't allowed */
11+ Assert(!(IsRangedParticipants() && IsRangedDerivation()));
12+ }
13
14 std::optional<CPubKey> GetPubKey(int pos, const SigningProvider& arg, FlatSigningProvider& out, const DescriptorCache* read_cache = nullptr, DescriptorCache* write_cache = nullptr) const override
15 {
Assume
.
MuSigPubkeyProvider
introduced in 35db4f2dcfc3435e10935581ffa447ffe219cc1e.
11@@ -12,6 +12,11 @@
12
13 struct secp256k1_musig_keyagg_cache;
14
15+//! MuSig2 chaincode as defined by BIP 328
16+//! uint256 will byteswap the hex
17+using namespace util::hex_literals;
18+constexpr uint256 MUSIG_CHAINCODE{"868087ca02a6f974c4598924c36b57762d32cb45717167e300622c7167e38965"_hex_u8};
in commit 35db4f2dcfc3435e10935581ffa447ffe219cc1e: the byteswap comment is obsolete now
0//! MuSig2 chaincode as defined by BIP 328
1using namespace util::hex_literals;
2constexpr uint256 MUSIG_CHAINCODE{"868087ca02a6f974c4598924c36b57762d32cb45717167e300622c7167e38965"_hex_u8};
632+ // Aggregate the pubkey
633+ m_aggregate_pubkey = MuSig2AggregatePubkeys(pubkeys);
634+ if (!Assume(m_aggregate_pubkey.has_value())) return std::nullopt;
635+
636+ // Make our pubkey provider
637+ if (m_derive != DeriveType::NO || !m_path.empty()) {
in commit 35db4f2dcfc3435e10935581ffa447ffe219cc1e: nit, could use the helper here:
0 if (IsRangedDerivation() || !m_path.empty()) {
1657@@ -1653,7 +1658,7 @@ std::optional<uint32_t> ParseKeyPathNum(std::span<const char> elem, bool& apostr
1658 * @param[in] allow_multipath Allows the parsed path to use the multipath specifier
1659 * @returns false if parsing failed
1660 **/
1661-[[nodiscard]] bool ParseKeyPath(const std::vector<std::span<const char>>& split, std::vector<KeyPath>& out, bool& apostrophe, std::string& error, bool allow_multipath)
1662+[[nodiscard]] bool ParseKeyPath(const std::vector<std::span<const char>>& split, std::vector<KeyPath>& out, bool& apostrophe, std::string& error, bool allow_multipath, bool allow_hardened = true)
1806@@ -1802,9 +1807,153 @@ std::vector<std::unique_ptr<PubkeyProvider>> ParsePubkeyInner(uint32_t key_exp_i
1807 }
1808
1809 /** Parse a public key including origin information (if enabled). */
1810-std::vector<std::unique_ptr<PubkeyProvider>> ParsePubkey(uint32_t key_exp_index, const std::span<const char>& sp, ParseScriptContext ctx, FlatSigningProvider& out, std::string& error)
1811+// NOLINTNEXTLINE(misc-no-recursion)
1812+std::vector<std::unique_ptr<PubkeyProvider>> ParsePubkey(uint32_t& key_exp_index, const std::span<const char>& sp, ParseScriptContext ctx, FlatSigningProvider& out, std::string& error)
key_exp_index
parameter changed to pass by reference? Without that the unit tests fail, so it seems necessary, but I’m confused why this was not needed earlier already for similar constructs (e.g. for multi()
expressions where the value is also incremented in the key expression parsing loop). Probably I’m missing some basic descriptors parsing knowledge.
musig()
expressions are a key expression, which itself contains multiple key expressions. It needs to be able to increment key_exp_index
such that the caller will also know what the key_exp_index
has been incremented to. The easiest way to do this was a reference.
ParsePubkey
vs ParseScript
; for the latter the key_exp_index
argument was already passed by reference).
611+ m_participants(std::move(providers)),
612+ m_path(std::move(path)),
613+ m_derive(derive)
614+ {
615+ if (!Assume(!(IsRangedParticipants() && IsRangedDerivation()))) {
616+ throw std::runtime_error("musig(): Cannot have both ranged participatns and ranged derivation");
participatns -> participants [correct misspelling in error message]
705+ break;
706+ case StringType::COMPAT:
707+ tmp = pubkey->ToString(PubkeyProvider::StringType::COMPAT);
708+ break;
709+ }
710+ out += tmp;
in commit ff7476ec32f4dbee07bf04169e741e2aea5a9ab7: can’t this be simplified to
0 out += pubkey->ToString(type);
? It’s a bit confusing that there are two StringType
enum classes within different classes, (PubkeyProvider::StringType
and DescriptorImpl::StringType
), but the code here seems to deal only with the former. I guess whether the diff makes sense or not depends on if it’s possible in the future that the default arguments for pubkey providers ToString(...)
method is ever changed from StringType::PUBLIC
to something else. The unit tests still seem to pass at least.