Skip to content

Commit ccd85b5

Browse files
committed
Merge #17681: wallet: Keep inactive seeds after sethdseed and derive keys from them as needed
1ed52fb Remove IBD check in sethdseed (Andrew Chow) b1810a1 Test that keys from inactive seeds are generated (Andrew Chow) c93082e Generate new keys for inactive seeds after marking used (Andrew Chow) 45f2f6a Determine inactive HD seeds from key metadata and track them in LegacyScriptPubKeyMan (Andrew Chow) b59b450 have GenerateNewKey and DeriveNewChildKey take a CHDChain as an argument (Andrew Chow) Pull request description: Largely implements the suggestion from bitcoin/bitcoin#17484 (comment). After `sethdseed` is called, the CHDChain for the old seed is kept in the wallet. It is kept on the file as a new `inactivehdseed` record and in memory in a map `m_inactive_hd_seeds`. In `LegacyScriptPubKeyMan::MarkUnusedAddresses` we check each used key's metadata for whether it was derived from an inactive seed. If it is, we then check to see how many keys after that key were derived from the inactive seed. If that number does not match the keypool parameter, we derive more keys from the inactive seed until it does match. This way we won't miss transactions belonging to keys outside of the range of the keypool initially. The indexes and internal-ness of a key is gotten by checking it's key origin data. Because of this change, we no longer need to wait for IBD to finish before `sethdseed` can work so that check is also removed. A test case for this is added as well which fails on master. ACKs for top commit: ryanofsky: Code review ACK 1ed52fb. Changes since last review: various commit message, code comment, log message, error checking improvements, and fix for topping up inactive seeds if wallet isn't reloaded after calling sethdseed and test for this ariard: Code Review ACK 1ed52fb jonatack: ACK 1ed52fb thanks for addressing the previous review feedback; would be happy to see the new review questions answered and feedback addressed and re-ack. Tree-SHA512: e658ae0e1dab94be55d2b62cdda506c94815e73a6881533fd30d41cc77477f82fee2095144957a3a1df0c129e256bdd7b7abe3737d515f393610446cae4edf1c
2 parents ad3a61c + 1ed52fb commit ccd85b5

File tree

6 files changed

+301
-32
lines changed

6 files changed

+301
-32
lines changed

src/wallet/rpcwallet.cpp

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3979,10 +3979,6 @@ UniValue sethdseed(const JSONRPCRequest& request)
39793979

39803980
LegacyScriptPubKeyMan& spk_man = EnsureLegacyScriptPubKeyMan(*pwallet, true);
39813981

3982-
if (pwallet->chain().isInitialBlockDownload()) {
3983-
throw JSONRPCError(RPC_CLIENT_IN_INITIAL_DOWNLOAD, "Cannot set a new HD seed while still in Initial Block Download");
3984-
}
3985-
39863982
if (pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
39873983
throw JSONRPCError(RPC_WALLET_ERROR, "Cannot set a HD seed to a wallet with private keys disabled");
39883984
}

src/wallet/scriptpubkeyman.cpp

Lines changed: 94 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
#include <util/translation.h>
1313
#include <wallet/scriptpubkeyman.h>
1414

