diff options
| author | mtg0001 <106211915+mtg0001@users.noreply.github.com> | 2026-08-01 21:43:45 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-01 21:43:45 -0300 |
| commit | 5a5478669c487963e99491112abc065c59973966 (patch) | |
| tree | e2e73a4e2283c5dae4fa342ec98cdf8d719017ca /src | |
| parent | fc071066c9de259795a210e449b364ac126416ad (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.cpp | 61 | ||||
| -rw-r--r-- | src/main.h | 5 | ||||
| -rw-r--r-- | src/selftest.cpp | 27 | ||||
| -rw-r--r-- | src/walletcmd.cpp | 17 |
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 | ||
| 170 | static bool WalletAlreadyHasKey(const vector<unsigned char>& vchPubKey) | ||
| 171 | { | ||
| 172 | CRITICAL_BLOCK(cs_mapKeys) | ||
| 173 | return mapKeys.count(vchPubKey) > 0; | ||
| 174 | return false; | ||
| 175 | } | ||
| 176 | |||
| 177 | static bool AddKeyIfMissing(const CKey& key) | ||
| 178 | { | ||
| 179 | if (WalletAlreadyHasKey(key.GetPubKey())) | ||
| 180 | return true; | ||
| 181 | return AddKey(key); | ||
| 182 | } | ||
| 183 | |||
| 170 | vector<unsigned char> GenerateNewKey() | 184 | vector<unsigned char> GenerateNewKey() |
| 171 | { | 185 | { |
| 172 | CKey key; | 186 | CKey key; |
| @@ -183,6 +197,23 @@ vector<unsigned char> vchHDMaster; | |||
| 183 | vector<unsigned char> vchHDChainCode; | 197 | vector<unsigned char> vchHDChainCode; |
| 184 | unsigned int nHDNext = 0; | 198 | unsigned int nHDNext = 0; |
| 185 | 199 | ||
| 200 | static 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 | |||
| 186 | bool DeriveHDKey(unsigned int nIndex, CKey& keyRet, string& strErrorRet) | 217 | bool 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)) |
| @@ -159,8 +159,9 @@ extern vector<unsigned char> vchHDChainCode; // 32 bytes (IR) | |||
| 159 | extern unsigned int nHDNext; // next child index to derive | 159 | extern unsigned int nHDNext; // next child index to derive |
| 160 | inline bool HaveHDSeed() { return vchHDMaster.size() == 32 && vchHDChainCode.size() == 32; } | 160 | inline 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. |
| 165 | bool SetHDSeedFromMnemonic(const string& strMnemonic, string& strErrorRet); | 166 | bool 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 | } |