Merge #14021: Import key origin data through descriptors in importmulti

cb3511b9d Add release notes for importing key origin info change (Andrew Chow)
4c75a69f3 Test importing descriptors with key origin information (Andrew Chow)
02d6586d7 Import KeyOriginData when importing descriptors (Andrew Chow)
3d235dff5 Implement a function to add KeyOriginInfo to a wallet (Andrew Chow)
eab63bc26 Store key origin info in key metadata (Andrew Chow)
345bff601 Remove hdmasterkeyid (Andrew Chow)
bac8c676a Add a method to CWallet to write just CKeyMetadata (Andrew Chow)
e7652d3f6 Add WriteHDKeypath function and move *HDKeypath to util/bip32.{h,cpp} (Andrew Chow)
c45415f73 Refactor keymetadata writing to a separate method (Andrew Chow)

Pull request description:

  This PR allows for key origin data as defined by the descriptors document to be imported to the wallet when importing a descriptor using `importmulti`. This allows the `walletprocesspsbt` to include the BIP 32 derivation paths for keys that it is watching that are from a different HD wallet.

  In order to make this easier to use, a new field `hdmasterkeyfingerprint` has been added to `getaddressinfo`. Additionally I have removed `hdmasterkeyid` as was planned. I think that this API change is fine since it was going to be removed in 0.18 anyways. `CKeyMetadata` has also been extended to store key origin info to facilitate this.

Tree-SHA512: 9c7794f3c793da57e23c5abbdc3d58779ee9dea3d53168bb86c0643a4ad5a11a446264961e2f772f35eea645048cb60954ed58050002caee4e43cd9f51215097
This commit is contained in:
MeshCollider 2019-02-15 12:10:52 +13:00
commit 8d0ec74801
No known key found for this signature in database
GPG key ID: D300116E1C875A3D
19 changed files with 307 additions and 87 deletions

View file

@ -0,0 +1,11 @@
Miscellaneous RPC Changes
-------------------------
- Descriptors with key origin information imported through `importmulti` will have their key origin information stored in the wallet for use with creating PSBTs.
- If `bip32derivs` of both `walletprocesspsbt` and `walletcreatefundedpsbt` is set to true but the key metadata for a public key has not been updated yet, then that key will have a derivation path as if it were just an independent key (i.e. no derivation path and its master fingerprint is itself)
Miscellaneous Wallet changes
----------------------------
- The key metadata will need to be upgraded the first time that the HD seed is available.
For unencrypted wallets this will occur on wallet loading.
For encrypted wallets this will occur the first time the wallet is unlocked.

View file

@ -196,6 +196,7 @@ BITCOIN_CORE_H = \
txmempool.h \ txmempool.h \
ui_interface.h \ ui_interface.h \
undo.h \ undo.h \
util/bip32.h \
util/bytevectorhash.h \ util/bytevectorhash.h \
util/system.h \ util/system.h \
util/memory.h \ util/memory.h \
@ -456,6 +457,7 @@ libbitcoin_util_a_SOURCES = \
support/cleanse.cpp \ support/cleanse.cpp \
sync.cpp \ sync.cpp \
threadinterrupt.cpp \ threadinterrupt.cpp \
util/bip32.cpp \
util/bytevectorhash.cpp \ util/bytevectorhash.cpp \
util/system.cpp \ util/system.cpp \
util/moneystr.cpp \ util/moneystr.cpp \

View file

@ -26,6 +26,7 @@
#include <script/sign.h> #include <script/sign.h>
#include <script/standard.h> #include <script/standard.h>
#include <uint256.h> #include <uint256.h>
#include <util/bip32.h>
#include <util/strencodings.h> #include <util/strencodings.h>
#include <validation.h> #include <validation.h>
#include <validationinterface.h> #include <validationinterface.h>

View file

