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) <noreply@anthropic.com>
This commit is contained in:
2026-08-23 06:37:43 +02:00
parent 3caec548ae
commit 2d7dd90c55
3 changed files with 149 additions and 2 deletions

View File

@@ -2587,6 +2587,72 @@ bool CWallet::LoadCryptedHDSeed(const uint256& seedFp, const std::vector<unsigne
{ {
return CCryptoKeyStore::SetCryptedHDSeed(seedFp, seed); return CCryptoKeyStore::SetCryptedHDSeed(seedFp, seed);
} }
bool CWallet::SetMnemonicEntropy(const RawHDSeed& entropy)
{
if (!CCryptoKeyStore::SetMnemonicEntropy(entropy)) {
return false;
}
if (!fFileBacked) {
return true;
}
{
LOCK(cs_wallet);
if (!IsCrypted()) {
// Keyed by fingerprint exactly as "hdseed" is, so ReadKeyValue can
// integrity-check it and EraseMnemonicEntropy can find it later.
return CWalletDB(strWalletFile).WriteMnemonicEntropy(
MnemonicEntropyFingerprint(entropy), entropy);
}
}
return true;
}
bool CWallet::SetCryptedMnemonicEntropy(const uint256& entropyFp, const std::vector<unsigned char>& 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<unsigned char>& vchCryptedSecret)
{
return CCryptoKeyStore::SetCryptedMnemonicEntropy(entropyFp, vchCryptedSecret);
}
bool CWallet::InstallHDSeed(const HDSeed& seed, bool fMnemonic, int64_t nCreateTime) 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 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) if (!hdChain.fMnemonicSeed)
return false; return false;

View File

@@ -1345,6 +1345,14 @@ public:
bool SetHDSeed(const HDSeed& seed); bool SetHDSeed(const HDSeed& seed);
bool SetCryptedHDSeed(const uint256& seedFp, const std::vector<unsigned char> &vchCryptedSecret); bool SetCryptedHDSeed(const uint256& seedFp, const std::vector<unsigned char> &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<unsigned char> &vchCryptedSecret);
/* Restore a wallet's HD seed from a hex string (as exported in the /* 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 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. */ wallet and the seed is available (unlocked). Returns false otherwise. */
bool GetMnemonicPhrase(std::string& phraseOut) const; bool GetMnemonicPhrase(std::string& phraseOut) const;
/* True if the HD seed was derived from a BIP39 mnemonic (stored as entropy). */ /* True if this wallet has a BIP39 seed phrase available. Two storage forms
bool IsMnemonicSeed() const { return hdChain.fMnemonicSeed; } 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 /* 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 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) */ /* Set the current encrypted HD seed, without saving it to disk (used by LoadWallet) */
bool LoadCryptedHDSeed(const uint256& seedFp, const std::vector<unsigned char>& seed); bool LoadCryptedHDSeed(const uint256& seedFp, const std::vector<unsigned char>& 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<unsigned char>& vchCryptedSecret);
/* Find notes filtered by payment address, min depth, ability to spend */ /* Find notes filtered by payment address, min depth, ability to spend */
void GetFilteredNotes(std::vector<SaplingNoteEntry>& saplingEntries, void GetFilteredNotes(std::vector<SaplingNoteEntry>& saplingEntries,

View File

@@ -860,6 +860,38 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
ssValue >> nOrigin; ssValue >> nOrigin;
pwallet->hdSeedOrigin = (int)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<unsigned char> vchCryptedSecret;
ssKey >> entropyFp;
ssValue >> vchCryptedSecret;
if (!pwallet->LoadCryptedMnemonicEntropy(entropyFp, vchCryptedSecret))
{
strErr = "Error reading wallet database: LoadCryptedMnemonicEntropy failed";
return false;
}
wss.fIsEncrypted = true;
}
} catch (...) } catch (...)
{ {
return false; return false;