Skip to content

Commit f93d555

Browse files
committed
Merge bitcoin#22838: descriptors: Be able to specify change and receiving in a single descriptor string
a0abcbd doc: Mention multipath specifier (Ava Chow) 0019f61 tests: Test importing of multipath descriptors (Ava Chow) f97d5c1 wallet, rpc: Allow importdescriptors to import multipath descriptors (Ava Chow) 32dcbca rpc: Allow importmulti to import multipath descriptors correctly (Ava Chow) 64dfe3c wallet: Move internal to be per key when importing (Ava Chow) 1692245 tests: Multipath descriptors for scantxoutset and deriveaddresses (Ava Chow) cddc0ba rpc: Have deriveaddresses derive receiving and change (Ava Chow) 360456c tests: Multipath descriptors for getdescriptorinfo (Ava Chow) a90eee4 tests: Add unit tests for multipath descriptors (Ava Chow) 1bbf46e descriptors: Change Parse to return vector of descriptors (Ava Chow) 0d640c6 descriptors: Have ParseKeypath handle multipath specifiers (Ava Chow) a5f39b1 descriptors: Change ParseScript to return vector of descriptors (Ava Chow) 0d55dea descriptors: Add DescriptorImpl::Clone (Ava Chow) 7e86541 descriptors: 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 bitcoin#17190 (comment), 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 bitcoin#17190 ACKs for top commit: darosior: re-ACK a0abcbd mjdietzx: reACK a0abcbd pythcoiner: reACK a0abcbd furszy: Code review ACK a0abcbd glozow: light code review ACK a0abcbd Tree-SHA512: 84ea40b3fd1b762194acd021cae018c2f09b98e595f5e87de5c832c265cfe8a6d0bc4dae25785392fa90db0f6301ddf9aea787980a29c74f81d04b711ac446c2
2 parents f175a73 + a0abcbd commit f93d555

30 files changed

+1242
-362
lines changed

doc/descriptors.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ Descriptors consist of several types of expressions. The top level expression is
9898
- [WIF](https://en.bitcoin.it/wiki/Wallet_import_format) encoded private keys may be specified instead of the corresponding public key, with the same meaning.
9999
- `xpub` encoded extended public key or `xprv` encoded extended private key (as defined in [BIP 32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)).
100100
- Followed by zero or more `/NUM` unhardened and `/NUM'` hardened BIP32 derivation steps.
101+
- No more than one of these derivation steps may be of the form `<NUM;NUM;...;NUM>` (including hardened indicators with either or both `NUM`). If such specifiers are included, the descriptor will be parsed as multiple descriptors where the first descriptor uses all of the first `NUM` in the pair, and the second descriptor uses the second `NUM` in the pair for all `KEY` expressions, and so on.
101102
- Optionally followed by a single `/*` or `/*'` final step to denote all (direct) unhardened or hardened children.
102103
- The usage of hardened derivation steps requires providing the private key.
103104

@@ -257,6 +258,30 @@ Note how the first key is an xprv private key with a specific derivation path,
257258
while the other two are public keys.
258259

259260

261+
### Specifying receiving and change descriptors in one descriptor
262+
263+
Since receiving and change addresses are frequently derived from the same
264+
extended key(s) but with a single derivation index changed, it is convenient
265+
to be able to specify a descriptor that can derive at the two different
266+
indexes. Thus a single tuple of indexes is allowed in each derivation path
267+
following the extended key. When this descriptor is parsed, multiple descriptors
268+
will be produced, the first one will use the first index in the tuple for all
269+
key expressions, the second will use the second index, the third will use the
270+
third index, and so on..
271+
272+
For example, a descriptor of the form:
273+
274+
multi(2,xpub.../<0;1;2>/0/*,xpub.../<2;3;4>/*)
275+
276+
will expand to the two descriptors
277+
278+
multi(2,xpub.../0/0/*,xpub.../2/*)
279+
multi(2,xpub.../1/0/*,xpub.../3/*)
280+
multi(2,xpub.../2/0/*,xpub.../4*)
281+
282+
When this tuple contains only two elements, wallet implementations can use the
283+
first descriptor for receiving addresses and the second descriptor for change addresses.
284+
260285
### Compatibility with old wallets
261286

262287
In order to easily represent the sets of scripts currently supported by

src/bench/descriptors.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ static void ExpandDescriptor(benchmark::Bench& bench)
2323
const std::pair<int64_t, int64_t> range = {0, 1000};
2424
FlatSigningProvider provider;
2525
std::string error;
26-
auto desc = Parse(desc_str, provider, error);
26+
auto descs = Parse(desc_str, provider, error);
2727

2828
bench.run([&] {
2929
for (int i = range.first; i <= range.second; ++i) {
3030
std::vector<CScript> scripts;
31-
bool success = desc->Expand(i, provider, scripts, provider);
31+
bool success = descs[0]->Expand(i, provider, scripts, provider);
3232
assert(success);
3333
}
3434
});

src/bench/wallet_ismine.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ static void WalletIsMine(benchmark::Bench& bench, bool legacy_wallet, int num_co
5252
key.MakeNewKey(/*fCompressed=*/true);
5353
FlatSigningProvider keys;
5454
std::string error;
55-
std::unique_ptr<Descriptor> desc = Parse("combo(" + EncodeSecret(key) + ")", keys, error, /*require_checksum=*/false);
56-
WalletDescriptor w_desc(std::move(desc), /*creation_time=*/0, /*range_start=*/0, /*range_end=*/0, /*next_index=*/0);
55+
std::vector<std::unique_ptr<Descriptor>> desc = Parse("combo(" + EncodeSecret(key) + ")", keys, error, /*require_checksum=*/false);
56+
WalletDescriptor w_desc(std::move(desc.at(0)), /*creation_time=*/0, /*range_start=*/0, /*range_end=*/0, /*next_index=*/0);
5757
auto spkm = wallet->AddWalletDescriptor(w_desc, keys, /*label=*/"", /*internal=*/false);
5858
assert(spkm);
5959
}

