Skip to content

Commit

Permalink
descriptors: Change Parse to return vector of descriptors
Browse files Browse the repository at this point in the history
When given a descriptor which contins a multipath derivation specifier,
a vector of descriptors will be returned.
  • Loading branch information
achow101 committed Aug 8, 2024
1 parent 0d640c6 commit 1bbf46e
Show file tree
Hide file tree
Showing 22 changed files with 174 additions and 107 deletions.
4 changes: 2 additions & 2 deletions src/bench/descriptors.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ static void ExpandDescriptor(benchmark::Bench& bench)
const std::pair<int64_t, int64_t> range = {0, 1000};
FlatSigningProvider provider;
std::string error;
auto desc = Parse(desc_str, provider, error);
auto descs = Parse(desc_str, provider, error);

bench.run([&] {
for (int i = range.first; i <= range.second; ++i) {
std::vector<CScript> scripts;
bool success = desc->Expand(i, provider, scripts, provider);
bool success = descs[0]->Expand(i, provider, scripts, provider);
assert(success);
}
});
Expand Down
4 changes: 2 additions & 2 deletions src/bench/wallet_ismine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ static void WalletIsMine(benchmark::Bench& bench, bool legacy_wallet, int num_co
key.MakeNewKey(/*fCompressed=*/true);
FlatSigningProvider keys;
std::string error;
std::unique_ptr<Descriptor> desc = Parse("combo(" + EncodeSecret(key) + ")", keys, error, /*require_checksum=*/false);
WalletDescriptor w_desc(std::move(desc), /*creation_time=*/0, /*range_start=*/0, /*range_end=*/0, /*next_index=*/0);
std::vector<std::unique_ptr<Descriptor>> desc = Parse("combo(" + EncodeSecret(key) + ")", keys, error, /*require_checksum=*/false);
WalletDescriptor w_desc(std::move(desc.at(0)), /*creation_time=*/0, /*range_start=*/0, /*range_end=*/0, /*next_index=*/0);
auto spkm = wallet->AddWalletDescriptor(w_desc, keys, /*label=*/"", /*internal=*/false);
assert(spkm);
}
Expand Down
6 changes: 4 additions & 2 deletions src/qt/test/wallettests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,10 @@ std::shared_ptr<CWallet> SetupDescriptorsWallet(interfaces::Node& node, TestChai
// Add the coinbase key
FlatSigningProvider provider;
std::string error;
std::unique_ptr<Descriptor> desc = Parse("combo(" + EncodeSecret(test.coinbaseKey) + ")", provider, error, /* require_checksum=*/ false);
assert(desc);
auto descs = Parse("combo(" + EncodeSecret(test.coinbaseKey) + ")", provider, error, /* require_checksum=*/ false);
assert(!descs.empty());
assert(descs.size() == 1);
auto& desc = descs.at(0);
WalletDescriptor w_desc(std::move(desc), 0, 0, 1, 1);
if (!wallet->AddWalletDescriptor(w_desc, provider, "", false)) assert(false);
CTxDestination dest = GetDestinationForKey(test.coinbaseKey.GetPubKey(), wallet->m_default_address_type);
Expand Down
49 changes: 25 additions & 24 deletions src/rpc/mining.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -180,35 +180,36 @@ static UniValue generateBlocks(ChainstateManager& chainman, Mining& miner, const
static bool getScriptFromDescriptor(const std::string& descriptor, CScript& script, std::string& error)
{
FlatSigningProvider key_provider;
const auto desc = Parse(descriptor, key_provider, error, /* require_checksum = */ false);
if (desc) {
if (desc->IsRange()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Ranged descriptor not accepted. Maybe pass through deriveaddresses first?");
}

FlatSigningProvider provider;
std::vector<CScript> scripts;
if (!desc->Expand(0, key_provider, scripts, provider)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Cannot derive script without private keys");
}
const auto descs = Parse(descriptor, key_provider, error, /* require_checksum = */ false);
if (descs.empty()) return false;
if (descs.size() > 1) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Multipath descriptor not accepted");
}
const auto& desc = descs.at(0);
if (desc->IsRange()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Ranged descriptor not accepted. Maybe pass through deriveaddresses first?");
}

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

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

return true;
if (scripts.size() == 1) {
script = scripts.at(0);
} else if (scripts.size() == 4) {
// For uncompressed keys, take the 3rd script, since it is p2wpkh
script = scripts.at(2);
} else {
return false;
// Else take the 2nd script, since it is p2pkh
script = scripts.at(1);
}

return true;
}

static RPCHelpMan generatetodescriptor()
Expand Down
37 changes: 27 additions & 10 deletions src/rpc/output_script.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,11 @@ static RPCHelpMan getdescriptorinfo()
RPCResult{
RPCResult::Type::OBJ, "", "",
{
{RPCResult::Type::STR, "descriptor", "The descriptor in canonical form, without private keys"},
{RPCResult::Type::STR, "descriptor", "The descriptor in canonical form, without private keys. For a multipath descriptor, only the first will be returned."},
{RPCResult::Type::ARR, "multipath_expansion", /*optional=*/true, "All descriptors produced by expanding multipath derivation elements. Only if the provided descriptor specifies multipath derivation elements.",
{
{RPCResult::Type::STR, "", ""},
}},
{RPCResult::Type::STR, "checksum", "The checksum for the input descriptor"},
{RPCResult::Type::BOOL, "isrange", "Whether the descriptor is ranged"},
{RPCResult::Type::BOOL, "issolvable", "Whether the descriptor is solvable"},
Expand All @@ -191,16 +195,25 @@ static RPCHelpMan getdescriptorinfo()
{
FlatSigningProvider provider;
std::string error;
auto desc = Parse(request.params[0].get_str(), provider, error);
if (!desc) {
auto descs = Parse(request.params[0].get_str(), provider, error);
if (descs.empty()) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, error);
}

UniValue result(UniValue::VOBJ);
result.pushKV("descriptor", desc->ToString());
result.pushKV("descriptor", descs.at(0)->ToString());

if (descs.size() > 1) {
UniValue multipath_descs(UniValue::VARR);
for (const auto& d : descs) {
multipath_descs.push_back(d->ToString());
}
result.pushKV("multipath_expansion", multipath_descs);
}

result.pushKV("checksum", GetDescriptorChecksum(request.params[0].get_str()));
result.pushKV("isrange", desc->IsRange());
result.pushKV("issolvable", desc->IsSolvable());
result.pushKV("isrange", descs.at(0)->IsRange());
result.pushKV("issolvable", descs.at(0)->IsSolvable());
result.pushKV("hasprivatekeys", provider.keys.size() > 0);
return result;
},
Expand All @@ -221,7 +234,8 @@ static RPCHelpMan deriveaddresses()
" tr(<pubkey>,multi_a(<n>,<pubkey>,<pubkey>,...)) P2TR-multisig outputs for the given threshold and pubkeys\n"
"\nIn the above, <pubkey> either refers to a fixed public key in hexadecimal notation, or to an xpub/xprv optionally followed by one\n"
"or more path elements separated by \"/\", where \"h\" represents a hardened child key.\n"
"For more information on output descriptors, see the documentation in the doc/descriptors.md file.\n"},
"For more information on output descriptors, see the documentation in the doc/descriptors.md file.\n"
"Note that only descriptors that specify a single derivation path can be derived.\n"},
{
{"descriptor", RPCArg::Type::STR, RPCArg::Optional::NO, "The descriptor."},
{"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."},
Expand Down Expand Up @@ -250,11 +264,14 @@ static RPCHelpMan deriveaddresses()

FlatSigningProvider key_provider;
std::string error;
auto desc = Parse(desc_str, key_provider, error, /* require_checksum = */ true);
if (!desc) {
auto descs = Parse(desc_str, key_provider, error, /* require_checksum = */ true);
if (descs.empty()) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, error);
}

if (descs.size() > 1) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Descriptor with multipath derivation path specifiers are not allowed");
}
auto& desc = descs.at(0);
if (!desc->IsRange() && request.params.size() > 1) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Range should not be specified for an un-ranged descriptor");
}
Expand Down
22 changes: 12 additions & 10 deletions src/rpc/util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1337,24 +1337,26 @@ std::vector<CScript> EvalDescriptorStringOrObject(const UniValue& scanobject, Fl
}

