upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authormtg0001 <106211915+mtg0001@users.noreply.github.com>2026-08-01 21:43:45 -0300
committerGitHub <noreply@github.com>2026-08-01 21:43:45 -0300
commit5a5478669c487963e99491112abc065c59973966 (patch)
treee2e73a4e2283c5dae4fa342ec98cdf8d719017ca /src
parentfc071066c9de259795a210e449b364ac126416ad (diff)
Keep new phrase addresses recoverable (#102)
-newphrase installed a seed but left the wallet's visible address as the pre-seed random key and the old random pool intact, so the next address handed out -- a mining payout included -- could be one the twelve words cannot reproduce, while the window said a recovery phrase existed. Installing a phrase now clears the stale pool records (private keys stay in wallet.dat), reserves a derived default receiving key, tolerates already-stored derived keys so repeated installs do not stall the pool, and -restorephrase reports what it recovered rather than the wallet's total transaction count. Self-test covers installing a phrase over a pre-existing random pool.
Diffstat (limited to 'src')
-rw-r--r--src/main.cpp61
-rw-r--r--src/main.h5
-rw-r--r--src/selftest.cpp27
-rw-r--r--src/walletcmd.cpp17
4 files changed, 99 insertions, 11 deletions
diff --git a/src/main.cpp b/src/main.cpp
index e5754e6..2c4e346 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -167,6 +167,20 @@ bool AddKey(const CKey& key)
167 return CWalletDB().WriteKey(key.GetPubKey(), key.GetPrivKey()); 167 return CWalletDB().WriteKey(key.GetPubKey(), key.GetPrivKey());
168} 168}
169 169
170static bool WalletAlreadyHasKey(const vector<unsigned char>& vchPubKey)
171{
172 CRITICAL_BLOCK(cs_mapKeys)
173 return mapKeys.count(vchPubKey) > 0;
174 return false;
175}
176
177static bool AddKeyIfMissing(const CKey& key)
178{
179 if (WalletAlreadyHasKey(key.GetPubKey()))
180 return true;
181 return AddKey(key);
182}
183
170vector<unsigned char> GenerateNewKey() 184vector<unsigned char> GenerateNewKey()
171{ 185{
172 CKey key; 186 CKey key;
@@ -183,6 +197,23 @@ vector<unsigned char> vchHDMaster;
183vector<unsigned char> vchHDChainCode; 197vector<unsigned char> vchHDChainCode;
184unsigned int nHDNext = 0; 198unsigned int nHDNext = 0;
185 199
200static bool ClearKeyPoolRecords(string& strErrorRet)
201{
202 CWalletDB walletdb;
203 for (map<int64, vector<unsigned char> >::const_iterator mi = mapKeyPool.begin();
204 mi != mapKeyPool.end(); ++mi)
205 {
206 if (!walletdb.ErasePool(mi->first))
207 {
208 strErrorRet = strprintf("could not clear old key-pool entry %lld",
209 (long long)mi->first);
210 return false;
211 }
212 }
213 mapKeyPool.clear();
214 return true;
215}
216
186bool DeriveHDKey(unsigned int nIndex, CKey& keyRet, string& strErrorRet) 217bool DeriveHDKey(unsigned int nIndex, CKey& keyRet, string& strErrorRet)
187{ 218{
188 if (!HaveHDSeed()) 219 if (!HaveHDSeed())
@@ -223,9 +254,12 @@ bool SetHDSeedFromMnemonic(const string& strMnemonic, string& strErrorRet)
223 254
224 CRITICAL_BLOCK(cs_keyPool) 255 CRITICAL_BLOCK(cs_keyPool)
225 { 256 {
226 // Written before the globals change, so a failed write leaves the 257 // Old private keys stay in wallet.dat, but old unused pool records must
227 // wallet on the seed it already had rather than on one that exists 258 // not survive the seed. Otherwise the next address can still be random
228 // only in memory. 259 // while the UI says a recovery phrase exists.
260 if (!ClearKeyPoolRecords(strErrorRet))
261 return false;
262
229 if (!CWalletDB().WriteHDMaster(master.privateKey, master.chainCode) || 263 if (!CWalletDB().WriteHDMaster(master.privateKey, master.chainCode) ||
230 !CWalletDB().WriteHDNext(0)) 264 !CWalletDB().WriteHDNext(0))
231 { 265 {
@@ -235,6 +269,25 @@ bool SetHDSeedFromMnemonic(const string& strMnemonic, string& strErrorRet)
235 vchHDMaster = master.privateKey; 269 vchHDMaster = master.privateKey;
236 vchHDChainCode = master.chainCode; 270 vchHDChainCode = master.chainCode;
237 nHDNext = 0; 271 nHDNext = 0;
272
273 CKey key;
274 if (!DeriveHDKey(0, key, strErrorRet))
275 return false;
276 if (!AddKeyIfMissing(key))
277 {
278 strErrorRet = "could not write the default derived key to wallet.dat";
279 return false;
280 }
281 vector<unsigned char> vchPubKey = key.GetPubKey();
282 if (!CWalletDB().WriteDefaultKey(vchPubKey) ||
283 !SetAddressBookName(PubKeyToAddress(vchPubKey), "Your Address") ||
284 !CWalletDB().WriteHDNext(1))
285 {
286 strErrorRet = "could not set the default derived receiving key";
287 return false;
288 }
289 keyUser = key;
290 nHDNext = 1;
238 } 291 }
239 return true; 292 return true;
240} 293}
@@ -279,7 +332,7 @@ void TopUpKeyPool()
279 // AddKey writes the private key. Order matters: the key is in 332 // AddKey writes the private key. Order matters: the key is in
280 // wallet.dat before anything can point at it, so a crash between 333 // wallet.dat before anything can point at it, so a crash between
281 // the two writes costs an unused key, never a usable one. 334 // the two writes costs an unused key, never a usable one.
282 if (!AddKey(key)) 335 if (!AddKeyIfMissing(key))
283 return; 336 return;
284 vector<unsigned char> vchPubKey = key.GetPubKey(); 337 vector<unsigned char> vchPubKey = key.GetPubKey();
285 if (!CWalletDB().WritePool(nIndex, vchPubKey)) 338 if (!CWalletDB().WritePool(nIndex, vchPubKey))
diff --git a/src/main.h b/src/main.h
index a04a4e3..1b0be59 100644
--- a/src/main.h
+++ b/src/main.h
@@ -159,8 +159,9 @@ extern vector<unsigned char> vchHDChainCode; // 32 bytes (IR)
159extern unsigned int nHDNext; // next child index to derive 159extern unsigned int nHDNext; // next child index to derive
160inline bool HaveHDSeed() { return vchHDMaster.size() == 32 && vchHDChainCode.size() == 32; } 160inline bool HaveHDSeed() { return vchHDMaster.size() == 32 && vchHDChainCode.size() == 32; }
161 161
162// Install a seed derived from a mnemonic, replacing any existing one, and reset 162// Install a seed derived from a mnemonic, replacing any existing one. The
163// the derivation counter. Writes to wallet.dat. Returns false and leaves the 163// default receiving key is derived immediately, so the next visible address is
164// covered by the phrase. Writes to wallet.dat. Returns false and leaves the
164// wallet untouched if the phrase is not valid. 165// wallet untouched if the phrase is not valid.
165bool SetHDSeedFromMnemonic(const string& strMnemonic, string& strErrorRet); 166bool SetHDSeedFromMnemonic(const string& strMnemonic, string& strErrorRet);
166 167
diff --git a/src/selftest.cpp b/src/selftest.cpp
index b733c60..bc9fa5a 100644
--- a/src/selftest.cpp
+++ b/src/selftest.cpp
@@ -327,10 +327,35 @@ static int RunWalletHDSelfTest()
327 throw std::runtime_error("LoadWallet failed"); 327 throw std::runtime_error("LoadWallet failed");
328 328
329 std::string strError; 329 std::string strError;
330 TopUpKeyPool();
331 int nRandomPoolSize = 0;
332 CRITICAL_BLOCK(cs_keyPool)
333 nRandomPoolSize = (int)mapKeyPool.size();
334 nFail += Check(nRandomPoolSize == KEYPOOL_SIZE,
335 "a wallet can start with a random key pool") ? 0 : 1;
336
330 nFail += Check(SetHDSeedFromMnemonic(strPhraseA, strError), 337 nFail += Check(SetHDSeedFromMnemonic(strPhraseA, strError),
331 "a valid phrase installs a seed") ? 0 : 1; 338 "a valid phrase installs a seed") ? 0 : 1;
332 nFail += Check(HaveHDSeed(), "the wallet reports having a seed") ? 0 : 1; 339 nFail += Check(HaveHDSeed(), "the wallet reports having a seed") ? 0 : 1;
333 340
341 int nPoolAfterSeed = 0;
342 CRITICAL_BLOCK(cs_keyPool)
343 nPoolAfterSeed = (int)mapKeyPool.size();
344 nFail += Check(nPoolAfterSeed == 0,
345 "installing a phrase clears the old random key pool") ? 0 : 1;
346 nFail += Check(nHDNext == 1,
347 "installing a phrase reserves one derived default key") ? 0 : 1;
348
349 CKey keyFirstDerived;
350 if (!DeriveHDKey(0, keyFirstDerived, strError))
351 throw std::runtime_error("default derivation failed: " + strError);
352 std::vector<unsigned char> vchDefaultKey;
353 bool fDefaultRead = CWalletDB("r").ReadDefaultKey(vchDefaultKey);
354 nFail += Check(fDefaultRead && vchDefaultKey == keyFirstDerived.GetPubKey(),
355 "the default receiving key is derived from the phrase") ? 0 : 1;
356 nFail += Check(WalletHasPrivateKey(vchDefaultKey),
357 "the derived default key is stored in wallet.dat") ? 0 : 1;
358
334 std::vector<unsigned char> vchBefore = vchHDMaster; 359 std::vector<unsigned char> vchBefore = vchHDMaster;
335 nFail += Check(!SetHDSeedFromMnemonic("not a mnemonic at all", strError), 360 nFail += Check(!SetHDSeedFromMnemonic("not a mnemonic at all", strError),
336 "an invalid phrase is refused") ? 0 : 1; 361 "an invalid phrase is refused") ? 0 : 1;
@@ -378,8 +403,6 @@ static int RunWalletHDSelfTest()
378 // With a seed installed the pool must be derived from it, and the 403 // With a seed installed the pool must be derived from it, and the
379 // counter must move exactly once per key. 404 // counter must move exactly once per key.
380 SetHDSeedFromMnemonic(strPhraseA, strError); 405 SetHDSeedFromMnemonic(strPhraseA, strError);
381 CRITICAL_BLOCK(cs_keyPool)
382 mapKeyPool.clear();
383 unsigned int nNextBefore = nHDNext; 406 unsigned int nNextBefore = nHDNext;
384 TopUpKeyPool(); 407 TopUpKeyPool();
385 408
diff --git a/src/walletcmd.cpp b/src/walletcmd.cpp
index 94e9d72..8eb3592 100644
--- a/src/walletcmd.cpp
+++ b/src/walletcmd.cpp
@@ -94,13 +94,17 @@ bool RestoreFromPhrase(const std::string& strMnemonic,
94 if (!CanScanWalletTransactions(strErrorRet)) 94 if (!CanScanWalletTransactions(strErrorRet))
95 return false; 95 return false;
96 96
97 size_t nWalletStart = 0;
98 CRITICAL_BLOCK(cs_mapWallet)
99 nWalletStart = mapWallet.size();
100
97 if (!SetHDSeedFromMnemonic(strMnemonic, strErrorRet)) 101 if (!SetHDSeedFromMnemonic(strMnemonic, strErrorRet))
98 return false; 102 return false;
99 103
100 // Derive forward in batches, scanning after each, until a whole batch turns 104 // Derive forward in batches, scanning after each, until a whole batch turns
101 // up nothing. Every derived key is written to the wallet before the scan, 105 // up nothing. Every derived key is written to the wallet before the scan,
102 // because the scan asks the wallet what belongs to it. 106 // because the scan asks the wallet what belongs to it.
103 int nTotalDerived = 0; 107 int nTotalDerived = (int)nHDNext;
104 while (nTotalDerived < RESTORE_MAX) 108 while (nTotalDerived < RESTORE_MAX)
105 { 109 {
106 // Count wallet transactions, not scan hits. 110 // Count wallet transactions, not scan hits.
@@ -134,7 +138,11 @@ bool RestoreFromPhrase(const std::string& strMnemonic,
134 nWalletAfter = mapWallet.size(); 138 nWalletAfter = mapWallet.size();
135 139
136 if (fnProgress) 140 if (fnProgress)
137 fnProgress(pArg, nTotalDerived, (int)nWalletAfter); 141 {
142 size_t nRecoveredNow = nWalletAfter > nWalletStart ?
143 nWalletAfter - nWalletStart : 0;
144 fnProgress(pArg, nTotalDerived, (int)nRecoveredNow);
145 }
138 146
139 // A whole batch with nothing in it means far enough -- unless the 147 // A whole batch with nothing in it means far enough -- unless the
140 // caller asked to look deeper anyway. 148 // caller asked to look deeper anyway.
@@ -148,8 +156,11 @@ bool RestoreFromPhrase(const std::string& strMnemonic,
148 mapKeyPool.clear(); 156 mapKeyPool.clear();
149 TopUpKeyPool(); 157 TopUpKeyPool();
150 158
159 size_t nWalletEnd = 0;
151 CRITICAL_BLOCK(cs_mapWallet) 160 CRITICAL_BLOCK(cs_mapWallet)
152 nRecoveredRet = (int)mapWallet.size(); 161 nWalletEnd = mapWallet.size();
162 nRecoveredRet = (int)(nWalletEnd > nWalletStart ?
163 nWalletEnd - nWalletStart : 0);
153 nDerivedRet = nTotalDerived; 164 nDerivedRet = nTotalDerived;
154 return true; 165 return true;
155} 166}