15+
//! Value for the first BIP 32 hardened derivation. Can be used as a bit mask and as a value. See BIP 32 for more details.
16+
const uint32_t BIP32_HARDENED_KEY_LIMIT = 0x80000000;
17+
1518
bool LegacyScriptPubKeyMan::GetNewDestination(const OutputType type, CTxDestination& dest, std::string& error)
1619
{
1720
LOCK(cs_KeyStore);
@@ -295,20 +298,72 @@ bool LegacyScriptPubKeyMan::GetReservedDestination(const OutputType type, bool i
295298
return true;
296299
}
297300

301+
bool LegacyScriptPubKeyMan::TopUpInactiveHDChain(const CKeyID seed_id, int64_t index, bool internal)
302+
{
303+
LOCK(cs_KeyStore);
304+
305+
if (m_storage.IsLocked()) return false;
306+
307+
auto it = m_inactive_hd_chains.find(seed_id);
308+
if (it == m_inactive_hd_chains.end()) {
309+
return false;
310+
}
311+
312+
CHDChain& chain = it->second;
313+
314+
// Top up key pool
315+
int64_t target_size = std::max(gArgs.GetArg("-keypool", DEFAULT_KEYPOOL_SIZE), (int64_t) 1);
316+
317+
// "size" of the keypools. Not really the size, actually the difference between index and the chain counter
318+
// Since chain counter is 1 based and index is 0 based, one of them needs to be offset by 1.
319+
int64_t kp_size = (internal ? chain.nInternalChainCounter : chain.nExternalChainCounter) - (index + 1);
320+
321+
// make sure the keypool fits the user-selected target (-keypool)
322+
int64_t missing = std::max(target_size - kp_size, (int64_t) 0);
323+
324+
if (missing > 0) {
325+
WalletBatch batch(m_storage.GetDatabase());
326+
for (int64_t i = missing; i > 0; --i) {
327+
GenerateNewKey(batch, chain, internal);
328+
}
329+
if (internal) {
330+
WalletLogPrintf("inactive seed with id %s added %d internal keys\n", HexStr(seed_id), missing);
331+
} else {
332+
WalletLogPrintf("inactive seed with id %s added %d keys\n", HexStr(seed_id), missing);
333+
}
334+
}
335+
return true;
336+
}
337+
298338
void LegacyScriptPubKeyMan::MarkUnusedAddresses(const CScript& script)
299339
{
300340
LOCK(cs_KeyStore);
301341
// extract addresses and check if they match with an unused keypool key
302342
for (const auto& keyid : GetAffectedKeys(script, *this)) {
303343
std::map<CKeyID, int64_t>::const_iterator mi = m_pool_key_to_index.find(keyid);
304344
if (mi != m_pool_key_to_index.end()) {
305-
WalletLogPrintf("%s: Detected a used keypool key, mark all keypool key up to this key as used\n", __func__);
345+
WalletLogPrintf("%s: Detected a used keypool key, mark all keypool keys up to this key as used\n", __func__);
306346
MarkReserveKeysAsUsed(mi->second);
307347

308348
if (!TopUp()) {
309349
WalletLogPrintf("%s: Topping up keypool failed (locked wallet)\n", __func__);
310350
}
311351
}
352+
353+
// Find the key's metadata and check if it's seed id (if it has one) is inactive, i.e. it is not the current m_hd_chain seed id.
354+
// If so, TopUp the inactive hd chain
355+
auto it = mapKeyMetadata.find(keyid);
356+
if (it != mapKeyMetadata.end()){
357+
CKeyMetadata meta = it->second;
358+
if (!meta.hd_seed_id.IsNull() && meta.hd_seed_id != m_hd_chain.seed_id) {
359+
bool internal = (meta.key_origin.path[1] & ~BIP32_HARDENED_KEY_LIMIT) != 0;
360+
int64_t index = meta.key_origin.path[2] & ~BIP32_HARDENED_KEY_LIMIT;
361+
362+
if (!TopUpInactiveHDChain(meta.hd_seed_id, index, internal)) {
363+
WalletLogPrintf("%s: Adding inactive seed keys failed\n", __func__);
364+
}
365+
}
366+
}
312367
}
313368
}
314369

@@ -362,7 +417,7 @@ bool LegacyScriptPubKeyMan::SetupGeneration(bool force)
362417

363418
bool LegacyScriptPubKeyMan::IsHDEnabled() const
364419
{
365-
return !hdChain.seed_id.IsNull();
420+
return !m_hd_chain.seed_id.IsNull();
366421
}
367422

368423
bool LegacyScriptPubKeyMan::CanGetAddresses(bool internal) const
@@ -848,10 +903,27 @@ bool LegacyScriptPubKeyMan::AddWatchOnly(const CScript& dest, int64_t nCreateTim
848903
void LegacyScriptPubKeyMan::SetHDChain(const CHDChain& chain, bool memonly)
849904
{
850905
LOCK(cs_KeyStore);
851-
if (!memonly && !WalletBatch(m_storage.GetDatabase()).WriteHDChain(chain))
852-
throw std::runtime_error(std::string(__func__) + ": writing chain failed");
906+
// memonly == true means we are loading the wallet file
907+
// memonly == false means that the chain is actually being changed
908+
if (!memonly) {
909+
// Store the new chain
910+
if (!WalletBatch(m_storage.GetDatabase()).WriteHDChain(chain)) {
911+
throw std::runtime_error(std::string(__func__) + ": writing chain failed");
912+
}
913+
// When there's an old chain, add it as an inactive chain as we are now rotating hd chains
914+
if (!m_hd_chain.seed_id.IsNull()) {
915+
AddInactiveHDChain(m_hd_chain);
916+
}
917+
}
853918

854-
hdChain = chain;
919+
m_hd_chain = chain;
920+
}
921+
922+
void LegacyScriptPubKeyMan::AddInactiveHDChain(const CHDChain& chain)
923+
{
924+
LOCK(cs_KeyStore);
925+
assert(!chain.seed_id.IsNull());
926+
m_inactive_hd_chains[chain.seed_id] = chain;
855927
}
856928

857929
bool LegacyScriptPubKeyMan::HaveKey(const CKeyID &address) const
@@ -930,7 +1002,7 @@ bool LegacyScriptPubKeyMan::GetPubKey(const CKeyID &address, CPubKey& vchPubKeyO
9301002
return GetWatchPubKey(address, vchPubKeyOut);
9311003
}
9321004

933-
CPubKey LegacyScriptPubKeyMan::GenerateNewKey(WalletBatch &batch, bool internal)
1005+
CPubKey LegacyScriptPubKeyMan::GenerateNewKey(WalletBatch &batch, CHDChain& hd_chain, bool internal)
9341006
{
9351007
assert(!m_storage.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS));
9361008
assert(!m_storage.IsWalletFlagSet(WALLET_FLAG_BLANK_WALLET));
@@ -945,7 +1017,7 @@ CPubKey LegacyScriptPubKeyMan::GenerateNewKey(WalletBatch &batch, bool internal)
9451017

9461018
// use HD key derivation if HD was enabled during wallet creation and a seed is present
9471019
if (IsHDEnabled()) {
948-
DeriveNewChildKey(batch, metadata, secret, (m_storage.CanSupportFeature(FEATURE_HD_SPLIT) ? internal : false));
1020+
DeriveNewChildKey(batch, metadata, secret, hd_chain, (m_storage.CanSupportFeature(FEATURE_HD_SPLIT) ? internal : false));
9491021
} else {
9501022
secret.MakeNewKey(fCompressed);
9511023
}
@@ -967,9 +1039,7 @@ CPubKey LegacyScriptPubKeyMan::GenerateNewKey(WalletBatch &batch, bool internal)
9671039
return pubkey;
9681040
}
9691041

970-
const uint32_t BIP32_HARDENED_KEY_LIMIT = 0x80000000;
971-
972-
void LegacyScriptPubKeyMan::DeriveNewChildKey(WalletBatch &batch, CKeyMetadata& metadata, CKey& secret, bool internal)
1042+
void LegacyScriptPubKeyMan::DeriveNewChildKey(WalletBatch &batch, CKeyMetadata& metadata, CKey& secret, CHDChain& hd_chain, bool internal)
9731043
{
9741044
// for now we use a fixed keypath scheme of m/0'/0'/k
9751045
CKey seed; //seed (256bit)
@@ -979,7 +1049,7 @@ void LegacyScriptPubKeyMan::DeriveNewChildKey(WalletBatch &batch, CKeyMetadata&
9791049
CExtKey childKey; //key at m/0'/0'/<n>'
9801050

9811051
// try to get the seed
982-
if (!GetKey(hdChain.seed_id, seed))
1052+
if (!GetKey(hd_chain.seed_id, seed))
9831053
throw std::runtime_error(std::string(__func__) + ": seed not found");
9841054

9851055
masterKey.SetSeed(seed.begin(), seed.size());
@@ -998,30 +1068,30 @@ void LegacyScriptPubKeyMan::DeriveNewChildKey(WalletBatch &batch, CKeyMetadata&
9981068
// childIndex | BIP32_HARDENED_KEY_LIMIT = derive childIndex in hardened child-index-range
9991069
// example: 1 | BIP32_HARDENED_KEY_LIMIT == 0x80000001 == 2147483649
10001070
if (internal) {
1001-
chainChildKey.Derive(childKey, hdChain.nInternalChainCounter | BIP32_HARDENED_KEY_LIMIT);
1002-
metadata.hdKeypath = "m/0'/1'/" + ToString(hdChain.nInternalChainCounter) + "'";
1071+
chainChildKey.Derive(childKey, hd_chain.nInternalChainCounter | BIP32_HARDENED_KEY_LIMIT);
1072+
metadata.hdKeypath = "m/0'/1'/" + ToString(hd_chain.nInternalChainCounter) + "'";
10031073
metadata.key_origin.path.push_back(0 | BIP32_HARDENED_KEY_LIMIT);
10041074
metadata.key_origin.path.push_back(1 | BIP32_HARDENED_KEY_LIMIT);
1005-
metadata.key_origin.path.push_back(hdChain.nInternalChainCounter | BIP32_HARDENED_KEY_LIMIT);
1006-
hdChain.nInternalChainCounter++;
1075+
metadata.key_origin.path.push_back(hd_chain.nInternalChainCounter | BIP32_HARDENED_KEY_LIMIT);
1076+
hd_chain.nInternalChainCounter++;
10071077
}
10081078
else {
1009-
chainChildKey.Derive(childKey, hdChain.nExternalChainCounter | BIP32_HARDENED_KEY_LIMIT);
1010-
metadata.hdKeypath = "m/0'/0'/" + ToString(hdChain.nExternalChainCounter) + "'";
1079+
chainChildKey.Derive(childKey, hd_chain.nExternalChainCounter | BIP32_HARDENED_KEY_LIMIT);
1080+
metadata.hdKeypath = "m/0'/0'/" + ToString(hd_chain.nExternalChainCounter) + "'";
10111081
metadata.key_origin.path.push_back(0 | BIP32_HARDENED_KEY_LIMIT);
10121082
metadata.key_origin.path.push_back(0 | BIP32_HARDENED_KEY_LIMIT);
1013-
metadata.key_origin.path.push_back(hdChain.nExternalChainCounter | BIP32_HARDENED_KEY_LIMIT);
1014-
hdChain.nExternalChainCounter++;
1083+
metadata.key_origin.path.push_back(hd_chain.nExternalChainCounter | BIP32_HARDENED_KEY_LIMIT);
1084+
hd_chain.nExternalChainCounter++;
10151085
}
10161086
} while (HaveKey(childKey.key.GetPubKey().GetID()));
10171087
secret = childKey.key;
1018-
metadata.hd_seed_id = hdChain.seed_id;
1088+
metadata.hd_seed_id = hd_chain.seed_id;
10191089
CKeyID master_id = masterKey.key.GetPubKey().GetID();
10201090
std::copy(master_id.begin(), master_id.begin() + 4, metadata.key_origin.fingerprint);
10211091
metadata.has_key_origin = true;
10221092
// update the chain model in the database
1023-
if (!batch.WriteHDChain(hdChain))
1024-
throw std::runtime_error(std::string(__func__) + ": Writing HD chain model failed");
1093+
if (hd_chain.seed_id == m_hd_chain.seed_id && !batch.WriteHDChain(hd_chain))
1094+
throw std::runtime_error(std::string(__func__) + ": writing HD chain model failed");
10251095
}
10261096

10271097
void LegacyScriptPubKeyMan::LoadKeyPool(int64_t nIndex, const CKeyPool &keypool)
@@ -1176,7 +1246,7 @@ bool LegacyScriptPubKeyMan::TopUp(unsigned int kpSize)
11761246
internal = true;
11771247
}
11781248

1179-
CPubKey pubkey(GenerateNewKey(batch, internal));
1249+
CPubKey pubkey(GenerateNewKey(batch, m_hd_chain, internal));
11801250
AddKeypoolPubkeyWithDB(pubkey, internal, batch);
11811251
}
11821252
if (missingInternal + missingExternal > 0) {
@@ -1249,7 +1319,7 @@ bool LegacyScriptPubKeyMan::GetKeyFromPool(CPubKey& result, const OutputType typ
12491319
if (!ReserveKeyFromKeyPool(nIndex, keypool, internal) && !m_storage.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
12501320
if (m_storage.IsLocked()) return false;
12511321
WalletBatch batch(m_storage.GetDatabase());
1252-
result = GenerateNewKey(batch, internal);
1322+
result = GenerateNewKey(batch, m_hd_chain, internal);
12531323
return true;
12541324
}
12551325
KeepDestination(nIndex, type);

src/wallet/scriptpubkeyman.h

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
#include <boost/signals2/signal.hpp>
2020

21+
#include <unordered_map>
22+
2123
enum class OutputType;
2224
struct bilingual_str;
2325

@@ -144,6 +146,17 @@ class CKeyPool
144146
}
145147
};
146148

