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/main.cpp | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 1 deletion(-) (limited to 'src/main.cpp') diff --git a/src/main.cpp b/src/main.cpp index 82a98d2..e5754e6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,6 +14,7 @@ #include #include #include +#include "bip32.h" #include "btfaddr.h" #include "btftunnel.h" #ifndef _WIN32 @@ -178,6 +179,66 @@ vector GenerateNewKey() CCriticalSection cs_keyPool; map > mapKeyPool; +vector vchHDMaster; +vector vchHDChainCode; +unsigned int nHDNext = 0; + +bool DeriveHDKey(unsigned int nIndex, CKey& keyRet, string& strErrorRet) +{ + if (!HaveHDSeed()) + { + strErrorRet = "no seed"; + return false; + } + + bitflash::BIP32PrivateNode parent; + parent.privateKey = vchHDMaster; + parent.chainCode = vchHDChainCode; + + bitflash::BIP32PrivateNode child; + if (!bitflash::BIP32DeriveHardenedChild(parent, nIndex, child, strErrorRet)) + return false; + + if (!keyRet.SetSecret(child.privateKey)) + { + strErrorRet = "derived scalar is not a usable key"; + return false; + } + return true; +} + +bool SetHDSeedFromMnemonic(const string& strMnemonic, string& strErrorRet) +{ + string strNormalized; + if (!bitflash::BIP39ValidateMnemonic(strMnemonic, strNormalized, strErrorRet)) + return false; + + vector vchSeed; + if (!bitflash::BIP39MnemonicToSeed(strNormalized, "", vchSeed, strErrorRet)) + return false; + + bitflash::BIP32PrivateNode master; + if (!bitflash::BIP32MasterFromSeed(vchSeed, master, strErrorRet)) + return false; + + CRITICAL_BLOCK(cs_keyPool) + { + // Written before the globals change, so a failed write leaves the + // wallet on the seed it already had rather than on one that exists + // only in memory. + if (!CWalletDB().WriteHDMaster(master.privateKey, master.chainCode) || + !CWalletDB().WriteHDNext(0)) + { + strErrorRet = "could not write the seed to wallet.dat"; + return false; + } + vchHDMaster = master.privateKey; + vchHDChainCode = master.chainCode; + nHDNext = 0; + } + return true; +} + void TopUpKeyPool() { CRITICAL_BLOCK(cs_keyPool) @@ -191,7 +252,30 @@ void TopUpKeyPool() nIndex = (--mapKeyPool.end())->first + 1; CKey key; - key.MakeNewKey(); + if (HaveHDSeed()) + { + // Derived, so a recovery phrase can reproduce this exact key. + // A derivation that fails is not a reason to stop filling the + // pool -- fall back to a random key, which is what a wallet + // without a seed uses anyway. It will not come back from the + // phrase, and that is better than a node that cannot mine. + string strError; + if (DeriveHDKey(nHDNext, key, strError)) + { + nHDNext++; + CWalletDB().WriteHDNext(nHDNext); + } + else + { + printf("TopUpKeyPool() : derivation at index %u failed (%s), " + "falling back to a random key\n", nHDNext, strError.c_str()); + key.MakeNewKey(); + } + } + else + { + key.MakeNewKey(); + } // AddKey writes the private key. Order matters: the key is in // wallet.dat before anything can point at it, so a crash between // the two writes costs an unused key, never a usable one. @@ -310,6 +394,32 @@ bool AddToWalletIfMine(const CTransaction& tx, const CBlock* pblock) return true; } +bool CanScanWalletTransactions(string& strErrorRet) +{ + strErrorRet.clear(); + + if (pindexGenesisBlock == NULL || pindexBest == NULL || hashBestChain == 0 || nBestHeight < 0) + { + strErrorRet = "The block index is not loaded, so the wallet scan cannot " + "prove whether imported or restored keys own coins."; + return false; + } + + if (nBestHeight == 0) + { + strErrorRet = "The local chain is only at height 0. A wallet scan would " + "inspect only the genesis block and could incorrectly " + "report zero transactions. Start the node and let it sync " + "before scanning. If this happened after copying a data " + "directory, keep wallet.dat safe and rebuild or re-download " + "the block data instead of trusting the copied database/ " + "environment."; + return false; + } + + return true; +} + int ScanForWalletTransactions(CBlockIndex* pindexStart) { // Walk the chain looking for transactions that belong to keys this wallet -- cgit v1.2.3