diff options
-rw-r--r-- | doc/release-notes-14021.md | 11 | ||||
-rw-r--r-- | src/Makefile.am | 2 | ||||
-rw-r--r-- | src/rpc/rawtransaction.cpp | 1 | ||||
-rw-r--r-- | src/script/descriptor.cpp | 17 | ||||
-rw-r--r-- | src/script/sign.h | 16 | ||||
-rw-r--r-- | src/util/bip32.cpp | 66 | ||||
-rw-r--r-- | src/util/bip32.h | 19 | ||||
-rw-r--r-- | src/util/strencodings.cpp | 41 | ||||
-rw-r--r-- | src/util/strencodings.h | 3 | ||||
-rw-r--r-- | src/wallet/rpcdump.cpp | 11 | ||||
-rw-r--r-- | src/wallet/rpcwallet.cpp | 11 | ||||
-rw-r--r-- | src/wallet/test/psbt_wallet_tests.cpp | 1 | ||||
-rw-r--r-- | src/wallet/wallet.cpp | 80 | ||||
-rw-r--r-- | src/wallet/wallet.h | 12 | ||||
-rw-r--r-- | src/wallet/walletdb.cpp | 17 | ||||
-rw-r--r-- | src/wallet/walletdb.h | 16 | ||||
-rwxr-xr-x | test/functional/wallet_hd.py | 3 | ||||
-rwxr-xr-x | test/functional/wallet_importmulti.py | 65 | ||||
-rwxr-xr-x | test/functional/wallet_keypool.py | 2 |
19 files changed, 307 insertions, 87 deletions
diff --git a/doc/release-notes-14021.md b/doc/release-notes-14021.md new file mode 100644 index 0000000000..4797a95bdb --- /dev/null +++ b/doc/release-notes-14021.md @@ -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. diff --git a/src/Makefile.am b/src/Makefile.am index 7490d8b790..e3f5734c0b 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -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 \ diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 6ec05565e9..4d8a1b87fc 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.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> diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp index 41e0f2e117..532a8028a2 100644 --- a/src/script/descriptor.cpp +++ b/src/script/descriptor.cpp @@ -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 += '\''; diff --git a/src/script/sign.h b/src/script/sign.h index 64eb3eb8e5..3e9a3b38c6 100644 --- a/src/script/sign.h +++ b/src/script/sign.h @@ -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. */ diff --git a/src/util/bip32.cpp b/src/util/bip32.cpp new file mode 100644 index 0000000000..6f176dd5ec --- /dev/null +++ b/src/util/bip32.cpp @@ -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); +} diff --git a/src/util/bip32.h b/src/util/bip32.h new file mode 100644 index 0000000000..7e58b79f38 --- /dev/null +++ b/src/util/bip32.h @@ -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 diff --git a/src/util/strencodings.cpp b/src/util/strencodings.cpp index b55547bc63..0acbb4f117 100644 --- a/src/util/strencodings.cpp +++ b/src/util/strencodings.cpp @@ -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);}); diff --git a/src/util/strencodings.h b/src/util/strencodings.h index 59eefff566..cf77044094 100644 --- a/src/util/strencodings.h +++ b/src/util/strencodings.h @@ -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 diff --git a/src/wallet/rpcdump.cpp b/src/wallet/rpcdump.cpp index ec49efcf22..5ceba39704 100644 --- a/src/wallet/rpcdump.cpp +++ b/src/wallet/rpcdump.cpp @@ -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) { diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index ee77739986..9b303cf239 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -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)); } } diff --git a/src/wallet/test/psbt_wallet_tests.cpp b/src/wallet/test/psbt_wallet_tests.cpp index e89d4121bc..2a3149de46 100644 --- a/src/wallet/test/psbt_wallet_tests.cpp +++ b/src/wallet/test/psbt_wallet_tests.cpp @@ -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> diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 063015d1d8..4df4e0981e 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -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,8 +497,11 @@ 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); +} diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 5846ac0f3e..3c5e475bd9 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -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. */ diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index 6e037808e3..2783f83fd6 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -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; } diff --git a/src/wallet/walletdb.h b/src/wallet/walletdb.h index 5584407a56..0532a55ff5 100644 --- a/src/wallet/walletdb.h +++ b/src/wallet/walletdb.h @@ -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); diff --git a/test/functional/wallet_hd.py b/test/functional/wallet_hd.py index eb42531693..61245e5104 100755 --- a/test/functional/wallet_hd.py +++ b/test/functional/wallet_hd.py @@ -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() diff --git a/test/functional/wallet_importmulti.py b/test/functional/wallet_importmulti.py index 7cce72b39f..ee59ef0966 100755 --- a/test/functional/wallet_importmulti.py +++ b/test/functional/wallet_importmulti.py @@ -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() diff --git a/test/functional/wallet_keypool.py b/test/functional/wallet_keypool.py index ceb9709712..1116196268 100755 --- a/test/functional/wallet_keypool.py +++ b/test/functional/wallet_keypool.py @@ -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) |