Skip to content

Commit 7a72536

Browse files
committed
Merge 267917f into merged_master (Bitcoin PR bitcoin/bitcoin#23304)
2 parents 1dc6ad6 + 267917f commit 7a72536

File tree

7 files changed

+234
-55
lines changed

7 files changed

+234
-55
lines changed

src/wallet/scriptpubkeyman.cpp

+78-53
Original file line numberDiff line numberDiff line change
@@ -329,36 +329,21 @@ bool LegacyScriptPubKeyMan::TopUpInactiveHDChain(const CKeyID seed_id, int64_t i
329329
{
330330
LOCK(cs_KeyStore);
331331

332-
if (m_storage.IsLocked()) return false;
333-
334332
auto it = m_inactive_hd_chains.find(seed_id);
335333
if (it == m_inactive_hd_chains.end()) {
336334
return false;
337335
}
338336

339337
CHDChain& chain = it->second;
340338

341-
// Top up key pool
342-
int64_t target_size = std::max(gArgs.GetIntArg("-keypool", DEFAULT_KEYPOOL_SIZE), (int64_t) 1);
343-
344-
// "size" of the keypools. Not really the size, actually the difference between index and the chain counter
345-
// Since chain counter is 1 based and index is 0 based, one of them needs to be offset by 1.
346-
int64_t kp_size = (internal ? chain.nInternalChainCounter : chain.nExternalChainCounter) - (index + 1);
339+
if (internal) {
340+
chain.m_next_internal_index = std::max(chain.m_next_internal_index, index + 1);
341+
} else {
342+
chain.m_next_external_index = std::max(chain.m_next_external_index, index + 1);
343+
}
347344

348-
// make sure the keypool fits the user-selected target (-keypool)
349-
int64_t missing = std::max(target_size - kp_size, (int64_t) 0);
345+
TopUpChain(chain, 0);
350346

351-
if (missing > 0) {
352-
WalletBatch batch(m_storage.GetDatabase());
353-
for (int64_t i = missing; i > 0; --i) {
354-
GenerateNewKey(batch, chain, internal);
355-
}
356-
if (internal) {
357-
WalletLogPrintf("inactive seed with id %s added %d internal keys\n", HexStr(seed_id), missing);
358-
} else {
359-
WalletLogPrintf("inactive seed with id %s added %d keys\n", HexStr(seed_id), missing);
360-
}
361-
}
362347
return true;
363348
}
364349

@@ -390,11 +375,26 @@ std::vector<WalletDestination> LegacyScriptPubKeyMan::MarkUnusedAddresses(const
390375
if (it != mapKeyMetadata.end()){
391376
CKeyMetadata meta = it->second;
392377
if (!meta.hd_seed_id.IsNull() && meta.hd_seed_id != m_hd_chain.seed_id) {
393-
bool internal = (meta.key_origin.path[1] & ~BIP32_HARDENED_KEY_LIMIT) != 0;
394-
int64_t index = meta.key_origin.path[2] & ~BIP32_HARDENED_KEY_LIMIT;
395-
396-
if (!TopUpInactiveHDChain(meta.hd_seed_id, index, internal)) {
397-
WalletLogPrintf("%s: Adding inactive seed keys failed\n", __func__);
378+
std::vector<uint32_t> path;
379+
if (meta.has_key_origin) {
380+
path = meta.key_origin.path;
381+
} else if (!ParseHDKeypath(meta.hdKeypath, path)) {
382+
WalletLogPrintf("%s: Adding inactive seed keys failed, invalid hdKeypath: %s\n",
383+
__func__,
384+
meta.hdKeypath);
385+
}
386+
if (path.size() != 3) {
387+
WalletLogPrintf("%s: Adding inactive seed keys failed, invalid path size: %d, has_key_origin: %s\n",
388+
__func__,
389+
path.size(),
390+
meta.has_key_origin);
391+
} else {
392+
bool internal = (path[1] & ~BIP32_HARDENED_KEY_LIMIT) != 0;
393+
int64_t index = path[2] & ~BIP32_HARDENED_KEY_LIMIT;
394+
395+
if (!TopUpInactiveHDChain(meta.hd_seed_id, index, internal)) {
396+
WalletLogPrintf("%s: Adding inactive seed keys failed\n", __func__);
397+
}
398398
}
399399
}
400400
}
@@ -1271,44 +1271,69 @@ bool LegacyScriptPubKeyMan::TopUp(unsigned int kpSize)
12711271
if (!CanGenerateKeys()) {
12721272
return false;
12731273
}
1274-
{
1275-
LOCK(cs_KeyStore);
12761274

1277-
if (m_storage.IsLocked()) return false;
1275+
if (!TopUpChain(m_hd_chain, kpSize)) {
1276+
return false;
1277+
}
1278+
for (auto& [chain_id, chain] : m_inactive_hd_chains) {
1279+
if (!TopUpChain(chain, kpSize)) {
1280+
return false;
1281+
}
1282+
}
1283+
NotifyCanGetAddressesChanged();
1284+
return true;
1285+
}
12781286

1279-
// Top up key pool
1280-
unsigned int nTargetSize;
1281-
if (kpSize > 0)
1282-
nTargetSize = kpSize;
1283-
else
1284-
nTargetSize = std::max(gArgs.GetIntArg("-keypool", DEFAULT_KEYPOOL_SIZE), (int64_t) 0);
1287+
bool LegacyScriptPubKeyMan::TopUpChain(CHDChain& chain, unsigned int kpSize)
1288+
{
1289+
LOCK(cs_KeyStore);
12851290

1286-
// count amount of available keys (internal, external)
1287-
// make sure the keypool of external and internal keys fits the user selected target (-keypool)
1288-
int64_t missingExternal = std::max(std::max((int64_t) nTargetSize, (int64_t) 1) - (int64_t)setExternalKeyPool.size(), (int64_t) 0);
1289-
int64_t missingInternal = std::max(std::max((int64_t) nTargetSize, (int64_t) 1) - (int64_t)setInternalKeyPool.size(), (int64_t) 0);
1291+
if (m_storage.IsLocked()) return false;
12901292

1291-
if (!IsHDEnabled() || !m_storage.CanSupportFeature(FEATURE_HD_SPLIT))
1292-
{
1293-
// don't create extra internal keys
1294-
missingInternal = 0;
1293+
// Top up key pool
1294+
unsigned int nTargetSize;
1295+
if (kpSize > 0) {
1296+
nTargetSize = kpSize;
1297+
} else {
1298+
nTargetSize = std::max(gArgs.GetIntArg("-keypool", DEFAULT_KEYPOOL_SIZE), int64_t{0});
1299+
}
1300+
int64_t target = std::max((int64_t) nTargetSize, int64_t{1});
1301+
1302+
// count amount of available keys (internal, external)
1303+
// make sure the keypool of external and internal keys fits the user selected target (-keypool)
1304+
int64_t missingExternal;
1305+
int64_t missingInternal;
1306+
if (chain == m_hd_chain) {
1307+
missingExternal = std::max(target - (int64_t)setExternalKeyPool.size(), int64_t{0});
1308+
missingInternal = std::max(target - (int64_t)setInternalKeyPool.size(), int64_t{0});
1309+
} else {
1310+
missingExternal = std::max(target - (chain.nExternalChainCounter - chain.m_next_external_index), int64_t{0});
1311+
missingInternal = std::max(target - (chain.nInternalChainCounter - chain.m_next_internal_index), int64_t{0});
1312+
}
1313+
1314+
if (!IsHDEnabled() || !m_storage.CanSupportFeature(FEATURE_HD_SPLIT)) {
1315+
// don't create extra internal keys
1316+
missingInternal = 0;
1317+
}
1318+
bool internal = false;
1319+
WalletBatch batch(m_storage.GetDatabase());
1320+
for (int64_t i = missingInternal + missingExternal; i--;) {
1321+
if (i < missingInternal) {
1322+
internal = true;
12951323
}
1296-
bool internal = false;
1297-
WalletBatch batch(m_storage.GetDatabase());
1298-
for (int64_t i = missingInternal + missingExternal; i--;)
1299-
{
1300-
if (i < missingInternal) {
1301-
internal = true;
1302-
}
13031324

1304-
CPubKey pubkey(GenerateNewKey(batch, m_hd_chain, internal));
1325+
CPubKey pubkey(GenerateNewKey(batch, chain, internal));
1326+
if (chain == m_hd_chain) {
13051327
AddKeypoolPubkeyWithDB(pubkey, internal, batch);
13061328
}
1307-
if (missingInternal + missingExternal > 0) {
1329+
}
1330+
if (missingInternal + missingExternal > 0) {
1331+
if (chain == m_hd_chain) {
13081332
WalletLogPrintf("keypool added %d keys (%d internal), size=%u (%u internal)\n", missingInternal + missingExternal, missingInternal, setInternalKeyPool.size() + setExternalKeyPool.size() + set_pre_split_keypool.size(), setInternalKeyPool.size());
1333+
} else {
1334+
WalletLogPrintf("inactive seed with id %s added %d external keys, %d internal keys\n", HexStr(chain.seed_id), missingExternal, missingInternal);
13091335
}
13101336
}
1311-
NotifyCanGetAddressesChanged();
13121337
return true;
13131338
}
13141339

src/wallet/scriptpubkeyman.h

+1
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,7 @@ class LegacyScriptPubKeyMan : public ScriptPubKeyMan, public FillableSigningProv
355355
*/
356356
bool TopUpInactiveHDChain(const CKeyID seed_id, int64_t index, bool internal);
357357

358+
bool TopUpChain(CHDChain& chain, unsigned int size);
358359
public:
359360
using ScriptPubKeyMan::ScriptPubKeyMan;
360361

src/wallet/walletdb.cpp

+2-2
Original file line numberDiff line numberDiff line change
@@ -590,9 +590,9 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
590590
}
591591
if (internal) {
592592
chain.nVersion = CHDChain::VERSION_HD_CHAIN_SPLIT;
593-
chain.nInternalChainCounter = std::max(chain.nInternalChainCounter, index);
593+
chain.nInternalChainCounter = std::max(chain.nInternalChainCounter, index + 1);
594594
} else {
595-
chain.nExternalChainCounter = std::max(chain.nExternalChainCounter, index);
595+
chain.nExternalChainCounter = std::max(chain.nExternalChainCounter, index + 1);
596596
}
597597
}
598598
} else if (strType == DBKeys::WATCHMETA) {

src/wallet/walletdb.h

+2
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ class CHDChain
9393
uint32_t nExternalChainCounter;
9494
uint32_t nInternalChainCounter;
9595
CKeyID seed_id; //!< seed hash160
96+
int64_t m_next_external_index{0}; // Next index in the keypool to be used. Memory only.
97+
int64_t m_next_internal_index{0}; // Next index in the keypool to be used. Memory only.
9698

9799
static const int VERSION_HD_BASE = 1;
98100
static const int VERSION_HD_CHAIN_SPLIT = 2;

test/functional/test_framework/test_node.py

+3
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,9 @@ def __init__(self, rpc, cli=False, descriptors=False):
758758
def __getattr__(self, name):
759759
return getattr(self.rpc, name)
760760

761+
def createwallet_passthrough(self, *args, **kwargs):
762+
return self.__getattr__("createwallet")(*args, **kwargs)
763+
761764
def createwallet(self, wallet_name, disable_private_keys=None, blank=None, passphrase='', avoid_reuse=None, descriptors=None, load_on_startup=None, external_signer=None):
762765
if descriptors is None:
763766
descriptors = self.descriptors

test/functional/test_runner.py

+1
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@
318318
'wallet_send.py --descriptors',
319319
'wallet_create_tx.py --descriptors',
320320
'wallet_taproot.py',
321+
'wallet_inactive_hdchains.py',
321322
'p2p_fingerprint.py',
322323
'feature_uacomment.py',
323324
'feature_init.py',
+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2021 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
"""
6+
Test Inactive HD Chains.
7+
"""
8+
import os
9+
import shutil
10+
import time
11+
12+
from test_framework.authproxy import JSONRPCException
13+
from test_framework.test_framework import BitcoinTestFramework
14+
from test_framework.wallet_util import (
15+
get_generate_key,
16+
)
17+
18+
19+
class InactiveHDChainsTest(BitcoinTestFramework):
20+
def set_test_params(self):
21+
self.setup_clean_chain = True
22+
self.num_nodes = 2
23+
self.extra_args = [["-keypool=10"], ["-nowallet", "-keypool=10"]]
24+
25+
def skip_test_if_missing_module(self):
26+
self.skip_if_no_wallet()
27+
self.skip_if_no_bdb()
28+
self.skip_if_no_previous_releases()
29+
30+
def setup_nodes(self):
31+
self.add_nodes(self.num_nodes, extra_args=self.extra_args, versions=[
32+
None,
33+
170200, # 0.17.2 Does not have the key metadata upgrade
34+
])
35+
36+
self.start_nodes()
37+
self.init_wallet(node=0)
38+
39+
def prepare_wallets(self, wallet_basename, encrypt=False):
40+
self.nodes[0].createwallet(wallet_name=f"{wallet_basename}_base", descriptors=False, blank=True)
41+
self.nodes[0].createwallet(wallet_name=f"{wallet_basename}_test", descriptors=False, blank=True)
42+
base_wallet = self.nodes[0].get_wallet_rpc(f"{wallet_basename}_base")
43+
test_wallet = self.nodes[0].get_wallet_rpc(f"{wallet_basename}_test")
44+
45+
# Setup both wallets with the same HD seed
46+
seed = get_generate_key()
47+
base_wallet.sethdseed(True, seed.privkey)
48+
test_wallet.sethdseed(True, seed.privkey)
49+
50+
if encrypt:
51+
# Encrypting will generate a new HD seed and flush the keypool
52+
test_wallet.encryptwallet("pass")
53+
else:
54+
# Generate a new HD seed on the test wallet
55+
test_wallet.sethdseed()
56+
57+
return base_wallet, test_wallet
58+
59+
def do_inactive_test(self, base_wallet, test_wallet, encrypt=False):
60+
default = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
61+
62+
# The first address should be known by both wallets.
63+
addr1 = base_wallet.getnewaddress()
64+
assert test_wallet.getaddressinfo(addr1)["ismine"]
65+
# The address at index 9 is the first address that the test wallet will not know initially
66+
for _ in range(0, 9):
67+
base_wallet.getnewaddress()
68+
addr2 = base_wallet.getnewaddress()
69+
assert not test_wallet.getaddressinfo(addr2)["ismine"]
70+
71+
# Send to first address on the old seed
72+
txid = default.sendtoaddress(addr1, 10)
73+
self.generate(self.nodes[0], 1)
74+
75+
# Wait for the test wallet to see the transaction
76+
while True:
77+
try:
78+
test_wallet.gettransaction(txid)
79+
break
80+
except JSONRPCException:
81+
time.sleep(0.1)
82+
83+
if encrypt:
84+
# The test wallet will not be able to generate the topped up keypool
85+
# until it is unlocked. So it still should not know about the second address
86+
assert not test_wallet.getaddressinfo(addr2)["ismine"]
87+
test_wallet.walletpassphrase("pass", 1)
88+
89+
# The test wallet should now know about the second address as it
90+
# should have generated it in the inactive chain's keypool
91+
assert test_wallet.getaddressinfo(addr2)["ismine"]
92+
93+
# Send to second address on the old seed
94+
txid = default.sendtoaddress(addr2, 10)
95+
self.generate(self.nodes[0], 1)
96+
test_wallet.gettransaction(txid)
97+
98+
def test_basic(self):
99+
self.log.info("Test basic case for inactive HD chains")
100+
self.do_inactive_test(*self.prepare_wallets("basic"))
101+
102+
def test_encrypted_wallet(self):
103+
self.log.info("Test inactive HD chains when wallet is encrypted")
104+
self.do_inactive_test(*self.prepare_wallets("enc", encrypt=True), encrypt=True)
105+
106+
def test_without_upgraded_keymeta(self):
107+
# Test that it is possible to top up inactive hd chains even if there is no key origin
108+
# in CKeyMetadata. This tests for the segfault reported in
109+
# https://github.com/bitcoin/bitcoin/issues/21605
110+
self.log.info("Test that topping up inactive HD chains does not need upgraded key origin")
111+
112+
self.nodes[0].createwallet(wallet_name="keymeta_base", descriptors=False, blank=True)
113+
# Createwallet is overridden in the test framework so that the descriptor option can be filled
114+
# depending on the test's cli args. However we don't want to do that when using old nodes that
115+
# do not support descriptors. So we use the createwallet_passthrough function.
116+
self.nodes[1].createwallet_passthrough(wallet_name="keymeta_test")
117+
base_wallet = self.nodes[0].get_wallet_rpc("keymeta_base")
118+
test_wallet = self.nodes[1].get_wallet_rpc("keymeta_test")
119+
120+
# Setup both wallets with the same HD seed
121+
seed = get_generate_key()
122+
base_wallet.sethdseed(True, seed.privkey)
123+
test_wallet.sethdseed(True, seed.privkey)
124+
125+
# Encrypting will generate a new HD seed and flush the keypool
126+
test_wallet.encryptwallet("pass")
127+
128+
# Copy test wallet to node 0
129+
test_wallet.unloadwallet()
130+
test_wallet_dir = os.path.join(self.nodes[1].datadir, "regtest/wallets/keymeta_test")
131+
new_test_wallet_dir = os.path.join(self.nodes[0].datadir, "regtest/wallets/keymeta_test")
132+
shutil.copytree(test_wallet_dir, new_test_wallet_dir)
133+
self.nodes[0].loadwallet("keymeta_test")
134+
test_wallet = self.nodes[0].get_wallet_rpc("keymeta_test")
135+
136+
self.do_inactive_test(base_wallet, test_wallet, encrypt=True)
137+
138+
def run_test(self):
139+
self.generate(self.nodes[0], 101)
140+
141+
self.test_basic()
142+
self.test_encrypted_wallet()
143+
self.test_without_upgraded_keymeta()
144+
145+
146+
if __name__ == '__main__':
147+
InactiveHDChainsTest().main()

0 commit comments

Comments
 (0)