Skip to content

Commit 5d2ccf0

Browse files
committed
Merge #15024: Allow specific private keys to be derived from descriptor
53b7de6 Add test for dumping the private key imported from descriptor (MeshCollider) 2857bc4 Extend importmulti descriptor tests (MeshCollider) 81a884b Import private keys from descriptor with importmulti if provided (MeshCollider) a4d1bd1 Add private key derivation functions to descriptors (MeshCollider) Pull request description: ~This is based on #14491, review the last 3 commits only.~ Currently, descriptors have an Expand() function which returns public keys and scripts for a specific index of a ranged descriptor. But the private key for a specific index is not given. This allows private keys for specific indices to be derived. This also allows those keys to be imported through the `importmulti` RPC rather than having to provide them separately. ACKs for commit 53b7de: achow101: ACK 53b7de6 Tree-SHA512: c060bc01358a1adc76d3d470fefc2bdd39c837027f452e9bc4bd2e726097e1ece4af9d5627efd942a5f8819271e15ba54f010b169b50a9435a1f0f40fd1cebf3
2 parents af05f36 + 53b7de6 commit 5d2ccf0

File tree

4 files changed

+75
-15
lines changed

4 files changed

+75
-15
lines changed

src/script/descriptor.cpp

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ struct PubkeyProvider
164164

165165
/** Get the descriptor string form including private data (if available in arg). */
166166
virtual bool ToPrivateString(const SigningProvider& arg, std::string& out) const = 0;
167+
168+
/** Derive a private key, if private data is available in arg. */
169+
virtual bool GetPrivKey(int pos, const SigningProvider& arg, CKey& key) const = 0;
167170
};
168171

169172
class OriginPubkeyProvider final : public PubkeyProvider
@@ -195,6 +198,10 @@ class OriginPubkeyProvider final : public PubkeyProvider
195198
ret = "[" + OriginString() + "]" + std::move(sub);
196199
return true;
197200
}
201+
bool GetPrivKey(int pos, const SigningProvider& arg, CKey& key) const override
202+
{
203+
return m_provider->GetPrivKey(pos, arg, key);
204+
}
198205
};
199206

200207
/** An object representing a parsed constant public key in a descriptor. */
@@ -222,6 +229,10 @@ class ConstPubkeyProvider final : public PubkeyProvider
222229
ret = EncodeSecret(key);
223230
return true;
224231
}
232+
bool GetPrivKey(int pos, const SigningProvider& arg, CKey& key) const override
233+
{
234+
return arg.GetKey(m_pubkey.GetID(), key);
235+
}
225236
};
226237

227238
enum class DeriveType {
@@ -266,14 +277,9 @@ class BIP32PubkeyProvider final : public PubkeyProvider
266277
{
267278
if (key) {
268279
if (IsHardened()) {
269-
CExtKey extkey;
270-
if (!GetExtKey(arg, extkey)) return false;
271-
for (auto entry : m_path) {
272-
extkey.Derive(extkey, entry);
273-
}
274-
if (m_derive == DeriveType::UNHARDENED) extkey.Derive(extkey, pos);
275-
if (m_derive == DeriveType::HARDENED) extkey.Derive(extkey, pos | 0x80000000UL);
276-
*key = extkey.Neuter().pubkey;
280+
CKey priv_key;
281+
if (!GetPrivKey(pos, arg, priv_key)) return false;
282+
*key = priv_key.GetPubKey();
277283
} else {
278284
// TODO: optimize by caching
279285
CExtPubKey extkey = m_extkey;
@@ -312,6 +318,18 @@ class BIP32PubkeyProvider final : public PubkeyProvider
312318
}
313319
return true;
314320
}
321+
bool GetPrivKey(int pos, const SigningProvider& arg, CKey& key) const override
322+
{
323+
CExtKey extkey;
324+
if (!GetExtKey(arg, extkey)) return false;
325+
for (auto entry : m_path) {
326+
extkey.Derive(extkey, entry);
327+
}
328+
if (m_derive == DeriveType::UNHARDENED) extkey.Derive(extkey, pos);
329+
if (m_derive == DeriveType::HARDENED) extkey.Derive(extkey, pos | 0x80000000UL);
330+
key = extkey.key;
331+
return true;
332+
}
315333
};
316334

317335
/** Base class for all Descriptor implementations. */
@@ -462,6 +480,20 @@ class DescriptorImpl : public Descriptor
462480
Span<const unsigned char> span = MakeSpan(cache);
463481
return ExpandHelper(pos, DUMMY_SIGNING_PROVIDER, &span, output_scripts, out, nullptr) && span.size() == 0;
464482
}
483+
484+
void ExpandPrivate(int pos, const SigningProvider& provider, FlatSigningProvider& out) const final
485+
{
486+
for (const auto& p : m_pubkey_args) {
487+
CKey key;
488+
if (!p->GetPrivKey(pos, provider, key)) continue;
489+
out.keys.emplace(key.GetPubKey().GetID(), key);
490+
}
491+
if (m_script_arg) {
492+
FlatSigningProvider subprovider;
493+
m_script_arg->ExpandPrivate(pos, provider, subprovider);
494+
out = Merge(out, subprovider);
495+
}
496+
}
465497
};
466498

467499
/** Construct a vector with one element, which is moved into it. */

