upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBitflash <btf01@proton.me>2026-08-01 21:48:03 -0300
committerGitHub <noreply@github.com>2026-08-01 21:48:03 -0300
commitd54a07170808b921c79e25c2a0a8f6b9c447d4ea (patch)
treebe83d3b40c1e466b1ecab1e3f5375b6a9b23c755
parent5a5478669c487963e99491112abc065c59973966 (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.cpp42
-rw-r--r--src/selftest.cpp12
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;