149+
class KeyIDHasher
150+
{
151+
public:
152+
KeyIDHasher() {}
153+
154+
size_t operator()(const CKeyID& id) const
155+
{
156+
return id.GetUint64(0);
157+
}
158+
};
159+
147160
/*
148161
* A class implementing ScriptPubKeyMan manages some (or all) scriptPubKeys used in a wallet.
149162
* It contains the scripts and keys related to the scriptPubKeys it manages.
@@ -288,10 +301,11 @@ class LegacyScriptPubKeyMan : public ScriptPubKeyMan, public FillableSigningProv
288301
bool AddKeyOriginWithDB(WalletBatch& batch, const CPubKey& pubkey, const KeyOriginInfo& info);
289302

290303
/* the HD chain data model (external chain counters) */
291-
CHDChain hdChain;
304+
CHDChain m_hd_chain;
305+
std::unordered_map<CKeyID, CHDChain, KeyIDHasher> m_inactive_hd_chains;
292306

293307
/* HD derive new child key (on internal or external chain) */
294-
void DeriveNewChildKey(WalletBatch& batch, CKeyMetadata& metadata, CKey& secret, bool internal = false) EXCLUSIVE_LOCKS_REQUIRED(cs_KeyStore);
308+
void DeriveNewChildKey(WalletBatch& batch, CKeyMetadata& metadata, CKey& secret, CHDChain& hd_chain, bool internal = false) EXCLUSIVE_LOCKS_REQUIRED(cs_KeyStore);
295309