std::string error;
auto desc = Parse(desc_str, provider, error);
if (!desc) {
auto descs = Parse(desc_str, provider, error);
if (descs.empty()) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, error);
}
if (!desc->IsRange()) {
if (!descs.at(0)->IsRange()) {
range.first = 0;
range.second = 0;
}
std::vector<CScript> ret;
for (int i = range.first; i <= range.second; ++i) {
std::vector<CScript> scripts;
if (!desc->Expand(i, provider, scripts, provider)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Cannot derive script without private keys: '%s'", desc_str));
}
if (expand_priv) {
desc->ExpandPrivate(/*pos=*/i, provider, /*out=*/provider);
for (const auto& desc : descs) {
std::vector<CScript> scripts;
if (!desc->Expand(i, provider, scripts, provider)) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Cannot derive script without private keys: '%s'", desc_str));
}
if (expand_priv) {
desc->ExpandPrivate(/*pos=*/i, provider, /*out=*/provider);
}
std::move(scripts.begin(), scripts.end(), std::back_inserter(ret));
}
std::move(scripts.begin(), scripts.end(), std::back_inserter(ret));
}
return ret;
}
Expand Down
15 changes: 11 additions & 4 deletions src/script/descriptor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2364,14 +2364,21 @@ bool CheckChecksum(Span<const char>& sp, bool require_checksum, std::string& err
return true;
}