src/qt/test/wallettests.cpp

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ std::shared_ptr<CWallet> SetupLegacyWatchOnlyWallet(interfaces::Node& node, Test
199199
wallet->SetupLegacyScriptPubKeyMan();
200200
// Add watched key
201201
CPubKey pubKey = test.coinbaseKey.GetPubKey();
202-
bool import_keys = wallet->ImportPubKeys({pubKey.GetID()}, {{pubKey.GetID(), pubKey}} , /*key_origins=*/{}, /*add_keypool=*/false, /*internal=*/false, /*timestamp=*/1);
202+
bool import_keys = wallet->ImportPubKeys({{pubKey.GetID(), false}}, {{pubKey.GetID(), pubKey}} , /*key_origins=*/{}, /*add_keypool=*/false, /*timestamp=*/1);
203203
assert(import_keys);
204204
wallet->SetLastBlockProcessed(105, WITH_LOCK(node.context()->chainman->GetMutex(), return node.context()->chainman->ActiveChain().Tip()->GetBlockHash()));
205205
}
@@ -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

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

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

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

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

215216
static RPCHelpMan generatetodescriptor()

src/rpc/output_script.cpp

Lines changed: 82 additions & 40 deletions
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,22 +195,65 @@ 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
},
207220
};
208221
}
209222

