diff options
| author | Bitflash <btf01@proton.me> | 2026-08-01 21:48:03 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-01 21:48:03 -0300 |
| commit | d54a07170808b921c79e25c2a0a8f6b9c447d4ea (patch) | |
| tree | be83d3b40c1e466b1ecab1e3f5375b6a9b23c755 | |
| parent | 5a5478669c487963e99491112abc065c59973966 (diff) | |
wallet: reserve index 0 before deriving it, and name the address the phrase does not cover (#104)
Follow-up to #102. Reserves derivation index 0 before deriving it, so a failure between installing the seed and writing the default key cannot leave the pool free to hand that index out as an ordinary key. Renames the address that stops being the default to 'Your Address (created before the recovery phrase)', which was otherwise left under the same name as the new one. Also documents why the key-pool clear precedes the seed write, correcting a review comment of mine that asked for the reverse.
| -rw-r--r-- | src/main.cpp | 42 | ||||
| -rw-r--r-- | src/selftest.cpp | 12 |
2 files changed, 46 insertions, 8 deletions
diff --git a/src/main.cpp b/src/main.cpp index 2c4e346..9082bd5 100644 --- a/src/main.cpp +++ b/src/main.cpp | |||
| @@ -254,21 +254,46 @@ bool SetHDSeedFromMnemonic(const string& strMnemonic, string& strErrorRet) | |||
| 254 | 254 | ||
| 255 | CRITICAL_BLOCK(cs_keyPool) | 255 | CRITICAL_BLOCK(cs_keyPool) |
| 256 | { | 256 | { |
| 257 | // Old private keys stay in wallet.dat, but old unused pool records must | 257 | // The pool is cleared before the seed is written, and the order is the |
| 258 | // not survive the seed. Otherwise the next address can still be random | 258 | // safe one of the two. Old private keys stay in wallet.dat either way; |
| 259 | // while the UI says a recovery phrase exists. | 259 | // what is at stake is which half-finished state a failure can leave. |
| 260 | // | ||
| 261 | // clear, then write -- a failed write leaves the old seed and an | ||
| 262 | // empty pool. TopUpKeyPool refills it, and nothing was promised to | ||
| 263 | // anybody. | ||
| 264 | // | ||
| 265 | // write, then clear -- a failed clear leaves a seed installed and the | ||
| 266 | // old random pool intact. The user has just been shown twelve words | ||
| 267 | // and the wallet goes on handing out addresses those words cannot | ||
| 268 | // reproduce. That is the bug this function exists to prevent. | ||
| 269 | // | ||
| 270 | // An earlier comment here said the seed was written first so a failure | ||
| 271 | // left the wallet untouched. That was true of the seed alone and stops | ||
| 272 | // being true once the pool has to move with it. | ||
| 260 | if (!ClearKeyPoolRecords(strErrorRet)) | 273 | if (!ClearKeyPoolRecords(strErrorRet)) |
| 261 | return false; | 274 | return false; |
| 262 | 275 | ||
| 263 | if (!CWalletDB().WriteHDMaster(master.privateKey, master.chainCode) || | 276 | if (!CWalletDB().WriteHDMaster(master.privateKey, master.chainCode) || |
| 264 | !CWalletDB().WriteHDNext(0)) | 277 | !CWalletDB().WriteHDNext(1)) |
| 265 | { | 278 | { |
| 266 | strErrorRet = "could not write the seed to wallet.dat"; | 279 | strErrorRet = "could not write the seed to wallet.dat"; |
| 267 | return false; | 280 | return false; |
| 268 | } | 281 | } |
| 269 | vchHDMaster = master.privateKey; | 282 | vchHDMaster = master.privateKey; |
| 270 | vchHDChainCode = master.chainCode; | 283 | vchHDChainCode = master.chainCode; |
| 271 | nHDNext = 0; | 284 | |
| 285 | // Index 0 is spoken for before it is derived, so a failure below cannot | ||
| 286 | // leave the pool free to hand it out as an ordinary key. The cost of | ||
| 287 | // the counter being ahead of the derivation is one unused index; the | ||
| 288 | // cost of the reverse is the default address and a pool key sharing a | ||
| 289 | // derivation path. | ||
| 290 | nHDNext = 1; | ||
| 291 | |||
| 292 | // Remember which address stops being the default, so it can be named | ||
| 293 | // for what it is. Two entries reading "Your Address" -- one covered by | ||
| 294 | // the phrase and one not -- is the wrong thing to find at the moment a | ||
| 295 | // person has just written twelve words down. | ||
| 296 | vector<unsigned char> vchOldDefault = keyUser.GetPubKey(); | ||
| 272 | 297 | ||
| 273 | CKey key; | 298 | CKey key; |
| 274 | if (!DeriveHDKey(0, key, strErrorRet)) | 299 | if (!DeriveHDKey(0, key, strErrorRet)) |
| @@ -280,14 +305,15 @@ bool SetHDSeedFromMnemonic(const string& strMnemonic, string& strErrorRet) | |||
| 280 | } | 305 | } |
| 281 | vector<unsigned char> vchPubKey = key.GetPubKey(); | 306 | vector<unsigned char> vchPubKey = key.GetPubKey(); |
| 282 | if (!CWalletDB().WriteDefaultKey(vchPubKey) || | 307 | if (!CWalletDB().WriteDefaultKey(vchPubKey) || |
| 283 | !SetAddressBookName(PubKeyToAddress(vchPubKey), "Your Address") || | 308 | !SetAddressBookName(PubKeyToAddress(vchPubKey), "Your Address")) |
| 284 | !CWalletDB().WriteHDNext(1)) | ||
| 285 | { | 309 | { |
| 286 | strErrorRet = "could not set the default derived receiving key"; | 310 | strErrorRet = "could not set the default derived receiving key"; |
| 287 | return false; | 311 | return false; |
| 288 | } | 312 | } |
| 313 | if (!vchOldDefault.empty() && vchOldDefault != vchPubKey) | ||
| 314 | SetAddressBookName(PubKeyToAddress(vchOldDefault), | ||
| 315 | "Your Address (created before the recovery phrase)"); | ||
| 289 | keyUser = key; | 316 | keyUser = key; |
| 290 | nHDNext = 1; | ||
| 291 | } | 317 | } |
| 292 | return true; | 318 | return true; |
| 293 | } | 319 | } |
diff --git a/src/selftest.cpp b/src/selftest.cpp index bc9fa5a..88e9863 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp | |||
| @@ -334,6 +334,10 @@ static int RunWalletHDSelfTest() | |||
| 334 | nFail += Check(nRandomPoolSize == KEYPOOL_SIZE, | 334 | nFail += Check(nRandomPoolSize == KEYPOOL_SIZE, |
| 335 | "a wallet can start with a random key pool") ? 0 : 1; | 335 | "a wallet can start with a random key pool") ? 0 : 1; |
| 336 | 336 | ||
| 337 | // The address the wallet was showing before any of this. It has to | ||
| 338 | // survive under a name that says it is not covered by the phrase. | ||
| 339 | std::string strPreSeedAddr = PubKeyToAddress(keyUser.GetPubKey()); | ||
| 340 | |||
| 337 | nFail += Check(SetHDSeedFromMnemonic(strPhraseA, strError), | 341 | nFail += Check(SetHDSeedFromMnemonic(strPhraseA, strError), |
| 338 | "a valid phrase installs a seed") ? 0 : 1; | 342 | "a valid phrase installs a seed") ? 0 : 1; |
| 339 | nFail += Check(HaveHDSeed(), "the wallet reports having a seed") ? 0 : 1; | 343 | nFail += Check(HaveHDSeed(), "the wallet reports having a seed") ? 0 : 1; |
| @@ -356,6 +360,14 @@ static int RunWalletHDSelfTest() | |||
| 356 | nFail += Check(WalletHasPrivateKey(vchDefaultKey), | 360 | nFail += Check(WalletHasPrivateKey(vchDefaultKey), |
| 357 | "the derived default key is stored in wallet.dat") ? 0 : 1; | 361 | "the derived default key is stored in wallet.dat") ? 0 : 1; |
| 358 | 362 | ||
| 363 | std::string strNewAddr = PubKeyToAddress(vchDefaultKey); | ||
| 364 | nFail += Check(strPreSeedAddr != strNewAddr, | ||
| 365 | "the visible address stops being the pre-seed one") ? 0 : 1; | ||
| 366 | nFail += Check(mapAddressBook.count(strPreSeedAddr) > 0 && | ||
| 367 | mapAddressBook[strPreSeedAddr] != | ||
| 368 | mapAddressBook[strNewAddr], | ||
| 369 | "the pre-seed address is kept, named apart from the new one") ? 0 : 1; | ||
| 370 | |||
| 359 | std::vector<unsigned char> vchBefore = vchHDMaster; | 371 | std::vector<unsigned char> vchBefore = vchHDMaster; |
| 360 | nFail += Check(!SetHDSeedFromMnemonic("not a mnemonic at all", strError), | 372 | nFail += Check(!SetHDSeedFromMnemonic("not a mnemonic at all", strError), |
| 361 | "an invalid phrase is refused") ? 0 : 1; | 373 | "an invalid phrase is refused") ? 0 : 1; |