diff --git a/src/script/descriptor.h b/src/script/descriptor.h --- a/src/script/descriptor.h +++ b/src/script/descriptor.h @@ -80,6 +80,17 @@ virtual bool ExpandFromCache(int pos, const std::vector &cache, std::vector &output_scripts, FlatSigningProvider &out) const = 0; + + /** Expand the private key for a descriptor at a specified position, if + * possible. + * + * pos: the position at which to expand the descriptor. If IsRange() is + * false, this is ignored. provider: the provider to query for the private + * keys. out: any private keys available for the specified pos will be + * placed here. + */ + virtual void ExpandPrivate(int pos, const SigningProvider &provider, + FlatSigningProvider &out) const = 0; }; /** Parse a descriptor string. Included private keys are put in out. diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp --- a/src/script/descriptor.cpp +++ b/src/script/descriptor.cpp @@ -200,6 +200,10 @@ */ virtual bool ToPrivateString(const SigningProvider &arg, std::string &out) const = 0; + + /** Derive a private key, if private data is available in arg. */ + virtual bool GetPrivKey(int pos, const SigningProvider &arg, + CKey &key) const = 0; }; class OriginPubkeyProvider final : public PubkeyProvider { @@ -241,6 +245,10 @@ ret = "[" + OriginString() + "]" + std::move(sub); return true; } + bool GetPrivKey(int pos, const SigningProvider &arg, + CKey &key) const override { + return m_provider->GetPrivKey(pos, arg, key); + } }; /** An object representing a parsed constant public key in a descriptor. */ @@ -274,6 +282,10 @@ ret = EncodeSecret(key); return true; } + bool GetPrivKey(int pos, const SigningProvider &arg, + CKey &key) const override { + return arg.GetKey(m_pubkey.GetID(), key); + } }; enum class DeriveType { @@ -325,20 +337,11 @@ KeyOriginInfo &info) const override { if (key) { if (IsHardened()) { - CExtKey extkey; - if (!GetExtKey(arg, extkey)) { + CKey priv_key; + if (!GetPrivKey(pos, arg, priv_key)) { return false; } - for (auto entry : m_path) { - extkey.Derive(extkey, entry); - } - if (m_derive == DeriveType::UNHARDENED) { - extkey.Derive(extkey, pos); - } - if (m_derive == DeriveType::HARDENED) { - extkey.Derive(extkey, pos | 0x80000000UL); - } - *key = extkey.Neuter().pubkey; + *key = priv_key.GetPubKey(); } else { // TODO: optimize by caching CExtPubKey extkey = m_extkey; @@ -389,6 +392,24 @@ } return true; } + bool GetPrivKey(int pos, const SigningProvider &arg, + CKey &key) const override { + CExtKey extkey; + if (!GetExtKey(arg, extkey)) { + return false; + } + for (auto entry : m_path) { + extkey.Derive(extkey, entry); + } + if (m_derive == DeriveType::UNHARDENED) { + extkey.Derive(extkey, pos); + } + if (m_derive == DeriveType::HARDENED) { + extkey.Derive(extkey, pos | 0x80000000UL); + } + key = extkey.key; + return true; + } }; /** Base class for all Descriptor implementations. */ @@ -596,6 +617,22 @@ out, nullptr) && span.size() == 0; } + + void ExpandPrivate(int pos, const SigningProvider &provider, + FlatSigningProvider &out) const final { + for (const auto &p : m_pubkey_args) { + CKey key; + if (!p->GetPrivKey(pos, provider, key)) { + continue; + } + out.keys.emplace(key.GetPubKey().GetID(), key); + } + if (m_subdescriptor_arg) { + FlatSigningProvider subprovider; + m_subdescriptor_arg->ExpandPrivate(pos, provider, subprovider); + out = Merge(out, subprovider); + } + } }; /** A parsed addr(A) descriptor. */ diff --git a/src/wallet/rpcdump.cpp b/src/wallet/rpcdump.cpp --- a/src/wallet/rpcdump.cpp +++ b/src/wallet/rpcdump.cpp @@ -1322,8 +1322,8 @@ const UniValue &priv_keys = data.exists("keys") ? data["keys"].get_array() : UniValue(); - // Expand all descriptors to get public keys and scripts. - // TODO: get private keys from descriptors too + // Expand all descriptors to get public keys and scripts, and private keys + // if available. for (int i = range_start; i <= range_end; ++i) { FlatSigningProvider out_keys; std::vector scripts_temp; @@ -1338,8 +1338,12 @@ import_data.import_scripts.emplace(x.second); } + parsed_desc->ExpandPrivate(i, keys, out_keys); + std::copy(out_keys.pubkeys.begin(), out_keys.pubkeys.end(), std::inserter(pubkey_map, pubkey_map.end())); + std::copy(out_keys.keys.begin(), out_keys.keys.end(), + std::inserter(privkey_map, privkey_map.end())); import_data.key_origins.insert(out_keys.origins.begin(), out_keys.origins.end()); } diff --git a/test/functional/wallet_importmulti.py b/test/functional/wallet_importmulti.py --- a/test/functional/wallet_importmulti.py +++ b/test/functional/wallet_importmulti.py @@ -481,12 +481,18 @@ error_code=-5, error_message='Missing checksum') + # Test ranged descriptor fails if range is not specified xpriv = "tprv8ZgxMBicQKsPeuVhWwi6wuMQGfPKi9Li5GtX35jVNknACgqe3CY4g5xgkfDDJcmtF7o1QnxWDRYw4H5P26PXq7sbcUkEqeR4fg3Kxp2tigg" # hdkeypath=m/0'/0'/0' and 1' addresses = [ - "2N7yv4p8G8yEaPddJxY41kPihnWvs39qCMf", - "2MsHxyb2JS3pAySeNUsJ7mNnurtpeenDzLA"] - desc = "pkh(" + xpriv + "/0'/0'/*'" + ")" + "bchreg:prvn9ycvgr5atuyh49sua3mapskh2mnnzg34lqtyst", + "bchreg:pp3n087yx0njv2e5wcvltahfxqst7l66ruyuaun8qt"] + # pkh subscripts corresponding to the above addresses + addresses += [ + "bchreg:qqdkxd2xnzftq2p8wr3sqqyw8lntap7tncl2076yur", + "bchreg:qpyryy83jfaec5u0gpzldk6teadsuq8zly0fwmm3pq", + ] + desc = "sh(pkh(" + xpriv + "/0'/0'/*'" + "))" self.log.info( "Ranged descriptor import should fail without a specified range") self.test_importmulti({"desc": descsum_create(desc), @@ -495,17 +501,15 @@ error_code=-8, error_message='Descriptor is ranged, please specify the range') - # Test importing of a ranged descriptor without keys + # Test importing of a ranged descriptor with xpriv self.log.info( "Should import the ranged descriptor with specified range as solvable") self.test_importmulti({"desc": descsum_create(desc), "timestamp": "now", "range": 1}, - success=True, - warnings=["Some private keys are missing, outputs will be considered watchonly. If this is intentional, specify the watchonly flag."]) + success=True) for address in addresses: - # P2PKH are not considered solvable. - test_address(self.nodes[1], key.p2pkh_addr, solvable=False) + test_address(self.nodes[1], address, solvable=True, ismine=True) self.test_importmulti({"desc": descsum_create(desc), "timestamp": "now", "range": -1}, success=False, error_code=-8, error_message='End of range is too high') @@ -522,6 +526,27 @@ self.test_importmulti({"desc": descsum_create(desc), "timestamp": "now", "range": [0, 1000001]}, success=False, error_code=-8, error_message='Range is too large') + # Test importing a descriptor containing a WIF private key + wif_priv = "cTe1f5rdT8A8DFgVWTjyPwACsDPJM9ff4QngFxUixCSvvbg1x6sh" + # Note: in Core's test, this address refers to the sh(wpkh()) address. + # For a sh(pkh()) this does not refer to a key, so we use the subscript + # address instead, which returns the same privkey. + address = "bchreg:qzh6rch6st3wjvp0h2ud87gn7xnxvf6h8yvgavjk6t" + desc = "sh(pkh(" + wif_priv + "))" + self.log.info( + "Should import a descriptor with a WIF private key as spendable") + self.test_importmulti({"desc": descsum_create(desc), + "timestamp": "now"}, + success=True) + test_address(self.nodes[1], + address, + solvable=True, + ismine=True) + + # dump the private key to ensure it matches what was imported + privkey = self.nodes[1].dumpprivkey(address) + assert_equal(privkey, wif_priv) + # Test importing of a P2PKH address via descriptor key = get_key(self.nodes[0]) p2pkh_label = "P2PKH descriptor import"