223+
static UniValue DeriveAddresses(const Descriptor* desc, int64_t range_begin, int64_t range_end, FlatSigningProvider& key_provider)
224+
{
225+
UniValue addresses(UniValue::VARR);
226+
227+
for (int64_t i = range_begin; i <= range_end; ++i) {
228+
FlatSigningProvider provider;
229+
std::vector<CScript> scripts;
230+
if (!desc->Expand(i, key_provider, scripts, provider)) {
231+
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Cannot derive script without private keys");
232+
}
233+
234+
for (const CScript& script : scripts) {
235+
CTxDestination dest;
236+
if (!ExtractDestination(script, dest)) {
237+
// ExtractDestination no longer returns true for P2PK since it doesn't have a corresponding address
238+
// However combo will output P2PK and should just ignore that script
239+
if (scripts.size() > 1 && std::get_if<PubKeyDestination>(&dest)) {
240+
continue;
241+
}
242+
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Descriptor does not have a corresponding address");
243+
}
244+
245+
addresses.push_back(EncodeDestination(dest));
246+
}
247+
}
248+
249+
// This should not be possible, but an assert seems overkill:
250+
if (addresses.empty()) {
251+
throw JSONRPCError(RPC_MISC_ERROR, "Unexpected empty result");
252+
}
253+
254+
return addresses;
255+
}
256+
210257
static RPCHelpMan deriveaddresses()
211258
{
212259
const std::string EXAMPLE_DESCRIPTOR = "wpkh([d34db33f/84h/0h/0h]xpub6DJ2dNUysrn5Vt36jH2KLBT2i1auw1tTSSomg8PhqNiUtx8QX2SvC9nrHu81fT41fvDUnhMjEzQgXnQjKEu3oaqMSzhSrHMxyyoEAmUHQbY/0/*)#cjjspncu";
@@ -226,11 +273,24 @@ static RPCHelpMan deriveaddresses()
226273
{"descriptor", RPCArg::Type::STR, RPCArg::Optional::NO, "The descriptor."},
227274
{"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."},
228275
},
229-
RPCResult{
230-
RPCResult::Type::ARR, "", "",
231-
{
232-
{RPCResult::Type::STR, "address", "the derived addresses"},
233-
}
276+
{
277+
RPCResult{"for single derivation descriptors",
278+
RPCResult::Type::ARR, "", "",
279+
{
280+
{RPCResult::Type::STR, "address", "the derived addresses"},
281+
}
282+
},
283+
RPCResult{"for multipath descriptors",
284+
RPCResult::Type::ARR, "", "The derived addresses for each of the multipath expansions of the descriptor, in multipath specifier order",
285+
{
286+
{
287+
RPCResult::Type::ARR, "", "The derived addresses for a multipath descriptor expansion",
288+
{
289+
{RPCResult::Type::STR, "address", "the derived address"},
290+
},
291+
},
292+
},
293+
},
234294
},
235295
RPCExamples{
236296
"First three native segwit receive addresses\n" +
@@ -250,11 +310,11 @@ static RPCHelpMan deriveaddresses()
250310

251311
FlatSigningProvider key_provider;
252312
std::string error;
253-
auto desc = Parse(desc_str, key_provider, error, /* require_checksum = */ true);
254-
if (!desc) {
313+
auto descs = Parse(desc_str, key_provider, error, /* require_checksum = */ true);
314+
if (descs.empty()) {
255315
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, error);
256316
}
257-
317+
auto& desc = descs.at(0);
258318
if (!desc->IsRange() && request.params.size() > 1) {
259319
throw JSONRPCError(RPC_INVALID_PARAMETER, "Range should not be specified for an un-ranged descriptor");
260320
}
@@ -263,36 +323,18 @@ static RPCHelpMan deriveaddresses()
263323
throw JSONRPCError(RPC_INVALID_PARAMETER, "Range must be specified for a ranged descriptor");
264324
}
265325

266-
UniValue addresses(UniValue::VARR);
267-
268-
for (int64_t i = range_begin; i <= range_end; ++i) {
269-
FlatSigningProvider provider;
270-
std::vector<CScript> scripts;
271-
if (!desc->Expand(i, key_provider, scripts, provider)) {
272-
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Cannot derive script without private keys");
273-
}
326+
UniValue addresses = DeriveAddresses(desc.get(), range_begin, range_end, key_provider);
274327

275-
for (const CScript& script : scripts) {
276-
CTxDestination dest;
277-
if (!ExtractDestination(script, dest)) {
278-
// ExtractDestination no longer returns true for P2PK since it doesn't have a corresponding address
279-
// However combo will output P2PK and should just ignore that script
280-
if (scripts.size() > 1 && std::get_if<PubKeyDestination>(&dest)) {
281-
continue;
282-
}
283-
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Descriptor does not have a corresponding address");
284-
}
285-
286-
addresses.push_back(EncodeDestination(dest));
287-
}
328+
if (descs.size() == 1) {
329+
return addresses;
288330
}
289331

290-
// This should not be possible, but an assert seems overkill:
291-
if (addresses.empty()) {
292-
throw JSONRPCError(RPC_MISC_ERROR, "Unexpected empty result");
332+
UniValue ret(UniValue::VARR);
333+
ret.push_back(addresses);
334+
for (size_t i = 1; i < descs.size(); ++i) {
335+
ret.push_back(DeriveAddresses(descs.at(i).get(), range_begin, range_end, key_provider));
293336
}
294-
295-
return addresses;
337+
return ret;
296338
},
297339
};
298340
}

src/rpc/util.cpp

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1345,24 +1345,26 @@ std::vector<CScript> EvalDescriptorStringOrObject(const UniValue& scanobject, Fl
13451345
}
13461346

13471347
std::string error;
1348-
auto desc = Parse(desc_str, provider, error);
1349-
if (!desc) {
1348+
auto descs = Parse(desc_str, provider, error);
1349+
if (descs.empty()) {
13501350
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, error);
13511351
}
1352-
if (!desc->IsRange()) {
1352+
if (!descs.at(0)->IsRange()) {
13531353
range.first = 0;
13541354
range.second = 0;
13551355
}
13561356
std::vector<CScript> ret;
13571357
for (int i = range.first; i <= range.second; ++i) {
1358-
std::vector<CScript> scripts;
1359-
if (!desc->Expand(i, provider, scripts, provider)) {
1360-
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Cannot derive script without private keys: '%s'", desc_str));
1361-
}
1362-
if (expand_priv) {
1363-
desc->ExpandPrivate(/*pos=*/i, provider, /*out=*/provider);
1358+
for (const auto& desc : descs) {
1359+
std::vector<CScript> scripts;
1360+
if (!desc->Expand(i, provider, scripts, provider)) {
1361+
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Cannot derive script without private keys: '%s'", desc_str));
1362+
}
1363+
if (expand_priv) {
1364+
desc->ExpandPrivate(/*pos=*/i, provider, /*out=*/provider);
1365+
}
1366+
std::move(scripts.begin(), scripts.end(), std::back_inserter(ret));
13641367
}
1365-
std::move(scripts.begin(), scripts.end(), std::back_inserter(ret));
13661368
}
13671369
return ret;
13681370
}

0 commit comments

Comments
 (0)