From 2d7dd90c552c7598e3b1d3e4b91c8dc914d56276 Mon Sep 17 00:00:00 2001 From: DanS Date: Sun, 23 Aug 2026 06:37:43 +0200 Subject: [PATCH] wallet: plumb the mnemonic entropy through CWallet Wires the key store secret to the database records: load and store paths on CWallet, the two ReadKeyValue arms, and the export path. GetMnemonicPhrase now prefers the entropy record and verifies it before printing: a phrase is only returned if expanding it reproduces the seed derivation actually uses. It falls back to the existing fMnemonicSeed path, so wallets that store the entropy AS the seed keep working unchanged. IsMnemonicSeed() now means "a phrase is available" rather than "the seed is the entropy", which is what every caller actually wants. Still a no-op on every existing wallet: nothing creates an entropy record yet, so GetMnemonicEntropy returns false and the old code path is taken. Co-Authored-By: Claude Opus 5 (1M context) --- src/wallet/wallet.cpp | 96 +++++++++++++++++++++++++++++++++++++++++ src/wallet/wallet.h | 23 +++++++++- src/wallet/walletdb.cpp | 32 ++++++++++++++ 3 files changed, 149 insertions(+), 2 deletions(-) diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 5c63a319a..b73e5c3a9 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -2587,6 +2587,72 @@ bool CWallet::LoadCryptedHDSeed(const uint256& seedFp, const std::vector& vchCryptedSecret) +{ + if (!CCryptoKeyStore::SetCryptedMnemonicEntropy(entropyFp, vchCryptedSecret)) { + return false; + } + + if (!fFileBacked) { + return true; + } + + { + LOCK(cs_wallet); + // Same write-then-erase discipline as SetCryptedHDSeed: CDB::Rewrite at + // the end of EncryptWallet copies every surviving record, so a leftover + // plaintext "mnementropy" would keep the seed phrase recoverable from an + // encrypted wallet.dat. The erase is best-effort for the same reason: a + // hard failure would propagate into EncryptKeys -> assert(false). + if (pwalletdbEncryption) { + if (!pwalletdbEncryption->WriteCryptedMnemonicEntropy(entropyFp, vchCryptedSecret)) + return false; + if (!pwalletdbEncryption->EraseMnemonicEntropy(entropyFp)) + LogPrintf("%s: WARNING: could not erase the plaintext mnementropy record\n", __func__); + return true; + } else { + CWalletDB walletdb(strWalletFile); + if (!walletdb.WriteCryptedMnemonicEntropy(entropyFp, vchCryptedSecret)) + return false; + if (!walletdb.EraseMnemonicEntropy(entropyFp)) + LogPrintf("%s: WARNING: could not erase the plaintext mnementropy record\n", __func__); + return true; + } + } + return false; +} + +bool CWallet::LoadMnemonicEntropy(const RawHDSeed& entropy) +{ + return CBasicKeyStore::SetMnemonicEntropy(entropy); +} + +bool CWallet::LoadCryptedMnemonicEntropy(const uint256& entropyFp, const std::vector& vchCryptedSecret) +{ + return CCryptoKeyStore::SetCryptedMnemonicEntropy(entropyFp, vchCryptedSecret); +} bool CWallet::InstallHDSeed(const HDSeed& seed, bool fMnemonic, int64_t nCreateTime) { @@ -2675,6 +2741,36 @@ bool CWallet::GetHDSeedForDerivation(HDSeed& seedOut) const bool CWallet::GetMnemonicPhrase(std::string& phraseOut) const { + // Preferred form: the HD seed is the EXPANDED 64-byte BIP39 seed and the + // entropy sits in its own record. + RawHDSeed entropy; + if (GetMnemonicEntropy(entropy)) { // false on an encrypted+locked wallet + // NEVER hand out a phrase that does not restore THIS wallet. Prove the + // entropy expands to the exact bytes derivation consumes; if it does + // not (a torn install, a wallet.dat edited by hand, an entropy record + // paired with a different seed), refuse rather than print a phrase that + // silently restores someone else's key tree. Costs one PBKDF2 on a + // user-initiated RPC. + RawHDSeed seed64; + if (!Bip39SeedFromEntropy(entropy, seed64)) + return false; + + HDSeed derivationSeed; + if (!GetHDSeedForDerivation(derivationSeed)) + return false; + + if (derivationSeed.RawSeed() != seed64) { + LogPrintf("%s: refusing to export a seed phrase: the stored mnemonic entropy does not " + "expand to this wallet's HD seed\n", __func__); + return false; + } + + return EntropyToMnemonic(entropy, phraseOut); + } + + // Legacy form (earlier builds of this branch): the stored HD seed IS the + // 32-byte BIP39 entropy, expanded on every derivation. Consistent by + // construction, so no cross-check is possible or needed. if (!hdChain.fMnemonicSeed) return false; diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 697823878..1b77ca291 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -1345,6 +1345,14 @@ public: bool SetHDSeed(const HDSeed& seed); bool SetCryptedHDSeed(const uint256& seedFp, const std::vector &vchCryptedSecret); + /* Record this wallet's BIP39 entropy so its seed phrase can be reprinted. + Display-only: derivation never reads it (the HD seed holds the bytes that + are actually derived from). Refuses to replace an existing record. + SetCryptedMnemonicEntropy overrides the CCryptoKeyStore virtual so the + record reaches disk; SetMnemonicEntropy merely hides the base version, + which is safe because no call site holds a base pointer. */ + bool SetMnemonicEntropy(const RawHDSeed& entropy); + bool SetCryptedMnemonicEntropy(const uint256& entropyFp, const std::vector &vchCryptedSecret); /* Restore a wallet's HD seed from a hex string (as exported in the z_exportwallet "# HDSeed=" comment): 32 bytes for a legacy raw seed, or @@ -1363,8 +1371,14 @@ public: wallet and the seed is available (unlocked). Returns false otherwise. */ bool GetMnemonicPhrase(std::string& phraseOut) const; - /* True if the HD seed was derived from a BIP39 mnemonic (stored as entropy). */ - bool IsMnemonicSeed() const { return hdChain.fMnemonicSeed; } + /* True if this wallet has a BIP39 seed phrase available. Two storage forms + qualify: + - current: the HD seed is the EXPANDED 64-byte BIP39 seed and the + entropy lives in its own record (HaveMnemonicEntropy()); + - legacy: hdChain.fMnemonicSeed, where the stored HD seed IS the + 32-byte entropy and is expanded on every derivation. + Gates z_exportmnemonic (rpcdump.cpp). */ + bool IsMnemonicSeed() const { return hdChain.fMnemonicSeed || HaveMnemonicEntropy(); } /* Return the seed to feed into HD derivation. For mnemonic wallets this expands the stored 32-byte entropy into the 64-byte BIP39 seed; for legacy @@ -1396,6 +1410,11 @@ public: /* Set the current encrypted HD seed, without saving it to disk (used by LoadWallet) */ bool LoadCryptedHDSeed(const uint256& seedFp, const std::vector& seed); + /* Set the mnemonic entropy, without saving it to disk (used by LoadWallet) */ + bool LoadMnemonicEntropy(const RawHDSeed& entropy); + + /* Set the encrypted mnemonic entropy, without saving it to disk (used by LoadWallet) */ + bool LoadCryptedMnemonicEntropy(const uint256& entropyFp, const std::vector& vchCryptedSecret); /* Find notes filtered by payment address, min depth, ability to spend */ void GetFilteredNotes(std::vector& saplingEntries, diff --git a/src/wallet/walletdb.cpp b/src/wallet/walletdb.cpp index eb144bd25..abac194ba 100644 --- a/src/wallet/walletdb.cpp +++ b/src/wallet/walletdb.cpp @@ -860,6 +860,38 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue, ssValue >> nOrigin; pwallet->hdSeedOrigin = (int)nOrigin; } + else if (strType == "mnementropy") + { + uint256 entropyFp; + RawHDSeed entropy; + ssKey >> entropyFp; + ssValue >> entropy; + + if (MnemonicEntropyFingerprint(entropy) != entropyFp) + { + strErr = "Error reading wallet database: mnemonic entropy corrupt"; + return false; + } + + if (!pwallet->LoadMnemonicEntropy(entropy)) + { + strErr = "Error reading wallet database: LoadMnemonicEntropy failed"; + return false; + } + } + else if (strType == "cmnementropy") + { + uint256 entropyFp; + vector vchCryptedSecret; + ssKey >> entropyFp; + ssValue >> vchCryptedSecret; + if (!pwallet->LoadCryptedMnemonicEntropy(entropyFp, vchCryptedSecret)) + { + strErr = "Error reading wallet database: LoadCryptedMnemonicEntropy failed"; + return false; + } + wss.fIsEncrypted = true; + } } catch (...) { return false;