296310
std::set<int64_t> setInternalKeyPool GUARDED_BY(cs_KeyStore);
297311
std::set<int64_t> setExternalKeyPool GUARDED_BY(cs_KeyStore);
@@ -320,6 +334,18 @@ class LegacyScriptPubKeyMan : public ScriptPubKeyMan, public FillableSigningProv
320334
*/
321335
bool ReserveKeyFromKeyPool(int64_t& nIndex, CKeyPool& keypool, bool fRequestedInternal);
322336

337+
/**
338+
* Like TopUp() but adds keys for inactive HD chains.
339+
* Ensures that there are at least -keypool number of keys derived after the given index.
340+
*
341+
* @param seed_id the CKeyID for the HD seed.
342+
* @param index the index to start generating keys from
343+
* @param internal whether the internal chain should be used. true for internal chain, false for external chain.
344+
*
345+
* @return true if seed was found and keys were derived. false if unable to derive seeds
346+
*/
347+
bool TopUpInactiveHDChain(const CKeyID seed_id, int64_t index, bool internal);
348+
323349
public:
324350
using ScriptPubKeyMan::ScriptPubKeyMan;
325351

@@ -393,11 +419,12 @@ class LegacyScriptPubKeyMan : public ScriptPubKeyMan, public FillableSigningProv
393419
void LoadKeyMetadata(const CKeyID& keyID, const CKeyMetadata &metadata);
394420
void LoadScriptMetadata(const CScriptID& script_id, const CKeyMetadata &metadata);
395421
//! Generate a new key
396-
CPubKey GenerateNewKey(WalletBatch& batch, bool internal = false) EXCLUSIVE_LOCKS_REQUIRED(cs_KeyStore);
422+
CPubKey GenerateNewKey(WalletBatch& batch, CHDChain& hd_chain, bool internal = false) EXCLUSIVE_LOCKS_REQUIRED(cs_KeyStore);
397423

398424
/* Set the HD chain model (chain child index counters) */
399425
void SetHDChain(const CHDChain& chain, bool memonly);
400-
const CHDChain& GetHDChain() const { return hdChain; }
426+
const CHDChain& GetHDChain() const { return m_hd_chain; }
427+
void AddInactiveHDChain(const CHDChain& chain);
401428

402429
//! Adds a watch-only address to the store, without saving it to disk (used by LoadWallet)
403430
bool LoadWatchOnly(const CScript &dest);

0 commit comments

Comments
 (0)