Skip to content

Commit 1bbf46e

Browse files
committed
descriptors: Change Parse to return vector of descriptors
When given a descriptor which contins a multipath derivation specifier, a vector of descriptors will be returned.
1 parent 0d640c6 commit 1bbf46e

22 files changed

+174
-107
lines changed

src/bench/descriptors.cpp

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ static void ExpandDescriptor(benchmark::Bench& bench)
1818
const std::pair<int64_t, int64_t> range = {0, 1000};
1919
FlatSigningProvider provider;
2020
std::string error;
21-
auto desc = Parse(desc_str, provider, error);
21+
auto descs = Parse(desc_str, provider, error);
2222

2323
bench.run([&] {
2424
for (int i = range.first; i <= range.second; ++i) {
2525
std::vector<CScript> scripts;
26-
bool success = desc->Expand(i, provider, scripts, provider);
26+
bool success = descs[0]->Expand(i, provider, scripts, provider);
2727
assert(success);
2828
}
2929
});

src/bench/wallet_ismine.cpp

+2-2
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ static void WalletIsMine(benchmark::Bench& bench, bool legacy_wallet, int num_co
4343
key.MakeNewKey(/*fCompressed=*/true);
4444
FlatSigningProvider keys;
4545
std::string error;
46-
std::unique_ptr<Descriptor> desc = Parse("combo(" + EncodeSecret(key) + ")", keys, error, /*require_checksum=*/false);
47-
WalletDescriptor w_desc(std::move(desc), /*creation_time=*/0, /*range_start=*/0, /*range_end=*/0, /*next_index=*/0);
46+
std::vector<std::unique_ptr<Descriptor>> desc = Parse("combo(" + EncodeSecret(key) + ")", keys, error, /*require_checksum=*/false);
47+
WalletDescriptor w_desc(std::move(desc.at(0)), /*creation_time=*/0, /*range_start=*/0, /*range_end=*/0, /*next_index=*/0);
4848
auto spkm = wallet->AddWalletDescriptor(w_desc, keys, /*label=*/"", /*internal=*/false);
4949
assert(spkm);
5050
}

src/qt/test/wallettests.cpp