@ -10,6 +10,7 @@
#include <script/standard.h> #include <script/standard.h>
#include <span.h> #include <span.h>
#include <util/bip32.h>
#include <util/system.h> #include <util/system.h>
#include <util/strencodings.h> #include <util/strencodings.h>
@ -25,16 +26,6 @@ namespace {
typedef std::vector<uint32_t> KeyPath; typedef std::vector<uint32_t> KeyPath;
std::string FormatKeyPath(const KeyPath& path)
{
std::string ret;
for (auto i : path) {
ret += strprintf("/%i", (i << 1) >> 1);
if (i >> 31) ret += '\'';
}
return ret;
}
/** Interface for public key objects in descriptors. */ /** Interface for public key objects in descriptors. */
struct PubkeyProvider struct PubkeyProvider
{ {
@ -63,7 +54,7 @@ class OriginPubkeyProvider final : public PubkeyProvider
std::string OriginString() const std::string OriginString() const
{ {
return HexStr(std::begin(m_origin.fingerprint), std::end(m_origin.fingerprint)) + FormatKeyPath(m_origin.path); return HexStr(std::begin(m_origin.fingerprint), std::end(m_origin.fingerprint)) + FormatHDKeypath(m_origin.path);
} }
public: public:
@ -184,7 +175,7 @@ public:
} }
std::string ToString() const override std::string ToString() const override
{ {
std::string ret = EncodeExtPubKey(m_extkey) + FormatKeyPath(m_path); std::string ret = EncodeExtPubKey(m_extkey) + FormatHDKeypath(m_path);
if (IsRange()) { if (IsRange()) {
ret += "/*"; ret += "/*";
if (m_derive == DeriveType::HARDENED) ret += '\''; if (m_derive == DeriveType::HARDENED) ret += '\'';
@ -195,7 +186,7 @@ public:
{ {
CExtKey key; CExtKey key;
if (!GetExtKey(arg, key)) return false; if (!GetExtKey(arg, key)) return false;
out = EncodeExtKey(key) + FormatKeyPath(m_path); out = EncodeExtKey(key) + FormatHDKeypath(m_path);
if (IsRange()) { if (IsRange()) {
out += "/*"; out += "/*";
if (m_derive == DeriveType::HARDENED) out += '\''; if (m_derive == DeriveType::HARDENED) out += '\'';

View file

@ -22,13 +22,27 @@ struct CMutableTransaction;
struct KeyOriginInfo struct KeyOriginInfo
{ {
unsigned char fingerprint[4]; unsigned char fingerprint[4]; //!< First 32 bits of the Hash160 of the public key at the root of the path
std::vector<uint32_t> path; std::vector<uint32_t> path;
friend bool operator==(const KeyOriginInfo& a, const KeyOriginInfo& b) friend bool operator==(const KeyOriginInfo& a, const KeyOriginInfo& b)
{ {
return std::equal(std::begin(a.fingerprint), std::end(a.fingerprint), std::begin(b.fingerprint)) && a.path == b.path; return std::equal(std::begin(a.fingerprint), std::end(a.fingerprint), std::begin(b.fingerprint)) && a.path == b.path;
} }
ADD_SERIALIZE_METHODS;
template <typename Stream, typename Operation>
inline void SerializationOp(Stream& s, Operation ser_action)
{
READWRITE(fingerprint);
READWRITE(path);
}
void clear()
{
memset(fingerprint, 0, 4);
path.clear();
}
}; };
/** An interface to be implemented by keystores that support signing. */ /** An interface to be implemented by keystores that support signing. */

66
src/util/bip32.cpp Normal file
View file

@ -0,0 +1,66 @@
// Copyright (c) 2019 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <sstream>
#include <stdio.h>
#include <tinyformat.h>
#include <util/bip32.h>
#include <util/strencodings.h>
bool ParseHDKeypath(const std::string& keypath_str, std::vector<uint32_t>& keypath)
{
std::stringstream ss(keypath_str);
std::string item;
bool first = true;
while (std::getline(ss, item, '/')) {
if (item.compare("m") == 0) {
if (first) {
first = false;
continue;
}
return false;
}
// Finds whether it is hardened
uint32_t path = 0;
size_t pos = item.find("'");
if (pos != std::string::npos) {
// The hardened tick can only be in the last index of the string
if (pos != item.size() - 1) {
return false;
}
path |= 0x80000000;
item = item.substr(0, item.size() - 1); // Drop the last character which is the hardened tick
}
// Ensure this is only numbers
if (item.find_first_not_of( "0123456789" ) != std::string::npos) {
return false;
}
uint32_t number;
if (!ParseUInt32(item, &number)) {
return false;
}
path |= number;
keypath.push_back(path);
first = false;
}
return true;
}
std::string FormatHDKeypath(const std::vector<uint32_t>& path)
{
std::string ret;
for (auto i : path) {
ret += strprintf("/%i", (i << 1) >> 1);
if (i >> 31) ret += '\'';
}
return ret;
}
std::string WriteHDKeypath(const std::vector<uint32_t>& keypath)
{
return "m" + FormatHDKeypath(keypath);
}

19
src/util/bip32.h Normal file
View file

@ -0,0 +1,19 @@
// Copyright (c) 2019 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#ifndef BITCOIN_UTIL_BIP32_H
#define BITCOIN_UTIL_BIP32_H
#include <attributes.h>
#include <string>
#include <vector>
/** Parse an HD keypaths like "m/7/0'/2000". */
NODISCARD bool ParseHDKeypath(const std::string& keypath_str, std::vector<uint32_t>& keypath);
/** Write HD keypaths as strings */
std::string WriteHDKeypath(const std::vector<uint32_t>& keypath);
std::string FormatHDKeypath(const std::vector<uint32_t>& path);
#endif // BITCOIN_UTIL_BIP32_H

View file

@ -546,47 +546,6 @@ bool ParseFixedPoint(const std::string &val, int decimals, int64_t *amount_out)
return true; return true;
} }
bool ParseHDKeypath(const std::string& keypath_str, std::vector<uint32_t>& keypath)
{
std::stringstream ss(keypath_str);
std::string item;
bool first = true;
while (std::getline(ss, item, '/')) {
if (item.compare("m") == 0) {
if (first) {
first = false;
continue;
}
return false;
}
// Finds whether it is hardened
uint32_t path = 0;
size_t pos = item.find("'");
if (pos != std::string::npos) {
// The hardened tick can only be in the last index of the string
if (pos != item.size() - 1) {
return false;
}
path |= 0x80000000;
item = item.substr(0, item.size() - 1); // Drop the last character which is the hardened tick
}
// Ensure this is only numbers
if (item.find_first_not_of( "0123456789" ) != std::string::npos) {
return false;
}
uint32_t number;
if (!ParseUInt32(item, &number)) {
return false;
}
path |= number;
keypath.push_back(path);
first = false;
}
return true;
}
void Downcase(std::string& str) void Downcase(std::string& str)
{ {
std::transform(str.begin(), str.end(), str.begin(), [](char c){return ToLower(c);}); std::transform(str.begin(), str.end(), str.begin(), [](char c){return ToLower(c);});

View file

@ -197,9 +197,6 @@ bool ConvertBits(const O& outfn, I it, I end) {
return true; return true;
} }
/** Parse an HD keypaths like "m/7/0'/2000". */
NODISCARD bool ParseHDKeypath(const std::string& keypath_str, std::vector<uint32_t>& keypath);
/** /**
* Converts the given character to its lowercase equivalent. * Converts the given character to its lowercase equivalent.
* This function is locale independent. It only converts uppercase * This function is locale independent. It only converts uppercase

View file

@ -13,6 +13,7 @@
#include <script/script.h> #include <script/script.h>
#include <script/standard.h> #include <script/standard.h>
#include <sync.h> #include <sync.h>
#include <util/bip32.h>
#include <util/system.h> #include <util/system.h>
#include <util/time.h> #include <util/time.h>
#include <validation.h> #include <validation.h>
@ -850,7 +851,7 @@ UniValue dumpwallet(const JSONRPCRequest& request)
} else { } else {
file << "change=1"; file << "change=1";
} }
file << strprintf(" # addr=%s%s\n", strAddr, (pwallet->mapKeyMetadata[keyid].hdKeypath.size() > 0 ? " hdkeypath="+pwallet->mapKeyMetadata[keyid].hdKeypath : "")); file << strprintf(" # addr=%s%s\n", strAddr, (pwallet->mapKeyMetadata[keyid].has_key_origin ? " hdkeypath="+WriteHDKeypath(pwallet->mapKeyMetadata[keyid].key_origin.path) : ""));
} }
} }
file << "\n"; file << "\n";
@ -887,6 +888,7 @@ struct ImportData
// Output data // Output data
std::set<CScript> import_scripts; std::set<CScript> import_scripts;
std::map<CKeyID, bool> used_keys; //!< Import these private keys if available (the value indicates whether if the key is required for solvability) std::map<CKeyID, bool> used_keys; //!< Import these private keys if available (the value indicates whether if the key is required for solvability)
std::map<CKeyID, KeyOriginInfo> key_origins;
}; };
enum class ScriptContext enum class ScriptContext
@ -1157,7 +1159,7 @@ static UniValue ProcessImportDescriptor(ImportData& import_data, std::map<CKeyID
} }
std::copy(out_keys.pubkeys.begin(), out_keys.pubkeys.end(), std::inserter(pubkey_map, pubkey_map.end())); std::copy(out_keys.pubkeys.begin(), out_keys.pubkeys.end(), std::inserter(pubkey_map, pubkey_map.end()));
import_data.key_origins.insert(out_keys.origins.begin(), out_keys.origins.end());
for (size_t i = 0; i < priv_keys.size(); ++i) { for (size_t i = 0; i < priv_keys.size(); ++i) {
const auto& str = priv_keys[i].get_str(); const auto& str = priv_keys[i].get_str();
CKey key = DecodeSecret(str); CKey key = DecodeSecret(str);
@ -1260,6 +1262,11 @@ static UniValue ProcessImport(CWallet * const pwallet, const UniValue& data, con
if (!pwallet->GetPubKey(id, temp) && !pwallet->AddWatchOnly(GetScriptForRawPubKey(pubkey), timestamp)) { if (!pwallet->GetPubKey(id, temp) && !pwallet->AddWatchOnly(GetScriptForRawPubKey(pubkey), timestamp)) {
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding address to wallet"); throw JSONRPCError(RPC_WALLET_ERROR, "Error adding address to wallet");
} }
const auto& key_orig_it = import_data.key_origins.find(id);
if (key_orig_it != import_data.key_origins.end()) {
pwallet->AddKeyOrigin(pubkey, key_orig_it->second);
}
pwallet->mapKeyMetadata[id].nCreateTime = timestamp;
} }
for (const CScript& script : script_pub_keys) { for (const CScript& script : script_pub_keys) {

View file

@ -27,6 +27,7 @@
#include <script/sign.h> #include <script/sign.h>
#include <shutdown.h> #include <shutdown.h>
#include <timedata.h> #include <timedata.h>
#include <util/bip32.h>
#include <util/system.h> #include <util/system.h>
#include <util/moneystr.h> #include <util/moneystr.h>
#include <wallet/coincontrol.h> #include <wallet/coincontrol.h>
@ -2418,7 +2419,6 @@ static UniValue getwalletinfo(const JSONRPCRequest& request)
" \"unlocked_until\": ttt, (numeric) the timestamp in seconds since epoch (midnight Jan 1 1970 GMT) that the wallet is unlocked for transfers, or 0 if the wallet is locked\n" " \"unlocked_until\": ttt, (numeric) the timestamp in seconds since epoch (midnight Jan 1 1970 GMT) that the wallet is unlocked for transfers, or 0 if the wallet is locked\n"
" \"paytxfee\": x.xxxx, (numeric) the transaction fee configuration, set in " + CURRENCY_UNIT + "/kB\n" " \"paytxfee\": x.xxxx, (numeric) the transaction fee configuration, set in " + CURRENCY_UNIT + "/kB\n"
" \"hdseedid\": \"<hash160>\" (string, optional) the Hash160 of the HD seed (only present when HD is enabled)\n" " \"hdseedid\": \"<hash160>\" (string, optional) the Hash160 of the HD seed (only present when HD is enabled)\n"
" \"hdmasterkeyid\": \"<hash160>\" (string, optional) alias for hdseedid retained for backwards-compatibility. Will be removed in V0.18.\n"
" \"private_keys_enabled\": true|false (boolean) false if privatekeys are disabled for this wallet (enforced watch-only wallet)\n" " \"private_keys_enabled\": true|false (boolean) false if privatekeys are disabled for this wallet (enforced watch-only wallet)\n"
"}\n" "}\n"
}, },
@ -2456,7 +2456,6 @@ static UniValue getwalletinfo(const JSONRPCRequest& request)
obj.pushKV("paytxfee", ValueFromAmount(pwallet->m_pay_tx_fee.GetFeePerK())); obj.pushKV("paytxfee", ValueFromAmount(pwallet->m_pay_tx_fee.GetFeePerK()));
if (!seed_id.IsNull()) { if (!seed_id.IsNull()) {
obj.pushKV("hdseedid", seed_id.GetHex()); obj.pushKV("hdseedid", seed_id.GetHex());
obj.pushKV("hdmasterkeyid", seed_id.GetHex());
} }
obj.pushKV("private_keys_enabled", !pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)); obj.pushKV("private_keys_enabled", !pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS));
return obj; return obj;
@ -3684,7 +3683,7 @@ UniValue getaddressinfo(const JSONRPCRequest& request)
" \"timestamp\" : timestamp, (number, optional) The creation time of the key if available in seconds since epoch (Jan 1 1970 GMT)\n" " \"timestamp\" : timestamp, (number, optional) The creation time of the key if available in seconds since epoch (Jan 1 1970 GMT)\n"
" \"hdkeypath\" : \"keypath\" (string, optional) The HD keypath if the key is HD and available\n" " \"hdkeypath\" : \"keypath\" (string, optional) The HD keypath if the key is HD and available\n"
" \"hdseedid\" : \"<hash160>\" (string, optional) The Hash160 of the HD seed\n" " \"hdseedid\" : \"<hash160>\" (string, optional) The Hash160 of the HD seed\n"
" \"hdmasterkeyid\" : \"<hash160>\" (string, optional) alias for hdseedid maintained for backwards compatibility. Will be removed in V0.18.\n" " \"hdmasterfingerprint\" : \"<hash160>\" (string, optional) The fingperint of the master key.\n"
" \"labels\" (object) Array of labels associated with the address.\n" " \"labels\" (object) Array of labels associated with the address.\n"
" [\n" " [\n"
" { (json object of label data)\n" " { (json object of label data)\n"
@ -3747,10 +3746,10 @@ UniValue getaddressinfo(const JSONRPCRequest& request)
} }
if (meta) { if (meta) {
ret.pushKV("timestamp", meta->nCreateTime); ret.pushKV("timestamp", meta->nCreateTime);
if (!meta->hdKeypath.empty()) { if (meta->has_key_origin) {
ret.pushKV("hdkeypath", meta->hdKeypath); ret.pushKV("hdkeypath", WriteHDKeypath(meta->key_origin.path));
ret.pushKV("hdseedid", meta->hd_seed_id.GetHex()); ret.pushKV("hdseedid", meta->hd_seed_id.GetHex());
ret.pushKV("hdmasterkeyid", meta->hd_seed_id.GetHex()); ret.pushKV("hdmasterfingerprint", HexStr(meta->key_origin.fingerprint, meta->key_origin.fingerprint + 4));
} }
} }

View file

@ -4,6 +4,7 @@
#include <key_io.h> #include <key_io.h>
#include <script/sign.h> #include <script/sign.h>
#include <util/bip32.h>
#include <util/strencodings.h> #include <util/strencodings.h>
#include <wallet/psbtwallet.h> #include <wallet/psbtwallet.h>
#include <wallet/rpcwallet.h> #include <wallet/rpcwallet.h>

View file

@ -27,6 +27,7 @@
#include <shutdown.h> #include <shutdown.h>
#include <timedata.h> #include <timedata.h>
#include <txmempool.h> #include <txmempool.h>
#include <util/bip32.h>
#include <util/moneystr.h> #include <util/moneystr.h>
#include <wallet/fees.h> #include <wallet/fees.h>
@ -255,16 +256,25 @@ void CWallet::DeriveNewChildKey(WalletBatch &batch, CKeyMetadata& metadata, CKey
if (internal) { if (internal) {
chainChildKey.Derive(childKey, hdChain.nInternalChainCounter | BIP32_HARDENED_KEY_LIMIT); chainChildKey.Derive(childKey, hdChain.nInternalChainCounter | BIP32_HARDENED_KEY_LIMIT);
metadata.hdKeypath = "m/0'/1'/" + std::to_string(hdChain.nInternalChainCounter) + "'"; metadata.hdKeypath = "m/0'/1'/" + std::to_string(hdChain.nInternalChainCounter) + "'";
metadata.key_origin.path.push_back(0 | BIP32_HARDENED_KEY_LIMIT);
metadata.key_origin.path.push_back(1 | BIP32_HARDENED_KEY_LIMIT);
metadata.key_origin.path.push_back(hdChain.nInternalChainCounter | BIP32_HARDENED_KEY_LIMIT);
hdChain.nInternalChainCounter++; hdChain.nInternalChainCounter++;
} }
else { else {
chainChildKey.Derive(childKey, hdChain.nExternalChainCounter | BIP32_HARDENED_KEY_LIMIT); chainChildKey.Derive(childKey, hdChain.nExternalChainCounter | BIP32_HARDENED_KEY_LIMIT);
metadata.hdKeypath = "m/0'/0'/" + std::to_string(hdChain.nExternalChainCounter) + "'"; metadata.hdKeypath = "m/0'/0'/" + std::to_string(hdChain.nExternalChainCounter) + "'";
metadata.key_origin.path.push_back(0 | BIP32_HARDENED_KEY_LIMIT);
metadata.key_origin.path.push_back(0 | BIP32_HARDENED_KEY_LIMIT);
metadata.key_origin.path.push_back(hdChain.nExternalChainCounter | BIP32_HARDENED_KEY_LIMIT);
hdChain.nExternalChainCounter++; hdChain.nExternalChainCounter++;
} }
} while (HaveKey(childKey.key.GetPubKey().GetID())); } while (HaveKey(childKey.key.GetPubKey().GetID()));
secret = childKey.key; secret = childKey.key;
metadata.hd_seed_id = hdChain.seed_id; metadata.hd_seed_id = hdChain.seed_id;
CKeyID master_id = masterKey.key.GetPubKey().GetID();
std::copy(master_id.begin(), master_id.begin() + 4, metadata.key_origin.fingerprint);
metadata.has_key_origin = true;
// update the chain model in the database // update the chain model in the database
if (!batch.WriteHDChain(hdChain)) if (!batch.WriteHDChain(hdChain))
throw std::runtime_error(std::string(__func__) + ": Writing HD chain model failed"); throw std::runtime_error(std::string(__func__) + ": Writing HD chain model failed");
@ -348,6 +358,47 @@ void CWallet::LoadScriptMetadata(const CScriptID& script_id, const CKeyMetadata
m_script_metadata[script_id] = meta; m_script_metadata[script_id] = meta;
} }
// Writes a keymetadata for a public key. overwrite specifies whether to overwrite an existing metadata for that key if there exists one.
bool CWallet::WriteKeyMetadata(const CKeyMetadata& meta, const CPubKey& pubkey, const bool overwrite)
{
return WalletBatch(*database).WriteKeyMetadata(meta, pubkey, overwrite);
}
void CWallet::UpgradeKeyMetadata()
{
AssertLockHeld(cs_wallet); // mapKeyMetadata
if (IsLocked() || IsWalletFlagSet(WALLET_FLAG_KEY_ORIGIN_METADATA)) {
return;
}
for (auto& meta_pair : mapKeyMetadata) {
CKeyMetadata& meta = meta_pair.second;
if (!meta.hd_seed_id.IsNull() && !meta.has_key_origin && meta.hdKeypath != "s") { // If the hdKeypath is "s", that's the seed and it doesn't have a key origin
CKey key;
GetKey(meta.hd_seed_id, key);
CExtKey masterKey;
masterKey.SetSeed(key.begin(), key.size());
// Add to map
CKeyID master_id = masterKey.key.GetPubKey().GetID();
std::copy(master_id.begin(), master_id.begin() + 4, meta.key_origin.fingerprint);
if (!ParseHDKeypath(meta.hdKeypath, meta.key_origin.path)) {
throw std::runtime_error("Invalid stored hdKeypath");
}
meta.has_key_origin = true;
if (meta.nVersion < CKeyMetadata::VERSION_WITH_KEY_ORIGIN) {
meta.nVersion = CKeyMetadata::VERSION_WITH_KEY_ORIGIN;
}
// Write meta to wallet
CPubKey pubkey;
if (GetPubKey(meta_pair.first, pubkey)) {
WriteKeyMetadata(meta, pubkey, true);
}
}
}
SetWalletFlag(WALLET_FLAG_KEY_ORIGIN_METADATA);
}
bool CWallet::LoadCryptedKey(const CPubKey &vchPubKey, const std::vector<unsigned char> &vchCryptedSecret) bool CWallet::LoadCryptedKey(const CPubKey &vchPubKey, const std::vector<unsigned char> &vchCryptedSecret)
{ {
return CCryptoKeyStore::AddCryptedKey(vchPubKey, vchCryptedSecret); return CCryptoKeyStore::AddCryptedKey(vchPubKey, vchCryptedSecret);
@ -446,8 +497,11 @@ bool CWallet::Unlock(const SecureString& strWalletPassphrase, bool accept_no_key
return false; return false;
if (!crypter.Decrypt(pMasterKey.second.vchCryptedKey, _vMasterKey)) if (!crypter.Decrypt(pMasterKey.second.vchCryptedKey, _vMasterKey))
continue; // try another master key continue; // try another master key
if (CCryptoKeyStore::Unlock(_vMasterKey, accept_no_keys)) if (CCryptoKeyStore::Unlock(_vMasterKey, accept_no_keys)) {
// Now that we've unlocked, upgrade the key metadata
UpgradeKeyMetadata();
return true; return true;
}
} }
} }
return false; return false;
@ -1407,6 +1461,7 @@ CPubKey CWallet::DeriveNewSeed(const CKey& key)
// set the hd keypath to "s" -> Seed, refers the seed to itself // set the hd keypath to "s" -> Seed, refers the seed to itself
metadata.hdKeypath = "s"; metadata.hdKeypath = "s";
metadata.has_key_origin = false;
metadata.hd_seed_id = seed.GetID(); metadata.hd_seed_id = seed.GetID();
{ {
@ -4487,18 +4542,21 @@ bool CWallet::GetKeyOrigin(const CKeyID& keyID, KeyOriginInfo& info) const
meta = it->second; meta = it->second;
} }
} }
if (!meta.hdKeypath.empty()) { if (meta.has_key_origin) {
if (!ParseHDKeypath(meta.hdKeypath, info.path)) return false; std::copy(meta.key_origin.fingerprint, meta.key_origin.fingerprint + 4, info.fingerprint);
// Get the proper master key id info.path = meta.key_origin.path;
CKey key;
GetKey(meta.hd_seed_id, key);
CExtKey masterKey;
masterKey.SetSeed(key.begin(), key.size());
// Compute identifier
CKeyID masterid = masterKey.key.GetPubKey().GetID();
std::copy(masterid.begin(), masterid.begin() + 4, info.fingerprint);
} else { // Single pubkeys get the master fingerprint of themselves } else { // Single pubkeys get the master fingerprint of themselves
std::copy(keyID.begin(), keyID.begin() + 4, info.fingerprint); std::copy(keyID.begin(), keyID.begin() + 4, info.fingerprint);
} }
return true; return true;
} }
bool CWallet::AddKeyOrigin(const CPubKey& pubkey, const KeyOriginInfo& info)
{
LOCK(cs_wallet);
std::copy(info.fingerprint, info.fingerprint + 4, mapKeyMetadata[pubkey.GetID()].key_origin.fingerprint);
mapKeyMetadata[pubkey.GetID()].key_origin.path = info.path;
mapKeyMetadata[pubkey.GetID()].has_key_origin = true;
mapKeyMetadata[pubkey.GetID()].hdKeypath = WriteHDKeypath(info.path);
return WriteKeyMetadata(mapKeyMetadata[pubkey.GetID()], pubkey, true);
}

View file

@ -135,6 +135,9 @@ enum WalletFlags : uint64_t {
// wallet flags in the upper section (> 1 << 31) will lead to not opening the wallet if flag is unknown // wallet flags in the upper section (> 1 << 31) will lead to not opening the wallet if flag is unknown
// unknown wallet flags in the lower section <= (1 << 31) will be tolerated // unknown wallet flags in the lower section <= (1 << 31) will be tolerated
// Indicates that the metadata has already been upgraded to contain key origins
WALLET_FLAG_KEY_ORIGIN_METADATA = (1ULL << 1),
// will enforce the rule that the wallet can't contain any private keys (only watch-only/pubkeys) // will enforce the rule that the wallet can't contain any private keys (only watch-only/pubkeys)
WALLET_FLAG_DISABLE_PRIVATE_KEYS = (1ULL << 32), WALLET_FLAG_DISABLE_PRIVATE_KEYS = (1ULL << 32),
@ -151,7 +154,7 @@ enum WalletFlags : uint64_t {
WALLET_FLAG_BLANK_WALLET = (1ULL << 33), WALLET_FLAG_BLANK_WALLET = (1ULL << 33),
}; };
static constexpr uint64_t g_known_wallet_flags = WALLET_FLAG_DISABLE_PRIVATE_KEYS | WALLET_FLAG_BLANK_WALLET; static constexpr uint64_t g_known_wallet_flags = WALLET_FLAG_DISABLE_PRIVATE_KEYS | WALLET_FLAG_BLANK_WALLET | WALLET_FLAG_KEY_ORIGIN_METADATA;
/** A key pool entry */ /** A key pool entry */
class CKeyPool class CKeyPool
@ -776,6 +779,8 @@ public:
// Map from Script ID to key metadata (for watch-only keys). // Map from Script ID to key metadata (for watch-only keys).
std::map<CScriptID, CKeyMetadata> m_script_metadata GUARDED_BY(cs_wallet); std::map<CScriptID, CKeyMetadata> m_script_metadata GUARDED_BY(cs_wallet);
bool WriteKeyMetadata(const CKeyMetadata& meta, const CPubKey& pubkey, bool overwrite);
typedef std::map<unsigned int, CMasterKey> MasterKeyMap; typedef std::map<unsigned int, CMasterKey> MasterKeyMap;
MasterKeyMap mapMasterKeys; MasterKeyMap mapMasterKeys;
unsigned int nMasterKeyMaxID = 0; unsigned int nMasterKeyMaxID = 0;
@ -866,6 +871,8 @@ public:
//! Load metadata (used by LoadWallet) //! Load metadata (used by LoadWallet)
void LoadKeyMetadata(const CKeyID& keyID, const CKeyMetadata &metadata) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); void LoadKeyMetadata(const CKeyID& keyID, const CKeyMetadata &metadata) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
void LoadScriptMetadata(const CScriptID& script_id, const CKeyMetadata &metadata) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); void LoadScriptMetadata(const CScriptID& script_id, const CKeyMetadata &metadata) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
//! Upgrade stored CKeyMetadata objects to store key origin info as KeyOriginInfo
void UpgradeKeyMetadata() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
bool LoadMinVersion(int nVersion) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) { AssertLockHeld(cs_wallet); nWalletVersion = nVersion; nWalletMaxVersion = std::max(nWalletMaxVersion, nVersion); return true; } bool LoadMinVersion(int nVersion) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet) { AssertLockHeld(cs_wallet); nWalletVersion = nVersion; nWalletMaxVersion = std::max(nWalletMaxVersion, nVersion); return true; }
void UpdateTimeFirstKey(int64_t nCreateTime) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); void UpdateTimeFirstKey(int64_t nCreateTime) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
@ -1212,6 +1219,9 @@ public:
/** Implement lookup of key origin information through wallet key metadata. */ /** Implement lookup of key origin information through wallet key metadata. */
bool GetKeyOrigin(const CKeyID& keyid, KeyOriginInfo& info) const override; bool GetKeyOrigin(const CKeyID& keyid, KeyOriginInfo& info) const override;
/** Add a KeyOriginInfo to the wallet */
bool AddKeyOrigin(const CPubKey& pubkey, const KeyOriginInfo& info);
}; };
/** A key allocated from the key pool. */ /** A key allocated from the key pool. */

View file

@ -57,9 +57,14 @@ bool WalletBatch::EraseTx(uint256 hash)
return EraseIC(std::make_pair(std::string("tx"), hash)); return EraseIC(std::make_pair(std::string("tx"), hash));
} }
bool WalletBatch::WriteKeyMetadata(const CKeyMetadata& meta, const CPubKey& pubkey, const bool overwrite)
{
return WriteIC(std::make_pair(std::string("keymeta"), pubkey), meta, overwrite);
}
bool WalletBatch::WriteKey(const CPubKey& vchPubKey, const CPrivKey& vchPrivKey, const CKeyMetadata& keyMeta) bool WalletBatch::WriteKey(const CPubKey& vchPubKey, const CPrivKey& vchPrivKey, const CKeyMetadata& keyMeta)
{ {
if (!WriteIC(std::make_pair(std::string("keymeta"), vchPubKey), keyMeta, false)) { if (!WriteKeyMetadata(keyMeta, vchPubKey, false)) {
return false; return false;
} }
@ -76,7 +81,7 @@ bool WalletBatch::WriteCryptedKey(const CPubKey& vchPubKey,
const std::vector<unsigned char>& vchCryptedSecret, const std::vector<unsigned char>& vchCryptedSecret,
const CKeyMetadata &keyMeta) const CKeyMetadata &keyMeta)
{ {
if (!WriteIC(std::make_pair(std::string("keymeta"), vchPubKey), keyMeta)) { if (!WriteKeyMetadata(keyMeta, vchPubKey, true)) {
return false; return false;
} }
@ -529,6 +534,14 @@ DBErrors WalletBatch::LoadWallet(CWallet* pwallet)
if (wss.fAnyUnordered) if (wss.fAnyUnordered)
result = pwallet->ReorderTransactions(); result = pwallet->ReorderTransactions();
// Upgrade all of the wallet keymetadata to have the hd master key id
// This operation is not atomic, but if it fails, updated entries are still backwards compatible with older software
try {
pwallet->UpgradeKeyMetadata();
} catch (...) {
result = DBErrors::CORRUPT;
}
return result; return result;
} }

View file

@ -8,6 +8,7 @@
#include <amount.h> #include <amount.h>
#include <primitives/transaction.h> #include <primitives/transaction.h>
#include <script/sign.h>
#include <wallet/db.h> #include <wallet/db.h>
#include <key.h> #include <key.h>
@ -93,11 +94,14 @@ class CKeyMetadata
public: public:
static const int VERSION_BASIC=1; static const int VERSION_BASIC=1;
static const int VERSION_WITH_HDDATA=10; static const int VERSION_WITH_HDDATA=10;
static const int CURRENT_VERSION=VERSION_WITH_HDDATA; static const int VERSION_WITH_KEY_ORIGIN = 12;
static const int CURRENT_VERSION=VERSION_WITH_KEY_ORIGIN;
int nVersion; int nVersion;
int64_t nCreateTime; // 0 means unknown int64_t nCreateTime; // 0 means unknown
std::string hdKeypath; //optional HD/bip32 keypath std::string hdKeypath; //optional HD/bip32 keypath. Still used to determine whether a key is a seed. Also kept for backwards compatibility
CKeyID hd_seed_id; //id of the HD seed used to derive this key CKeyID hd_seed_id; //id of the HD seed used to derive this key
KeyOriginInfo key_origin; // Key origin info with path and fingerprint
bool has_key_origin = false; //< Whether the key_origin is useful
CKeyMetadata() CKeyMetadata()
{ {
@ -120,6 +124,11 @@ public:
READWRITE(hdKeypath); READWRITE(hdKeypath);
READWRITE(hd_seed_id); READWRITE(hd_seed_id);
} }
if (this->nVersion >= VERSION_WITH_KEY_ORIGIN)
{
READWRITE(key_origin);
READWRITE(has_key_origin);
}
} }
void SetNull() void SetNull()
@ -128,6 +137,8 @@ public:
nCreateTime = 0; nCreateTime = 0;
hdKeypath.clear(); hdKeypath.clear();
hd_seed_id.SetNull(); hd_seed_id.SetNull();
key_origin.clear();
has_key_origin = false;
} }
}; };
@ -177,6 +188,7 @@ public:
bool WriteTx(const CWalletTx& wtx); bool WriteTx(const CWalletTx& wtx);
bool EraseTx(uint256 hash); bool EraseTx(uint256 hash);
bool WriteKeyMetadata(const CKeyMetadata& meta, const CPubKey& pubkey, const bool overwrite);
bool WriteKey(const CPubKey& vchPubKey, const CPrivKey& vchPrivKey, const CKeyMetadata &keyMeta); bool WriteKey(const CPubKey& vchPubKey, const CPrivKey& vchPrivKey, const CKeyMetadata &keyMeta);
bool WriteCryptedKey(const CPubKey& vchPubKey, const std::vector<unsigned char>& vchCryptedSecret, const CKeyMetadata &keyMeta); bool WriteCryptedKey(const CPubKey& vchPubKey, const std::vector<unsigned char>& vchCryptedSecret, const CKeyMetadata &keyMeta);
bool WriteMasterKey(unsigned int nID, const CMasterKey& kMasterKey); bool WriteMasterKey(unsigned int nID, const CMasterKey& kMasterKey);