std::unique_ptr<Descriptor> Parse(const std::string& descriptor, FlatSigningProvider& out, std::string& error, bool require_checksum)
std::vector<std::unique_ptr<Descriptor>> Parse(const std::string& descriptor, FlatSigningProvider& out, std::string& error, bool require_checksum)
{
Span<const char> sp{descriptor};
if (!CheckChecksum(sp, require_checksum, error)) return nullptr;
if (!CheckChecksum(sp, require_checksum, error)) return {};
uint32_t key_exp_index = 0;
auto ret = ParseScript(key_exp_index, sp, ParseScriptContext::TOP, out, error);
if (sp.size() == 0 && !ret.empty()) return std::unique_ptr<Descriptor>(std::move(ret.at(0)));
return nullptr;
if (sp.size() == 0 && !ret.empty()) {
std::vector<std::unique_ptr<Descriptor>> descs;
descs.reserve(ret.size());
for (auto& r : ret) {
descs.emplace_back(std::unique_ptr<Descriptor>(std::move(r)));
}
return descs;
}
return {};
}

std::string GetDescriptorChecksum(const std::string& descriptor)
Expand Down
4 changes: 2 additions & 2 deletions src/script/descriptor.h
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,9 @@ struct Descriptor {
* is set, the checksum is mandatory - otherwise it is optional.
*
* If a parse error occurs, or the checksum is missing/invalid, or anything
* else is wrong, `nullptr` is returned.
* else is wrong, an empty vector is returned.
*/
std::unique_ptr<Descriptor> Parse(const std::string& descriptor, FlatSigningProvider& out, std::string& error, bool require_checksum = false);
std::vector<std::unique_ptr<Descriptor>> Parse(const std::string& descriptor, FlatSigningProvider& out, std::string& error, bool require_checksum = false);

/** Get the checksum for a `descriptor`.
*
Expand Down
19 changes: 11 additions & 8 deletions src/test/descriptor_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ void CheckUnparsable(const std::string& prv, const std::string& pub, const std::
std::string error;
auto parse_priv = Parse(prv, keys_priv, error);
auto parse_pub = Parse(pub, keys_pub, error);
BOOST_CHECK_MESSAGE(!parse_priv, prv);
BOOST_CHECK_MESSAGE(!parse_pub, pub);
BOOST_CHECK_MESSAGE(parse_priv.empty(), prv);
BOOST_CHECK_MESSAGE(parse_pub.empty(), pub);
BOOST_CHECK_EQUAL(error, expected_error);
}

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

std::unique_ptr<Descriptor> parse_priv;
std::unique_ptr<Descriptor> parse_pub;
std::vector<std::unique_ptr<Descriptor>> parse_privs;
std::vector<std::unique_ptr<Descriptor>> parse_pubs;
// Check that parsing succeeds.
if (replace_apostrophe_with_h_in_prv) {
prv = UseHInsteadOfApostrophe(prv);
}
parse_priv = Parse(prv, keys_priv, error);
BOOST_CHECK_MESSAGE(parse_priv, error);
parse_privs = Parse(prv, keys_priv, error);
BOOST_CHECK_MESSAGE(!parse_privs.empty(), error);
if (replace_apostrophe_with_h_in_pub) {
pub = UseHInsteadOfApostrophe(pub);
}
parse_pub = Parse(pub, keys_pub, error);
BOOST_CHECK_MESSAGE(parse_pub, error);
parse_pubs = Parse(pub, keys_pub, error);
BOOST_CHECK_MESSAGE(!parse_pubs.empty(), error);

auto& parse_priv = parse_privs.at(0);
auto& parse_pub = parse_pubs.at(0);

// We must be able to estimate the max satisfaction size for any solvable descriptor top descriptor (but combo).
const bool is_nontop_or_nonsolvable{!parse_priv->IsSolvable() || !parse_priv->GetOutputType()};
Expand Down
30 changes: 25 additions & 5 deletions src/test/fuzz/descriptor_parse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,24 @@
MockedDescriptorConverter MOCKED_DESC_CONVERTER;

/** Test a successfully parsed descriptor. */
static void TestDescriptor(const Descriptor& desc, FlatSigningProvider& sig_provider, std::string& dummy)
static void TestDescriptor(const Descriptor& desc, FlatSigningProvider& sig_provider, std::string& dummy, std::optional<bool>& is_ranged, std::optional<bool>& is_solvable)
{
// Trivial helpers.
(void)desc.IsRange();
const bool is_solvable{desc.IsSolvable()};
(void)desc.IsSingleType();
(void)desc.GetOutputType();

if (is_ranged.has_value()) {
assert(desc.IsRange() == *is_ranged);
} else {
is_ranged = desc.IsRange();
}
if (is_solvable.has_value()) {
assert(desc.IsSolvable() == *is_solvable);
} else {
is_solvable = desc.IsSolvable();
}

// Serialization to string representation.
(void)desc.ToString();
(void)desc.ToPrivateString(sig_provider, dummy);
Expand All @@ -48,7 +58,7 @@ static void TestDescriptor(const Descriptor& desc, FlatSigningProvider& sig_prov
const auto max_sat_nonmaxsig{desc.MaxSatisfactionWeight(true)};
const auto max_elems{desc.MaxSatisfactionElems()};
// We must be able to estimate the max satisfaction size for any solvable descriptor (but combo).
const bool is_nontop_or_nonsolvable{!is_solvable || !desc.GetOutputType()};
const bool is_nontop_or_nonsolvable{!*is_solvable || !desc.GetOutputType()};
const bool is_input_size_info_set{max_sat_maxsig && max_sat_nonmaxsig && max_elems};
assert(is_input_size_info_set || is_nontop_or_nonsolvable);
}
Expand Down Expand Up @@ -77,7 +87,12 @@ FUZZ_TARGET(mocked_descriptor_parse, .init = initialize_mocked_descriptor_parse)
FlatSigningProvider signing_provider;
std::string error;
const auto desc = Parse(*descriptor, signing_provider, error);
if (desc) TestDescriptor(*desc, signing_provider, error);
std::optional<bool> is_ranged;
std::optional<bool> is_solvable;
for (const auto& d : desc) {
assert(d);
TestDescriptor(*d, signing_provider, error, is_ranged, is_solvable);
}
}
}