+4-2
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,10 @@ std::shared_ptr<CWallet> SetupDescriptorsWallet(interfaces::Node& node, TestChai
218218
// Add the coinbase key
219219
FlatSigningProvider provider;
220220
std::string error;
221-
std::unique_ptr<Descriptor> desc = Parse("combo(" + EncodeSecret(test.coinbaseKey) + ")", provider, error, /* require_checksum=*/ false);
222-
assert(desc);
221+
auto descs = Parse("combo(" + EncodeSecret(test.coinbaseKey) + ")", provider, error, /* require_checksum=*/ false);
222+
assert(!descs.empty());
223+
assert(descs.size() == 1);
224+
auto& desc = descs.at(0);
223225
WalletDescriptor w_desc(std::move(desc), 0, 0, 1, 1);
224226
if (!wallet->AddWalletDescriptor(w_desc, provider, "", false)) assert(false);
225227
CTxDestination dest = GetDestinationForKey(test.coinbaseKey.GetPubKey(), wallet->m_default_address_type);

src/rpc/mining.cpp

+25-24
Original file line numberDiff line numberDiff line change
@@ -180,35 +180,36 @@ static UniValue generateBlocks(ChainstateManager& chainman, Mining& miner, const
180180
static bool getScriptFromDescriptor(const std::string& descriptor, CScript& script, std::string& error)
181181
{
182182
FlatSigningProvider key_provider;
183-
const auto desc = Parse(descriptor, key_provider, error, /* require_checksum = */ false);
184-
if (desc) {
185-
if (desc->IsRange()) {
186-
throw JSONRPCError(RPC_INVALID_PARAMETER, "Ranged descriptor not accepted. Maybe pass through deriveaddresses first?");
187-
}
188-
189-
FlatSigningProvider provider;
190-
std::vector<CScript> scripts;
191-
if (!desc->Expand(0, key_provider, scripts, provider)) {
192-
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Cannot derive script without private keys");
193-
}
183+
const auto descs = Parse(descriptor, key_provider, error, /* require_checksum = */ false);
184+
if (descs.empty()) return false;
185+
if (descs.size() > 1) {
186+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Multipath descriptor not accepted");
187+
}
188+
const auto& desc = descs.at(0);
189+
if (desc->IsRange()) {
190+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Ranged descriptor not accepted. Maybe pass through deriveaddresses first?");
191+
}
194192

195-
// Combo descriptors can have 2 or 4 scripts, so we can't just check scripts.size() == 1
196-
CHECK_NONFATAL(scripts.size() > 0 && scripts.size() <= 4);
193+
FlatSigningProvider provider;
194+
std::vector<CScript> scripts;
195+
if (!desc->Expand(0, key_provider, scripts, provider)) {
196+
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Cannot derive script without private keys");
197+
}
197198

198-
if (scripts.size() == 1) {
199-
script = scripts.at(0);
200-
} else if (scripts.size() == 4) {
201-
// For uncompressed keys, take the 3rd script, since it is p2wpkh
202-
script = scripts.at(2);
203-
} else {
204-
// Else take the 2nd script, since it is p2pkh
205-
script = scripts.at(1);
206-
}
199+
// Combo descriptors can have 2 or 4 scripts, so we can't just check scripts.size() == 1
200+
CHECK_NONFATAL(scripts.size() > 0 && scripts.size() <= 4);
207201

208-
return true;
202+
if (scripts.size() == 1) {
203+
script = scripts.at(0);
204+
} else if (scripts.size() == 4) {
205+
// For uncompressed keys, take the 3rd script, since it is p2wpkh
206+
script = scripts.at(2);
209207
} else {
210-
return false;
208+
// Else take the 2nd script, since it is p2pkh
209+
script = scripts.at(1);
211210
}
211+
212+
return true;
212213
}
213214

214215
static RPCHelpMan generatetodescriptor()

src/rpc/output_script.cpp

+27-10
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,11 @@ static RPCHelpMan getdescriptorinfo()
175175
RPCResult{
176176
RPCResult::Type::OBJ, "", "",
177177
{
178-
{RPCResult::Type::STR, "descriptor", "The descriptor in canonical form, without private keys"},
178+
{RPCResult::Type::STR, "descriptor", "The descriptor in canonical form, without private keys. For a multipath descriptor, only the first will be returned."},
179+
{RPCResult::Type::ARR, "multipath_expansion", /*optional=*/true, "All descriptors produced by expanding multipath derivation elements. Only if the provided descriptor specifies multipath derivation elements.",
180+
{
181+
{RPCResult::Type::STR, "", ""},
182+
}},
179183
{RPCResult::Type::STR, "checksum", "The checksum for the input descriptor"},
180184
{RPCResult::Type::BOOL, "isrange", "Whether the descriptor is ranged"},
181185
{RPCResult::Type::BOOL, "issolvable", "Whether the descriptor is solvable"},
@@ -191,16 +195,25 @@ static RPCHelpMan getdescriptorinfo()
191195
{
192196
FlatSigningProvider provider;
193197
std::string error;
194-
auto desc = Parse(request.params[0].get_str(), provider, error);
195-
if (!desc) {
198+
auto descs = Parse(request.params[0].get_str(), provider, error);
199+
if (descs.empty()) {
196200
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, error);
197201
}
198202

199203
UniValue result(UniValue::VOBJ);
200-
result.pushKV("descriptor", desc->ToString());
204+
result.pushKV("descriptor", descs.at(0)->ToString());
205+
206+
if (descs.size() > 1) {
207+
UniValue multipath_descs(UniValue::VARR);
208+
for (const auto& d : descs) {
209+
multipath_descs.push_back(d->ToString());
210+
}
211+
result.pushKV("multipath_expansion", multipath_descs);
212+
}
213+
201214
result.pushKV("checksum", GetDescriptorChecksum(request.params[0].get_str()));
202-
result.pushKV("isrange", desc->IsRange());
203-
result.pushKV("issolvable", desc->IsSolvable());
215+
result.pushKV("isrange", descs.at(0)->IsRange());
216+
result.pushKV("issolvable", descs.at(0)->IsSolvable());
204217
result.pushKV("hasprivatekeys", provider.keys.size() > 0);
205218
return result;
206219
},
@@ -221,7 +234,8 @@ static RPCHelpMan deriveaddresses()
221234
" tr(<pubkey>,multi_a(<n>,<pubkey>,<pubkey>,...)) P2TR-multisig outputs for the given threshold and pubkeys\n"
222235
"\nIn the above, <pubkey> either refers to a fixed public key in hexadecimal notation, or to an xpub/xprv optionally followed by one\n"
223236
"or more path elements separated by \"/\", where \"h\" represents a hardened child key.\n"
224-
"For more information on output descriptors, see the documentation in the doc/descriptors.md file.\n"},
237+
"For more information on output descriptors, see the documentation in the doc/descriptors.md file.\n"
238+
"Note that only descriptors that specify a single derivation path can be derived.\n"},
225239
{
226240
{"descriptor", RPCArg::Type::STR, RPCArg::Optional::NO, "The descriptor."},
227241
{"range", RPCArg::Type::RANGE, RPCArg::Optional::OMITTED, "If a ranged descriptor is used, this specifies the end or the range (in [begin,end] notation) to derive."},
@@ -250,11 +264,14 @@ static RPCHelpMan deriveaddresses()
250264

251265
FlatSigningProvider key_provider;
252266
std::string error;
253-
auto desc = Parse(desc_str, key_provider, error, /* require_checksum = */ true);
254-
if (!desc) {
267+
auto descs = Parse(desc_str, key_provider, error, /* require_checksum = */ true);
268+
if (descs.empty()) {
255269
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, error);
256270
}
257-
271+
if (descs.size() > 1) {
272+
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Descriptor with multipath derivation path specifiers are not allowed");
273+
}
274+
auto& desc = descs.at(0);
258275
if (!desc->IsRange() && request.params.size() > 1) {
259276
throw JSONRPCError(RPC_INVALID_PARAMETER, "Range should not be specified for an un-ranged descriptor");
260277
}

src/rpc/util.cpp

+12-10
Original file line numberDiff line numberDiff line change
@@ -1337,24 +1337,26 @@ std::vector<CScript> EvalDescriptorStringOrObject(const UniValue& scanobject, Fl
13371337
}
13381338

13391339
std::string error;
1340-
auto desc = Parse(desc_str, provider, error);
1341-
if (!desc) {
1340+
auto descs = Parse(desc_str, provider, error);
1341+
if (descs.empty()) {
13421342
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, error);
13431343
}
1344-
if (!desc->IsRange()) {
1344+
if (!descs.at(0)->IsRange()) {
13451345
range.first = 0;
13461346
range.second = 0;
13471347
}
13481348
std::vector<CScript> ret;
13491349
for (int i = range.first; i <= range.second; ++i) {
1350-
std::vector<CScript> scripts;
1351-
if (!desc->Expand(i, provider, scripts, provider)) {
1352-
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Cannot derive script without private keys: '%s'", desc_str));
1353-
}
1354-
if (expand_priv) {
1355-
desc->ExpandPrivate(/*pos=*/i, provider, /*out=*/provider);
1350+
for (const auto& desc : descs) {
1351+
std::vector<CScript> scripts;
1352+
if (!desc->Expand(i, provider, scripts, provider)) {
1353+
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Cannot derive script without private keys: '%s'", desc_str));
1354+
}
1355+
if (expand_priv) {
1356+
desc->ExpandPrivate(/*pos=*/i, provider, /*out=*/provider);
1357+
}
1358+
std::move(scripts.begin(), scripts.end(), std::back_inserter(ret));
13561359
}
1357-
std::move(scripts.begin(), scripts.end(), std::back_inserter(ret));
13581360
}
13591361
return ret;
13601362
}

src/script/descriptor.cpp

+11-4
Original file line numberDiff line numberDiff line change
@@ -2364,14 +2364,21 @@ bool CheckChecksum(Span<const char>& sp, bool require_checksum, std::string& err
23642364
return true;
23652365
}
23662366

2367-
std::unique_ptr<Descriptor> Parse(const std::string& descriptor, FlatSigningProvider& out, std::string& error, bool require_checksum)
2367+
std::vector<std::unique_ptr<Descriptor>> Parse(const std::string& descriptor, FlatSigningProvider& out, std::string& error, bool require_checksum)
23682368
{
23692369
Span<const char> sp{descriptor};
2370-
if (!CheckChecksum(sp, require_checksum, error)) return nullptr;
2370+
if (!CheckChecksum(sp, require_checksum, error)) return {};
23712371
uint32_t key_exp_index = 0;
23722372
auto ret = ParseScript(key_exp_index, sp, ParseScriptContext::TOP, out, error);
2373-
if (sp.size() == 0 && !ret.empty()) return std::unique_ptr<Descriptor>(std::move(ret.at(0)));
2374-
return nullptr;
2373+
if (sp.size() == 0 && !ret.empty()) {
2374+
std::vector<std::unique_ptr<Descriptor>> descs;
2375+
descs.reserve(ret.size());
2376+
for (auto& r : ret) {
2377+
descs.emplace_back(std::unique_ptr<Descriptor>(std::move(r)));
2378+
}
2379+
return descs;
2380+
}
2381+
return {};
23752382
}
23762383

23772384
std::string GetDescriptorChecksum(const std::string& descriptor)

src/script/descriptor.h

+2-2
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,9 @@ struct Descriptor {
173173
* is set, the checksum is mandatory - otherwise it is optional.
174174
*
175175
* If a parse error occurs, or the checksum is missing/invalid, or anything
176-
* else is wrong, `nullptr` is returned.
176+
* else is wrong, an empty vector is returned.
177177
*/
178-
std::unique_ptr<Descriptor> Parse(const std::string& descriptor, FlatSigningProvider& out, std::string& error, bool require_checksum = false);
178+
std::vector<std::unique_ptr<Descriptor>> Parse(const std::string& descriptor, FlatSigningProvider& out, std::string& error, bool require_checksum = false);
179179

180180
/** Get the checksum for a `descriptor`.
181181
*

src/test/descriptor_tests.cpp

+11-8
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ void CheckUnparsable(const std::string& prv, const std::string& pub, const std::
2525
std::string error;
2626
auto parse_priv = Parse(prv, keys_priv, error);
2727
auto parse_pub = Parse(pub, keys_pub, error);
28-
BOOST_CHECK_MESSAGE(!parse_priv, prv);
29-
BOOST_CHECK_MESSAGE(!parse_pub, pub);
28+
BOOST_CHECK_MESSAGE(parse_priv.empty(), prv);
29+
BOOST_CHECK_MESSAGE(parse_pub.empty(), pub);
3030
BOOST_CHECK_EQUAL(error, expected_error);
3131
}
3232

@@ -139,19 +139,22 @@ void DoCheck(std::string prv, std::string pub, const std::string& norm_pub, int
139139
std::set<std::vector<uint32_t>> left_paths = paths;
140140
std::string error;
141141

142-
std::unique_ptr<Descriptor> parse_priv;
143-
std::unique_ptr<Descriptor> parse_pub;
142+
std::vector<std::unique_ptr<Descriptor>> parse_privs;
143+
std::vector<std::unique_ptr<Descriptor>> parse_pubs;
144144
// Check that parsing succeeds.
145145
if (replace_apostrophe_with_h_in_prv) {
146146
prv = UseHInsteadOfApostrophe(prv);
147147
}
148-
parse_priv = Parse(prv, keys_priv, error);
149-
BOOST_CHECK_MESSAGE(parse_priv, error);
148+
parse_privs = Parse(prv, keys_priv, error);
149+
BOOST_CHECK_MESSAGE(!parse_privs.empty(), error);
150150
if (replace_apostrophe_with_h_in_pub) {
151151
pub = UseHInsteadOfApostrophe(pub);
152152
}
153-
parse_pub = Parse(pub, keys_pub, error);
154-
BOOST_CHECK_MESSAGE(parse_pub, error);
153+
parse_pubs = Parse(pub, keys_pub, error);
154+
BOOST_CHECK_MESSAGE(!parse_pubs.empty(), error);
155+
156+
auto& parse_priv = parse_privs.at(0);
157+
auto& parse_pub = parse_pubs.at(0);
155158

156159
// We must be able to estimate the max satisfaction size for any solvable descriptor top descriptor (but combo).
157160
const bool is_nontop_or_nonsolvable{!parse_priv->IsSolvable() || !parse_priv->GetOutputType()};

src/test/fuzz/descriptor_parse.cpp

+25-5
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,24 @@
1515
MockedDescriptorConverter MOCKED_DESC_CONVERTER;
1616

1717
/** Test a successfully parsed descriptor. */
18-
static void TestDescriptor(const Descriptor& desc, FlatSigningProvider& sig_provider, std::string& dummy)
18+
static void TestDescriptor(const Descriptor& desc, FlatSigningProvider& sig_provider, std::string& dummy, std::optional<bool>& is_ranged, std::optional<bool>& is_solvable)
1919
{
2020
// Trivial helpers.
2121
(void)desc.IsRange();
22-
const bool is_solvable{desc.IsSolvable()};
2322
(void)desc.IsSingleType();
2423
(void)desc.GetOutputType();
2524

25+
if (is_ranged.has_value()) {
26+
assert(desc.IsRange() == *is_ranged);
27+
} else {
28+
is_ranged = desc.IsRange();
29+
}
30+
if (is_solvable.has_value()) {
31+
assert(desc.IsSolvable() == *is_solvable);
32+
} else {
33+
is_solvable = desc.IsSolvable();
34+
}
35+
2636
// Serialization to string representation.
2737
(void)desc.ToString();
2838
(void)desc.ToPrivateString(sig_provider, dummy);
@@ -48,7 +58,7 @@ static void TestDescriptor(const Descriptor& desc, FlatSigningProvider& sig_prov
4858
const auto max_sat_nonmaxsig{desc.MaxSatisfactionWeight(true)};
4959
const auto max_elems{desc.MaxSatisfactionElems()};
5060
// We must be able to estimate the max satisfaction size for any solvable descriptor (but combo).
51-
const bool is_nontop_or_nonsolvable{!is_solvable || !desc.GetOutputType()};
61+
const bool is_nontop_or_nonsolvable{!*is_solvable || !desc.GetOutputType()};
5262
const bool is_input_size_info_set{max_sat_maxsig && max_sat_nonmaxsig && max_elems};
5363
assert(is_input_size_info_set || is_nontop_or_nonsolvable);
5464
}
@@ -77,7 +87,12 @@ FUZZ_TARGET(mocked_descriptor_parse, .init = initialize_mocked_descriptor_parse)
7787
FlatSigningProvider signing_provider;
7888
std::string error;
7989
const auto desc = Parse(*descriptor, signing_provider, error);
80-
if (desc) TestDescriptor(*desc, signing_provider, error);
90+
std::optional<bool> is_ranged;
91+
std::optional<bool> is_solvable;
92+
for (const auto& d : desc) {
93+
assert(d);
94+
TestDescriptor(*d, signing_provider, error, is_ranged, is_solvable);
95+
}
8196
}
8297
}
8398

@@ -91,6 +106,11 @@ FUZZ_TARGET(descriptor_parse, .init = initialize_descriptor_parse)
91106
std::string error;
92107
for (const bool require_checksum : {true, false}) {
93108
const auto desc = Parse(descriptor, signing_provider, error, require_checksum);
94-
if (desc) TestDescriptor(*desc, signing_provider, error);
109+
std::optional<bool> is_ranged;
110+
std::optional<bool> is_solvable;
111+
for (const auto& d : desc) {
112+
assert(d);
113+
TestDescriptor(*d, signing_provider, error, is_ranged, is_solvable);
114+
}
95115
}
96116
}

src/wallet/rpc/backup.cpp

+6-4
Original file line numberDiff line numberDiff line change
@@ -1061,10 +1061,11 @@ static UniValue ProcessImportDescriptor(ImportData& import_data, std::map<CKeyID
10611061
const std::string& descriptor = data["desc"].get_str();
10621062
FlatSigningProvider keys;
10631063
std::string error;
1064-
auto parsed_desc = Parse(descriptor, keys, error, /* require_checksum = */ true);
1065-
if (!parsed_desc) {
1064+
auto parsed_descs = Parse(descriptor, keys, error, /* require_checksum = */ true);
1065+
if (parsed_descs.empty()) {
10661066
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, error);
10671067
}
1068+
const auto& parsed_desc = parsed_descs.at(0);
10681069
if (parsed_desc->GetOutputType() == OutputType::BECH32M) {
10691070
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Bech32m descriptors cannot be imported into legacy wallets");
10701071
}
@@ -1452,10 +1453,11 @@ static UniValue ProcessDescriptorImport(CWallet& wallet, const UniValue& data, c
14521453
// Parse descriptor string
14531454
FlatSigningProvider keys;
14541455
std::string error;
1455-
auto parsed_desc = Parse(descriptor, keys, error, /* require_checksum = */ true);
1456-
if (!parsed_desc) {
1456+
auto parsed_descs = Parse(descriptor, keys, error, /* require_checksum = */ true);
1457+
if (parsed_descs.empty()) {
14571458
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, error);
14581459
}
1460+
auto& parsed_desc = parsed_descs.at(0);
14591461

14601462
// Range check
14611463
int64_t range_start = 0, range_end = 1, next_index = 0;

src/wallet/rpc/spend.cpp

+5-3
Original file line numberDiff line numberDiff line change
@@ -660,11 +660,13 @@ CreatedTransactionResult FundTransaction(CWallet& wallet, const CMutableTransact
660660
FlatSigningProvider desc_out;
661661
std::string error;
662662
std::vector<CScript> scripts_temp;
663-
std::unique_ptr<Descriptor> desc = Parse(desc_str, desc_out, error, true);
664-
if (!desc) {
663+
auto descs = Parse(desc_str, desc_out, error, true);
664+
if (descs.empty()) {
665665
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Unable to parse descriptor '%s': %s", desc_str, error));
666666
}
667-
desc->Expand(0, desc_out, scripts_temp, desc_out);
667+
for (auto& desc : descs) {
668+
desc->Expand(0, desc_out, scripts_temp, desc_out);
669+
}
668670
coinControl.m_external_provider.Merge(std::move(desc_out));
669671
}
670672
}

0 commit comments

Comments
 (0)