View file

@ -27,7 +27,6 @@ class WalletHDTest(BitcoinTestFramework):
def run_test(self): def run_test(self):
# Make sure we use hd, keep masterkeyid # Make sure we use hd, keep masterkeyid
masterkeyid = self.nodes[1].getwalletinfo()['hdseedid'] masterkeyid = self.nodes[1].getwalletinfo()['hdseedid']
assert_equal(masterkeyid, self.nodes[1].getwalletinfo()['hdmasterkeyid'])
assert_equal(len(masterkeyid), 40) assert_equal(len(masterkeyid), 40)
# create an internal key # create an internal key
@ -53,7 +52,6 @@ class WalletHDTest(BitcoinTestFramework):
hd_info = self.nodes[1].getaddressinfo(hd_add) hd_info = self.nodes[1].getaddressinfo(hd_add)
assert_equal(hd_info["hdkeypath"], "m/0'/0'/"+str(i)+"'") assert_equal(hd_info["hdkeypath"], "m/0'/0'/"+str(i)+"'")
assert_equal(hd_info["hdseedid"], masterkeyid) assert_equal(hd_info["hdseedid"], masterkeyid)
assert_equal(hd_info["hdmasterkeyid"], masterkeyid)
self.nodes[0].sendtoaddress(hd_add, 1) self.nodes[0].sendtoaddress(hd_add, 1)
self.nodes[0].generate(1) self.nodes[0].generate(1)
self.nodes[0].sendtoaddress(non_hd_add, 1) self.nodes[0].sendtoaddress(non_hd_add, 1)
@ -83,7 +81,6 @@ class WalletHDTest(BitcoinTestFramework):
hd_info_2 = self.nodes[1].getaddressinfo(hd_add_2) hd_info_2 = self.nodes[1].getaddressinfo(hd_add_2)
assert_equal(hd_info_2["hdkeypath"], "m/0'/0'/"+str(i)+"'") assert_equal(hd_info_2["hdkeypath"], "m/0'/0'/"+str(i)+"'")
assert_equal(hd_info_2["hdseedid"], masterkeyid) assert_equal(hd_info_2["hdseedid"], masterkeyid)
assert_equal(hd_info_2["hdmasterkeyid"], masterkeyid)
assert_equal(hd_add, hd_add_2) assert_equal(hd_add, hd_add_2)
connect_nodes_bi(self.nodes, 0, 1) connect_nodes_bi(self.nodes, 0, 1)
self.sync_all() self.sync_all()

