mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-01-19 23:03:45 +01:00
Merge bitcoin/bitcoin#22838: descriptors: Be able to specify change and receiving in a single descriptor string
a0abcbd382doc: Mention multipath specifier (Ava Chow)0019f61fc5tests: Test importing of multipath descriptors (Ava Chow)f97d5c137dwallet, rpc: Allow importdescriptors to import multipath descriptors (Ava Chow)32dcbca3fbrpc: Allow importmulti to import multipath descriptors correctly (Ava Chow)64dfe3ce4bwallet: Move internal to be per key when importing (Ava Chow)1692245525tests: Multipath descriptors for scantxoutset and deriveaddresses (Ava Chow)cddc0ba9a9rpc: Have deriveaddresses derive receiving and change (Ava Chow)360456cd22tests: Multipath descriptors for getdescriptorinfo (Ava Chow)a90eee444ctests: Add unit tests for multipath descriptors (Ava Chow)1bbf46e2dadescriptors: Change Parse to return vector of descriptors (Ava Chow)0d640c6f02descriptors: Have ParseKeypath handle multipath specifiers (Ava Chow)a5f39b1034descriptors: Change ParseScript to return vector of descriptors (Ava Chow)0d55deae15descriptors: Add DescriptorImpl::Clone (Ava Chow)7e86541f72descriptors: Add PubkeyProvider::Clone (Ava Chow) Pull request description: It is convenient to have a descriptor which specifies both receiving and change addresses in a single string. However, as discussed in https://github.com/bitcoin/bitcoin/issues/17190#issuecomment-895515768, it is not feasible to use a generic multipath specification like BIP 88 due to combinatorial blow up and that it would result in unexpected descriptors. To resolve that problem, this PR proposes a targeted solution which allows only a single pair of 2 derivation indexes to be inserted in the place of a single derivation index. So instead of two descriptor `wpkh(xpub.../0/0/*)` and `wpkh(xpub.../0/1/*)` to represent receive and change addresses, this could be written as `wpkh(xpub.../0/<0;1>/*)`. The multipath specifier is of the form `<NUM;NUM>`. Each `NUM` can have its own hardened specifier, e.g. `<0;1h>` is valid. The multipath specifier can also only appear in one path index in the derivation path. This results in the parser returning two descriptors. The first descriptor uses the first `NUM` in all pairs present, and the second uses the second `NUM`. In our implementation, if a multipath descriptor is not provided, a pair is still returned, but the second element is just `nullptr`. The wallet will not output the multipath descriptors (yet). Furthermore, when a multipath descriptor is imported, it is expanded to the two descriptors and each imported on its own, with the second descriptor being implicitly for internal (change) addresses. There is no change to how the wallet stores or outputs descriptors (yet). Note that the path specifier is different from what was proposed. It uses angle brackets and the semicolon because these are unused characters available in the character set and I wanted to avoid conflicts with characters already in use in descriptors. Closes #17190 ACKs for top commit: darosior: re-ACKa0abcbd382mjdietzx: reACKa0abcbd382pythcoiner: reACKa0abcbdfurszy: Code review ACKa0abcbdglozow: light code review ACKa0abcbd382Tree-SHA512: 84ea40b3fd1b762194acd021cae018c2f09b98e595f5e87de5c832c265cfe8a6d0bc4dae25785392fa90db0f6301ddf9aea787980a29c74f81d04b711ac446c2
This commit is contained in:
@@ -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"},
|
||||
@@ -191,22 +195,65 @@ 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;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
static UniValue DeriveAddresses(const Descriptor* desc, int64_t range_begin, int64_t range_end, FlatSigningProvider& key_provider)
|
||||
{
|
||||
UniValue addresses(UniValue::VARR);
|
||||
|
||||
for (int64_t i = range_begin; i <= range_end; ++i) {
|
||||
FlatSigningProvider provider;
|
||||
std::vector<CScript> scripts;
|
||||
if (!desc->Expand(i, key_provider, scripts, provider)) {
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Cannot derive script without private keys");
|
||||
}
|
||||
|
||||
for (const CScript& script : scripts) {
|
||||
CTxDestination dest;
|
||||
if (!ExtractDestination(script, dest)) {
|
||||
// ExtractDestination no longer returns true for P2PK since it doesn't have a corresponding address
|
||||
// However combo will output P2PK and should just ignore that script
|
||||
if (scripts.size() > 1 && std::get_if<PubKeyDestination>(&dest)) {
|
||||
continue;
|
||||
}
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Descriptor does not have a corresponding address");
|
||||
}
|
||||
|
||||
addresses.push_back(EncodeDestination(dest));
|
||||
}
|
||||
}
|
||||
|
||||
// This should not be possible, but an assert seems overkill:
|
||||
if (addresses.empty()) {
|
||||
throw JSONRPCError(RPC_MISC_ERROR, "Unexpected empty result");
|
||||
}
|
||||
|
||||
return addresses;
|
||||
}
|
||||
|
||||
static RPCHelpMan deriveaddresses()
|
||||
{
|
||||
const std::string EXAMPLE_DESCRIPTOR = "wpkh([d34db33f/84h/0h/0h]xpub6DJ2dNUysrn5Vt36jH2KLBT2i1auw1tTSSomg8PhqNiUtx8QX2SvC9nrHu81fT41fvDUnhMjEzQgXnQjKEu3oaqMSzhSrHMxyyoEAmUHQbY/0/*)#cjjspncu";
|
||||
@@ -226,11 +273,24 @@ static RPCHelpMan deriveaddresses()
|
||||
{"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."},
|
||||
},
|
||||
RPCResult{
|
||||
RPCResult::Type::ARR, "", "",
|
||||
{
|
||||
{RPCResult::Type::STR, "address", "the derived addresses"},
|
||||
}
|
||||
{
|
||||
RPCResult{"for single derivation descriptors",
|
||||
RPCResult::Type::ARR, "", "",
|
||||
{
|
||||
{RPCResult::Type::STR, "address", "the derived addresses"},
|
||||
}
|
||||
},
|
||||
RPCResult{"for multipath descriptors",
|
||||
RPCResult::Type::ARR, "", "The derived addresses for each of the multipath expansions of the descriptor, in multipath specifier order",
|
||||
{
|
||||
{
|
||||
RPCResult::Type::ARR, "", "The derived addresses for a multipath descriptor expansion",
|
||||
{
|
||||
{RPCResult::Type::STR, "address", "the derived address"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
RPCExamples{
|
||||
"First three native segwit receive addresses\n" +
|
||||
@@ -250,11 +310,11 @@ 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);
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
@@ -263,36 +323,18 @@ static RPCHelpMan deriveaddresses()
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Range must be specified for a ranged descriptor");
|
||||
}
|
||||
|
||||
UniValue addresses(UniValue::VARR);
|
||||
UniValue addresses = DeriveAddresses(desc.get(), range_begin, range_end, key_provider);
|
||||
|
||||
for (int64_t i = range_begin; i <= range_end; ++i) {
|
||||
FlatSigningProvider provider;
|
||||
std::vector<CScript> scripts;
|
||||
if (!desc->Expand(i, key_provider, scripts, provider)) {
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Cannot derive script without private keys");
|
||||
}
|
||||
|
||||
for (const CScript& script : scripts) {
|
||||
CTxDestination dest;
|
||||
if (!ExtractDestination(script, dest)) {
|
||||
// ExtractDestination no longer returns true for P2PK since it doesn't have a corresponding address
|
||||
// However combo will output P2PK and should just ignore that script
|
||||
if (scripts.size() > 1 && std::get_if<PubKeyDestination>(&dest)) {
|
||||
continue;
|
||||
}
|
||||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Descriptor does not have a corresponding address");
|
||||
}
|
||||
|
||||
addresses.push_back(EncodeDestination(dest));
|
||||
}
|
||||
if (descs.size() == 1) {
|
||||
return addresses;
|
||||
}
|
||||
|
||||
// This should not be possible, but an assert seems overkill:
|
||||
if (addresses.empty()) {
|
||||
throw JSONRPCError(RPC_MISC_ERROR, "Unexpected empty result");
|
||||
UniValue ret(UniValue::VARR);
|
||||
ret.push_back(addresses);
|
||||
for (size_t i = 1; i < descs.size(); ++i) {
|
||||
ret.push_back(DeriveAddresses(descs.at(i).get(), range_begin, range_end, key_provider));
|
||||
}
|
||||
|
||||
return addresses;
|
||||
return ret;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user