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:
commit
8d0ec74801
19 changed files with 307 additions and 87 deletions
11
doc/release-notes-14021.md
Normal file
11
doc/release-notes-14021.md
Normal 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.
|
|
@ -196,6 +196,7 @@ BITCOIN_CORE_H = \
|
|||
txmempool.h \
|
||||
ui_interface.h \
|
||||
undo.h \
|
||||
util/bip32.h \
|
||||
util/bytevectorhash.h \
|
||||
util/system.h \
|
||||
util/memory.h \
|
||||
|
@ -456,6 +457,7 @@ libbitcoin_util_a_SOURCES = \
|
|||
support/cleanse.cpp \
|
||||
sync.cpp \
|
||||
threadinterrupt.cpp \
|
||||
util/bip32.cpp \
|
||||
util/bytevectorhash.cpp \
|
||||
util/system.cpp \
|
||||
util/moneystr.cpp \
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
#include <script/sign.h>
|
||||
#include <script/standard.h>
|
||||
#include <uint256.h>
|
||||
#include <util/bip32.h>
|
||||
#include <util/strencodings.h>
|
||||
#include <validation.h>
|
||||
#include <validationinterface.h>
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include <script/standard.h>
|
||||
|
||||
#include <span.h>
|
||||
#include <util/bip32.h>
|
||||
#include <util/system.h>
|
||||
#include <util/strencodings.h>
|
||||
|
||||
|
@ -25,16 +26,6 @@ namespace {
|
|||
|
||||
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. */
|
||||
struct PubkeyProvider
|
||||
{
|
||||
|
@ -63,7 +54,7 @@ class OriginPubkeyProvider final : public PubkeyProvider
|
|||
|
||||
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:
|
||||
|
@ -184,7 +175,7 @@ public:
|
|||
}
|
||||
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()) {
|
||||
ret += "/*";
|
||||
if (m_derive == DeriveType::HARDENED) ret += '\'';
|
||||
|
@ -195,7 +186,7 @@ public:
|
|||
{
|
||||
CExtKey key;
|
||||
if (!GetExtKey(arg, key)) return false;
|
||||
out = EncodeExtKey(key) + FormatKeyPath(m_path);
|
||||
out = EncodeExtKey(key) + FormatHDKeypath(m_path);
|
||||
if (IsRange()) {
|
||||
out += "/*";
|
||||
if (m_derive == DeriveType::HARDENED) out += '\'';
|
||||
|
|
|
@ -22,13 +22,27 @@ struct CMutableTransaction;
|
|||
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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. */
|
||||
|
|
66
src/util/bip32.cpp
Normal file
66
src/util/bip32.cpp
Normal 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
19
src/util/bip32.h
Normal 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
|
|
@ -546,47 +546,6 @@ bool ParseFixedPoint(const std::string &val, int decimals, int64_t *amount_out)
|
|||
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)
|
||||
{
|
||||
std::transform(str.begin(), str.end(), str.begin(), [](char c){return ToLower(c);});
|
||||
|
|
|
@ -197,9 +197,6 @@ bool ConvertBits(const O& outfn, I it, I end) {
|
|||
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.
|
||||
* This function is locale independent. It only converts uppercase
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
#include <script/script.h>
|
||||
#include <script/standard.h>
|
||||
#include <sync.h>
|
||||
#include <util/bip32.h>
|
||||
#include <util/system.h>
|
||||
#include <util/time.h>
|
||||
#include <validation.h>
|
||||
|
@ -850,7 +851,7 @@ UniValue dumpwallet(const JSONRPCRequest& request)
|
|||
} else {
|
||||
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";
|
||||
|
@ -887,6 +888,7 @@ struct ImportData
|
|||
// Output data
|
||||
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, KeyOriginInfo> key_origins;
|
||||
};
|
||||
|
||||
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()));
|
||||
|
||||
import_data.key_origins.insert(out_keys.origins.begin(), out_keys.origins.end());
|
||||
for (size_t i = 0; i < priv_keys.size(); ++i) {
|
||||
const auto& str = priv_keys[i].get_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)) {
|
||||
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) {
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
#include <script/sign.h>
|
||||
#include <shutdown.h>
|
||||
#include <timedata.h>
|
||||
#include <util/bip32.h>
|
||||
#include <util/system.h>
|
||||
#include <util/moneystr.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"
|
||||
" \"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"
|
||||
" \"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"
|
||||
"}\n"
|
||||
},
|
||||
|
@ -2456,7 +2456,6 @@ static UniValue getwalletinfo(const JSONRPCRequest& request)
|
|||
obj.pushKV("paytxfee", ValueFromAmount(pwallet->m_pay_tx_fee.GetFeePerK()));
|
||||
if (!seed_id.IsNull()) {
|
||||
obj.pushKV("hdseedid", seed_id.GetHex());
|
||||
obj.pushKV("hdmasterkeyid", seed_id.GetHex());
|
||||
}
|
||||
obj.pushKV("private_keys_enabled", !pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS));
|
||||
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"
|
||||
" \"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"
|
||||
" \"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"
|
||||
" [\n"
|
||||
" { (json object of label data)\n"
|
||||
|
@ -3747,10 +3746,10 @@ UniValue getaddressinfo(const JSONRPCRequest& request)
|
|||
}
|
||||
if (meta) {
|
||||
ret.pushKV("timestamp", meta->nCreateTime);
|
||||
if (!meta->hdKeypath.empty()) {
|
||||
ret.pushKV("hdkeypath", meta->hdKeypath);
|
||||
if (meta->has_key_origin) {
|
||||
ret.pushKV("hdkeypath", WriteHDKeypath(meta->key_origin.path));
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
#include <key_io.h>
|
||||
#include <script/sign.h>
|
||||
#include <util/bip32.h>
|
||||
#include <util/strencodings.h>
|
||||
#include <wallet/psbtwallet.h>
|
||||
#include <wallet/rpcwallet.h>
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
#include <shutdown.h>
|
||||
#include <timedata.h>
|
||||
#include <txmempool.h>
|
||||
#include <util/bip32.h>
|
||||
#include <util/moneystr.h>
|
||||
#include <wallet/fees.h>
|
||||
|
||||
|
@ -255,16 +256,25 @@ void CWallet::DeriveNewChildKey(WalletBatch &batch, CKeyMetadata& metadata, CKey
|
|||
if (internal) {
|
||||
chainChildKey.Derive(childKey, hdChain.nInternalChainCounter | BIP32_HARDENED_KEY_LIMIT);
|
||||
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++;
|
||||
}
|
||||
else {
|
||||
chainChildKey.Derive(childKey, hdChain.nExternalChainCounter | BIP32_HARDENED_KEY_LIMIT);
|
||||
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++;
|
||||
}
|
||||
} while (HaveKey(childKey.key.GetPubKey().GetID()));
|
||||
secret = childKey.key;
|
||||
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
|
||||
if (!batch.WriteHDChain(hdChain))
|
||||
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;
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
return CCryptoKeyStore::AddCryptedKey(vchPubKey, vchCryptedSecret);
|
||||
|
@ -446,10 +497,13 @@ bool CWallet::Unlock(const SecureString& strWalletPassphrase, bool accept_no_key
|
|||
return false;
|
||||
if (!crypter.Decrypt(pMasterKey.second.vchCryptedKey, _vMasterKey))
|
||||
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 false;
|
||||
}
|
||||
|
||||
|
@ -1407,6 +1461,7 @@ CPubKey CWallet::DeriveNewSeed(const CKey& key)
|
|||
|
||||
// set the hd keypath to "s" -> Seed, refers the seed to itself
|
||||
metadata.hdKeypath = "s";
|
||||
metadata.has_key_origin = false;
|
||||
metadata.hd_seed_id = seed.GetID();
|
||||
|
||||
{
|
||||
|
@ -4487,18 +4542,21 @@ bool CWallet::GetKeyOrigin(const CKeyID& keyID, KeyOriginInfo& info) const
|
|||
meta = it->second;
|
||||
}
|
||||
}
|
||||
if (!meta.hdKeypath.empty()) {
|
||||
if (!ParseHDKeypath(meta.hdKeypath, info.path)) return false;
|
||||
// Get the proper master key id
|
||||
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);
|
||||
if (meta.has_key_origin) {
|
||||
std::copy(meta.key_origin.fingerprint, meta.key_origin.fingerprint + 4, info.fingerprint);
|
||||
info.path = meta.key_origin.path;
|
||||
} else { // Single pubkeys get the master fingerprint of themselves
|
||||
std::copy(keyID.begin(), keyID.begin() + 4, info.fingerprint);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
// 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)
|
||||
WALLET_FLAG_DISABLE_PRIVATE_KEYS = (1ULL << 32),
|
||||
|
||||
|
@ -151,7 +154,7 @@ enum WalletFlags : uint64_t {
|
|||
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 */
|
||||
class CKeyPool
|
||||
|
@ -776,6 +779,8 @@ public:
|
|||
// Map from Script ID to key metadata (for watch-only keys).
|
||||
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;
|
||||
MasterKeyMap mapMasterKeys;
|
||||
unsigned int nMasterKeyMaxID = 0;
|
||||
|
@ -866,6 +871,8 @@ public:
|
|||
//! Load metadata (used by LoadWallet)
|
||||
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);
|
||||
//! 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; }
|
||||
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. */
|
||||
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. */
|
||||
|
|
|
@ -57,9 +57,14 @@ bool WalletBatch::EraseTx(uint256 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)
|
||||
{
|
||||
if (!WriteIC(std::make_pair(std::string("keymeta"), vchPubKey), keyMeta, false)) {
|
||||
if (!WriteKeyMetadata(keyMeta, vchPubKey, false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -76,7 +81,7 @@ bool WalletBatch::WriteCryptedKey(const CPubKey& vchPubKey,
|
|||
const std::vector<unsigned char>& vchCryptedSecret,
|
||||
const CKeyMetadata &keyMeta)
|
||||
{
|
||||
if (!WriteIC(std::make_pair(std::string("keymeta"), vchPubKey), keyMeta)) {
|
||||
if (!WriteKeyMetadata(keyMeta, vchPubKey, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -529,6 +534,14 @@ DBErrors WalletBatch::LoadWallet(CWallet* pwallet)
|
|||
if (wss.fAnyUnordered)
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
#include <amount.h>
|
||||
#include <primitives/transaction.h>
|
||||
#include <script/sign.h>
|
||||
#include <wallet/db.h>
|
||||
#include <key.h>
|
||||
|
||||
|
@ -93,11 +94,14 @@ class CKeyMetadata
|
|||
public:
|
||||
static const int VERSION_BASIC=1;
|
||||
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;
|
||||
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
|
||||
KeyOriginInfo key_origin; // Key origin info with path and fingerprint
|
||||
bool has_key_origin = false; //< Whether the key_origin is useful
|
||||
|
||||
CKeyMetadata()
|
||||
{
|
||||
|
@ -120,6 +124,11 @@ public:
|
|||
READWRITE(hdKeypath);
|
||||
READWRITE(hd_seed_id);
|
||||
}
|
||||
if (this->nVersion >= VERSION_WITH_KEY_ORIGIN)
|
||||
{
|
||||
READWRITE(key_origin);
|
||||
READWRITE(has_key_origin);
|
||||
}
|
||||
}
|
||||
|
||||
void SetNull()
|
||||
|
@ -128,6 +137,8 @@ public:
|
|||
nCreateTime = 0;
|
||||
hdKeypath.clear();
|
||||
hd_seed_id.SetNull();
|
||||
key_origin.clear();
|
||||
has_key_origin = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -177,6 +188,7 @@ public:
|
|||
bool WriteTx(const CWalletTx& wtx);
|
||||
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 WriteCryptedKey(const CPubKey& vchPubKey, const std::vector<unsigned char>& vchCryptedSecret, const CKeyMetadata &keyMeta);
|
||||
bool WriteMasterKey(unsigned int nID, const CMasterKey& kMasterKey);
|
||||
|
|
|
@ -27,7 +27,6 @@ class WalletHDTest(BitcoinTestFramework):
|
|||
def run_test(self):
|
||||
# Make sure we use hd, keep masterkeyid
|
||||
masterkeyid = self.nodes[1].getwalletinfo()['hdseedid']
|
||||
assert_equal(masterkeyid, self.nodes[1].getwalletinfo()['hdmasterkeyid'])
|
||||
assert_equal(len(masterkeyid), 40)
|
||||
|
||||
# create an internal key
|
||||
|
@ -53,7 +52,6 @@ class WalletHDTest(BitcoinTestFramework):
|
|||
hd_info = self.nodes[1].getaddressinfo(hd_add)
|
||||
assert_equal(hd_info["hdkeypath"], "m/0'/0'/"+str(i)+"'")
|
||||
assert_equal(hd_info["hdseedid"], masterkeyid)
|
||||
assert_equal(hd_info["hdmasterkeyid"], masterkeyid)
|
||||
self.nodes[0].sendtoaddress(hd_add, 1)
|
||||
self.nodes[0].generate(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)
|
||||
assert_equal(hd_info_2["hdkeypath"], "m/0'/0'/"+str(i)+"'")
|
||||
assert_equal(hd_info_2["hdseedid"], masterkeyid)
|
||||
assert_equal(hd_info_2["hdmasterkeyid"], masterkeyid)
|
||||
assert_equal(hd_add, hd_add_2)
|
||||
connect_nodes_bi(self.nodes, 0, 1)
|
||||
self.sync_all()
|
||||
|
|
|
@ -626,5 +626,70 @@ class ImportMultiTest(BitcoinTestFramework):
|
|||
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__':
|
||||
ImportMultiTest().main()
|
||||
|
|
|
@ -21,7 +21,6 @@ class KeyPoolTest(BitcoinTestFramework):
|
|||
addr_before_encrypting = nodes[0].getnewaddress()
|
||||
addr_before_encrypting_data = nodes[0].getaddressinfo(addr_before_encrypting)
|
||||
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'])
|
||||
|
||||
# Encrypt wallet and wait to terminate
|
||||
|
@ -30,7 +29,6 @@ class KeyPoolTest(BitcoinTestFramework):
|
|||
addr = nodes[0].getnewaddress()
|
||||
addr_data = nodes[0].getaddressinfo(addr)
|
||||
wallet_info = nodes[0].getwalletinfo()
|
||||
assert_equal(wallet_info['hdseedid'], wallet_info['hdmasterkeyid'])
|
||||
assert(addr_before_encrypting_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)
|
||||
|
|
Loading…
Reference in a new issue