View file

@ -626,5 +626,70 @@ class ImportMultiTest(BitcoinTestFramework):
iswatchonly=False) iswatchonly=False)
# Import pubkeys with key origin info
self.log.info("Addresses should have hd keypath and master key id after import with key origin")
pub_addr = self.nodes[1].getnewaddress()
pub_addr = self.nodes[1].getnewaddress()
info = self.nodes[1].getaddressinfo(pub_addr)
pub = info['pubkey']
pub_keypath = info['hdkeypath']
pub_fpr = info['hdmasterfingerprint']
result = self.nodes[0].importmulti(
[{
'desc' : "wpkh([" + pub_fpr + pub_keypath[1:] +"]" + pub + ")",
"timestamp": "now",
}]
)
assert result[0]['success']
pub_import_info = self.nodes[0].getaddressinfo(pub_addr)
assert_equal(pub_import_info['hdmasterfingerprint'], pub_fpr)
assert_equal(pub_import_info['pubkey'], pub)
assert_equal(pub_import_info['hdkeypath'], pub_keypath)
# Import privkeys with key origin info
priv_addr = self.nodes[1].getnewaddress()
info = self.nodes[1].getaddressinfo(priv_addr)
priv = self.nodes[1].dumpprivkey(priv_addr)
priv_keypath = info['hdkeypath']
priv_fpr = info['hdmasterfingerprint']
result = self.nodes[0].importmulti(
[{
'desc' : "wpkh([" + priv_fpr + priv_keypath[1:] + "]" + priv + ")",
"timestamp": "now",
}]
)
assert result[0]['success']
priv_import_info = self.nodes[0].getaddressinfo(priv_addr)
assert_equal(priv_import_info['hdmasterfingerprint'], priv_fpr)
assert_equal(priv_import_info['hdkeypath'], priv_keypath)
# Make sure the key origin info are still there after a restart
self.stop_nodes()
self.start_nodes()
import_info = self.nodes[0].getaddressinfo(pub_addr)
assert_equal(import_info['hdmasterfingerprint'], pub_fpr)
assert_equal(import_info['hdkeypath'], pub_keypath)
import_info = self.nodes[0].getaddressinfo(priv_addr)
assert_equal(import_info['hdmasterfingerprint'], priv_fpr)
assert_equal(import_info['hdkeypath'], priv_keypath)
# Check legacy import does not import key origin info
self.log.info("Legacy imports don't have key origin info")
pub_addr = self.nodes[1].getnewaddress()
info = self.nodes[1].getaddressinfo(pub_addr)
pub = info['pubkey']
result = self.nodes[0].importmulti(
[{
'scriptPubKey': {'address': pub_addr},
'pubkeys': [pub],
"timestamp": "now",
}]
)
assert result[0]['success']
pub_import_info = self.nodes[0].getaddressinfo(pub_addr)
assert_equal(pub_import_info['pubkey'], pub)
assert 'hdmasterfingerprint' not in pub_import_info
assert 'hdkeypath' not in pub_import_info
if __name__ == '__main__': if __name__ == '__main__':
ImportMultiTest().main() ImportMultiTest().main()