Expand All @@ -91,6 +106,11 @@ FUZZ_TARGET(descriptor_parse, .init = initialize_descriptor_parse)
std::string error;
for (const bool require_checksum : {true, false}) {
const auto desc = Parse(descriptor, signing_provider, error, require_checksum);
if (desc) TestDescriptor(*desc, signing_provider, error);
std::optional<bool> is_ranged;
std::optional<bool> is_solvable;
for (const auto& d : desc) {
assert(d);
TestDescriptor(*d, signing_provider, error, is_ranged, is_solvable);
}
}
}
10 changes: 6 additions & 4 deletions src/wallet/rpc/backup.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1061,10 +1061,11 @@ static UniValue ProcessImportDescriptor(ImportData& import_data, std::map<CKeyID
const std::string& descriptor = data["desc"].get_str();
FlatSigningProvider keys;
std::string error;
auto parsed_desc = Parse(descriptor, keys, error, /* require_checksum = */ true);
if (!parsed_desc) {
auto parsed_descs = Parse(descriptor, keys, error, /* require_checksum = */ true);
if (parsed_descs.empty()) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, error);
}
const auto& parsed_desc = parsed_descs.at(0);
if (parsed_desc->GetOutputType() == OutputType::BECH32M) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Bech32m descriptors cannot be imported into legacy wallets");
}
Expand Down Expand Up @@ -1452,10 +1453,11 @@ static UniValue ProcessDescriptorImport(CWallet& wallet, const UniValue& data, c
// Parse descriptor string
FlatSigningProvider keys;
std::string error;
auto parsed_desc = Parse(descriptor, keys, error, /* require_checksum = */ true);
if (!parsed_desc) {
auto parsed_descs = Parse(descriptor, keys, error, /* require_checksum = */ true);
if (parsed_descs.empty()) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, error);
}
auto& parsed_desc = parsed_descs.at(0);

// Range check
int64_t range_start = 0, range_end = 1, next_index = 0;
Expand Down
8 changes: 5 additions & 3 deletions src/wallet/rpc/spend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -660,11 +660,13 @@ CreatedTransactionResult FundTransaction(CWallet& wallet, const CMutableTransact
FlatSigningProvider desc_out;
std::string error;
std::vector<CScript> scripts_temp;
std::unique_ptr<Descriptor> desc = Parse(desc_str, desc_out, error, true);
if (!desc) {
auto descs = Parse(desc_str, desc_out, error, true);
if (descs.empty()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Unable to parse descriptor '%s': %s", desc_str, error));
}
desc->Expand(0, desc_out, scripts_temp, desc_out);
for (auto& desc : descs) {
desc->Expand(0, desc_out, scripts_temp, desc_out);
}
coinControl.m_external_provider.Merge(std::move(desc_out));
}
}
Expand Down
Loading

0 comments on commit 1bbf46e

Please sign in to comment.