From ce59d8c72c667ce638bd12f37db26d8b3408a5d2 Mon Sep 17 00:00:00 2001 From: Bitflash Date: Sat, 1 Aug 2026 17:53:17 -0300 Subject: wallet: derive the key pool from a seed, so a phrase could bring it back (#93) Adds BIP32 derivation behind the wallet key pool, with no user-facing way to create a seed yet. wallet.dat gains hdmaster/hdchaincode/hdnext; the mnemonic is never stored. TopUpKeyPool derives m/n' when a seed exists and falls back to a random key, with a log line, if derivation fails. New -selftest=wallet-hd, wired into make tests on both platforms. --- src/selftest.cpp | 149 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 148 insertions(+), 1 deletion(-) (limited to 'src/selftest.cpp') diff --git a/src/selftest.cpp b/src/selftest.cpp index abe1d15..6792ae7 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -314,14 +314,161 @@ static void AttachTerminal() #endif } +static int RunWalletHDSelfTest() +{ + fflush(stdout); + printf("wallet-hd self-test\n"); + + std::string tmp; + std::string cwd; + if (!MakeTempDir(tmp)) + { + printf(" FAIL could not create a temporary data directory\n"); + return 1; + } + if (!GetCurrentDir(cwd) || !SetCurrentDir(tmp)) + { + printf(" FAIL could not move into the temporary data directory\n"); + RemoveTree(tmp); + return 1; + } + + int nFail = 0; + strSetDataDir = tmp; + printf(" temp datadir: %s\n", tmp.c_str()); + + // Test-vector phrases, so nothing here depends on randomness. + const std::string strPhraseA = + "abandon abandon abandon abandon abandon abandon " + "abandon abandon abandon abandon abandon about"; + const std::string strPhraseB = + "legal winner thank year wave sausage worth useful " + "legal winner thank yellow"; + + try + { + if (!LoadWallet()) + throw std::runtime_error("LoadWallet failed"); + + std::string strError; + nFail += Check(SetHDSeedFromMnemonic(strPhraseA, strError), + "a valid phrase installs a seed") ? 0 : 1; + nFail += Check(HaveHDSeed(), "the wallet reports having a seed") ? 0 : 1; + + std::vector vchBefore = vchHDMaster; + nFail += Check(!SetHDSeedFromMnemonic("not a mnemonic at all", strError), + "an invalid phrase is refused") ? 0 : 1; + nFail += Check(vchHDMaster == vchBefore, + "a refused phrase leaves the existing seed alone") ? 0 : 1; + + // The property the whole feature exists for: the same words give back + // the same keys, in the same order. + std::vector first; + for (unsigned int i = 0; i < 5; i++) + { + CKey key; + if (!DeriveHDKey(i, key, strError)) + throw std::runtime_error("derivation failed: " + strError); + first.push_back(HexStrLocal(key.GetPubKey())); + } + + SetHDSeedFromMnemonic(strPhraseA, strError); + std::vector again; + for (unsigned int i = 0; i < 5; i++) + { + CKey key; + if (!DeriveHDKey(i, key, strError)) + throw std::runtime_error("derivation failed: " + strError); + again.push_back(HexStrLocal(key.GetPubKey())); + } + nFail += Check(first == again, + "the same phrase derives the same keys in the same order") ? 0 : 1; + + SetHDSeedFromMnemonic(strPhraseB, strError); + std::vector other; + for (unsigned int i = 0; i < 5; i++) + { + CKey key; + if (!DeriveHDKey(i, key, strError)) + throw std::runtime_error("derivation failed: " + strError); + other.push_back(HexStrLocal(key.GetPubKey())); + } + nFail += Check(first != other, "a different phrase derives different keys") ? 0 : 1; + + std::set distinct(first.begin(), first.end()); + nFail += Check(distinct.size() == first.size(), + "consecutive indices give distinct keys") ? 0 : 1; + + // With a seed installed the pool must be derived from it, and the + // counter must move exactly once per key. + SetHDSeedFromMnemonic(strPhraseA, strError); + CRITICAL_BLOCK(cs_keyPool) + mapKeyPool.clear(); + unsigned int nNextBefore = nHDNext; + TopUpKeyPool(); + + int nPool = 0; + CRITICAL_BLOCK(cs_keyPool) + nPool = (int)mapKeyPool.size(); + nFail += Check(nPool == KEYPOOL_SIZE, "the derived pool fills") ? 0 : 1; + nFail += Check(nHDNext == nNextBefore + (unsigned int)KEYPOOL_SIZE, + "the derivation counter advances once per pooled key") ? 0 : 1; + + std::set derived; + for (unsigned int i = nNextBefore; i < nHDNext; i++) + { + CKey key; + if (!DeriveHDKey(i, key, strError)) + throw std::runtime_error("derivation failed: " + strError); + derived.insert(HexStrLocal(key.GetPubKey())); + } + + bool fStored = true; + bool fFromSeed = true; + CRITICAL_BLOCK(cs_keyPool) + { + for (map >::const_iterator mi = mapKeyPool.begin(); + mi != mapKeyPool.end(); ++mi) + { + if (!WalletHasPrivateKey(mi->second)) + fStored = false; + if (!derived.count(HexStrLocal(mi->second))) + fFromSeed = false; + } + } + nFail += Check(fStored, "every derived pooled key is stored in wallet.dat") ? 0 : 1; + nFail += Check(fFromSeed, "every pooled key came from the seed, not from chance") ? 0 : 1; + } + catch (const std::exception& e) + { + printf(" FAIL exception: %s\n", e.what()); + nFail++; + } + catch (...) + { + printf(" FAIL unknown exception\n"); + nFail++; + } + + DBFlush(true); + SetCurrentDir(cwd); + RemoveTree(tmp); + printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", + nFail, nFail == 1 ? "" : "s"); + fflush(stdout); + return nFail == 0 ? 0 : 1; +} + int RunSelfTest(const std::string& name) { AttachTerminal(); if (name == "wallet-keypool") return RunWalletKeyPoolSelfTest(); + if (name == "wallet-hd") + return RunWalletHDSelfTest(); printf("Unknown self-test '%s'\n", name.c_str()); - printf("Known self-tests: wallet-keypool\n"); + printf("Known self-tests: wallet-keypool, wallet-hd\n"); return 1; } -- cgit v1.2.3