View file

@ -21,7 +21,6 @@ class KeyPoolTest(BitcoinTestFramework):
addr_before_encrypting = nodes[0].getnewaddress() addr_before_encrypting = nodes[0].getnewaddress()
addr_before_encrypting_data = nodes[0].getaddressinfo(addr_before_encrypting) addr_before_encrypting_data = nodes[0].getaddressinfo(addr_before_encrypting)
wallet_info_old = nodes[0].getwalletinfo() wallet_info_old = nodes[0].getwalletinfo()
assert_equal(wallet_info_old['hdseedid'], wallet_info_old['hdmasterkeyid'])
assert(addr_before_encrypting_data['hdseedid'] == wallet_info_old['hdseedid']) assert(addr_before_encrypting_data['hdseedid'] == wallet_info_old['hdseedid'])
# Encrypt wallet and wait to terminate # Encrypt wallet and wait to terminate
@ -30,7 +29,6 @@ class KeyPoolTest(BitcoinTestFramework):
addr = nodes[0].getnewaddress() addr = nodes[0].getnewaddress()
addr_data = nodes[0].getaddressinfo(addr) addr_data = nodes[0].getaddressinfo(addr)
wallet_info = nodes[0].getwalletinfo() wallet_info = nodes[0].getwalletinfo()
assert_equal(wallet_info['hdseedid'], wallet_info['hdmasterkeyid'])
assert(addr_before_encrypting_data['hdseedid'] != wallet_info['hdseedid']) assert(addr_before_encrypting_data['hdseedid'] != wallet_info['hdseedid'])
assert(addr_data['hdseedid'] == wallet_info['hdseedid']) assert(addr_data['hdseedid'] == wallet_info['hdseedid'])
assert_raises_rpc_error(-12, "Error: Keypool ran out, please call keypoolrefill first", nodes[0].getnewaddress) assert_raises_rpc_error(-12, "Error: Keypool ran out, please call keypoolrefill first", nodes[0].getnewaddress)