src/script/descriptor.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ struct Descriptor {
6060
* out: scripts and public keys necessary for solving the expanded scriptPubKeys will be put here (may be equal to provider).
6161
*/
6262
virtual bool ExpandFromCache(int pos, const std::vector<unsigned char>& cache, std::vector<CScript>& output_scripts, FlatSigningProvider& out) const = 0;
63+
64+
/** Expand the private key for a descriptor at a specified position, if possible.
65+
*
66+
* pos: the position at which to expand the descriptor. If IsRange() is false, this is ignored.
67+
* provider: the provider to query for the private keys.
68+
* out: any private keys available for the specified pos will be placed here.
69+
*/
70+
virtual void ExpandPrivate(int pos, const SigningProvider& provider, FlatSigningProvider& out) const = 0;
6371
};
6472

6573
/** Parse a descriptor string. Included private keys are put in out.

src/wallet/rpcdump.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1165,8 +1165,7 @@ static UniValue ProcessImportDescriptor(ImportData& import_data, std::map<CKeyID
11651165

11661166
const UniValue& priv_keys = data.exists("keys") ? data["keys"].get_array() : UniValue();
11671167

1168-
// Expand all descriptors to get public keys and scripts.
1169-
// TODO: get private keys from descriptors too
1168+
// Expand all descriptors to get public keys and scripts, and private keys if available.
11701169
for (int i = range_start; i <= range_end; ++i) {
11711170
FlatSigningProvider out_keys;
11721171
std::vector<CScript> scripts_temp;
@@ -1180,7 +1179,10 @@ static UniValue ProcessImportDescriptor(ImportData& import_data, std::map<CKeyID
11801179
import_data.import_scripts.emplace(x.second);
11811180
}
11821181

1182+
parsed_desc->ExpandPrivate(i, keys, out_keys);
1183+
11831184
std::copy(out_keys.pubkeys.begin(), out_keys.pubkeys.end(), std::inserter(pubkey_map, pubkey_map.end()));
1185+
std::copy(out_keys.keys.begin(), out_keys.keys.end(), std::inserter(privkey_map, privkey_map.end()));
11841186
import_data.key_origins.insert(out_keys.origins.begin(), out_keys.origins.end());
11851187
}
11861188

test/functional/wallet_importmulti.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,7 @@ def run_test(self):
571571
# Test ranged descriptor fails if range is not specified
572572
xpriv = "tprv8ZgxMBicQKsPeuVhWwi6wuMQGfPKi9Li5GtX35jVNknACgqe3CY4g5xgkfDDJcmtF7o1QnxWDRYw4H5P26PXq7sbcUkEqeR4fg3Kxp2tigg"
573573
addresses = ["2N7yv4p8G8yEaPddJxY41kPihnWvs39qCMf", "2MsHxyb2JS3pAySeNUsJ7mNnurtpeenDzLA"] # hdkeypath=m/0'/0'/0' and 1'
574+
addresses += ["bcrt1qrd3n235cj2czsfmsuvqqpr3lu6lg0ju7scl8gn", "bcrt1qfqeppuvj0ww98r6qghmdkj70tv8qpchehegrg8"] # wpkh subscripts corresponding to the above addresses
574575
desc = "sh(wpkh(" + xpriv + "/0'/0'/*'" + "))"
575576
self.log.info("Ranged descriptor import should fail without a specified range")
576577
self.test_importmulti({"desc": descsum_create(desc),
@@ -579,17 +580,17 @@ def run_test(self):
579580
error_code=-8,
580581
error_message='Descriptor is ranged, please specify the range')
581582

582-
# Test importing of a ranged descriptor without keys
583+
# Test importing of a ranged descriptor with xpriv
583584
self.log.info("Should import the ranged descriptor with specified range as solvable")
584585
self.test_importmulti({"desc": descsum_create(desc),
585586
"timestamp": "now",
586587
"range": 1},
587-
success=True,
588-
warnings=["Some private keys are missing, outputs will be considered watchonly. If this is intentional, specify the watchonly flag."])
588+
success=True)
589589
for address in addresses:
590590
test_address(self.nodes[1],
591-
key.p2sh_p2wpkh_addr,
592-
solvable=True)
591+
address,
592+
solvable=True,
593+
ismine=True)
593594

594595
self.test_importmulti({"desc": descsum_create(desc), "timestamp": "now", "range": -1},
595596
success=False, error_code=-8, error_message='End of range is too high')
@@ -606,6 +607,23 @@ def run_test(self):
606607
self.test_importmulti({"desc": descsum_create(desc), "timestamp": "now", "range": [0, 1000001]},
607608
success=False, error_code=-8, error_message='Range is too large')
608609

610+
# Test importing a descriptor containing a WIF private key
611+
wif_priv = "cTe1f5rdT8A8DFgVWTjyPwACsDPJM9ff4QngFxUixCSvvbg1x6sh"
612+
address = "2MuhcG52uHPknxDgmGPsV18jSHFBnnRgjPg"
613+
desc = "sh(wpkh(" + wif_priv + "))"
614+
self.log.info("Should import a descriptor with a WIF private key as spendable")
615+
self.test_importmulti({"desc": descsum_create(desc),
616+
"timestamp": "now"},
617+
success=True)
618+
test_address(self.nodes[1],
619+
address,
620+
solvable=True,
621+
ismine=True)
622+
623+
# dump the private key to ensure it matches what was imported
624+
privkey = self.nodes[1].dumpprivkey(address)
625+
assert_equal(privkey, wif_priv)
626+
609627
# Test importing of a P2PKH address via descriptor
610628
key = get_key(self.nodes[0])
611629
self.log.info("Should import a p2pkh address from descriptor")

0 commit comments

Comments
 (0)