diff --git a/src/gtest/test_mnemonic_compat.cpp b/src/gtest/test_mnemonic_compat.cpp index cb623c921..ce7468e57 100644 --- a/src/gtest/test_mnemonic_compat.cpp +++ b/src/gtest/test_mnemonic_compat.cpp @@ -139,3 +139,103 @@ TEST(mnemonic_compat, RawEntropyDiffersFromMnemonicSeed) const std::string entropyT = DeriveTAddrFromSeedBytes(zeros); // wrong (32-byte) EXPECT_NE(seedT, entropyT); } +// New storage form: the HD seed IS the expanded 64-byte BIP39 seed, the chain is +// NOT flagged mnemonic, and the phrase comes from the separate entropy record. +TEST(mnemonic_compat, ExpandedSeedIsStoredDirectly) +{ + SelectParams(CBaseChainParams::MAIN); + + CWallet wallet; + ASSERT_TRUE(wallet.SetHDSeedFromMnemonic(ABANDON_ART)); + + // Stored bytes == the 64-byte BIP39 seed, fed to derivation unchanged. + HDSeed stored; + ASSERT_TRUE(wallet.GetHDSeed(stored)); + auto raw = stored.RawSeed(); + EXPECT_EQ(raw.size(), (size_t)64); + EXPECT_EQ(HexStr(raw.begin(), raw.end()), std::string(SEED64_HEX)); + + // No CHDChain version bump / no mnemonic flag: an older binary reads this + // wallet and derives the same tree. + EXPECT_FALSE(wallet.GetHDChain().fMnemonicSeed); + EXPECT_LT(wallet.GetHDChain().nVersion, CHDChain::VERSION_HD_MNEMONIC); + + HDSeed forDerivation; + ASSERT_TRUE(wallet.GetHDSeedForDerivation(forDerivation)); + EXPECT_EQ(forDerivation.RawSeed(), raw); + + // The phrase is still exportable, and IsMnemonicSeed() (which gates + // z_exportmnemonic) still says yes. + EXPECT_TRUE(wallet.IsMnemonicSeed()); + EXPECT_TRUE(wallet.HaveMnemonicEntropy()); + std::string exported; + ASSERT_TRUE(wallet.GetMnemonicPhrase(exported)); + EXPECT_EQ(exported, std::string(ABANDON_ART)); +} + +// Backwards compatibility: a wallet in the OLD form (stored HD seed == 32-byte +// entropy, fMnemonicSeed = true) must still derive and still export its phrase. +TEST(mnemonic_compat, LegacyEntropySeedStillWorks) +{ + SelectParams(CBaseChainParams::MAIN); + + RawHDSeed zeros(32, 0), seed64; + ASSERT_TRUE(Bip39SeedFromEntropy(zeros, seed64)); + + CWallet wallet; + { + LOCK(wallet.cs_wallet); + RawHDSeed entropy(32, 0); + HDSeed legacy(entropy); + ASSERT_TRUE(wallet.InstallHDSeed(legacy, true, 1)); + } + EXPECT_TRUE(wallet.GetHDChain().fMnemonicSeed); + EXPECT_FALSE(wallet.HaveMnemonicEntropy()); + EXPECT_TRUE(wallet.IsMnemonicSeed()); + + // Still expanded on read -> same key tree as the new form. + HDSeed forDerivation; + ASSERT_TRUE(wallet.GetHDSeedForDerivation(forDerivation)); + EXPECT_EQ(forDerivation.RawSeed(), seed64); + { + LOCK(wallet.cs_wallet); + EXPECT_EQ(EncodePaymentAddress(wallet.GenerateNewSaplingZKey()), + DeriveZAddrFromSeed64(seed64)); + } + + std::string exported; + ASSERT_TRUE(wallet.GetMnemonicPhrase(exported)); + EXPECT_EQ(exported, std::string(ABANDON_ART)); +} + +// The entropy record refuses replacement, and a phrase that does not restore the +// installed seed is never printed. +TEST(mnemonic_compat, MnemonicEntropyGuards) +{ + SelectParams(CBaseChainParams::MAIN); + + RawHDSeed zeros(32, 0), ones(32, 1); + + // Refuse-to-replace. + CWallet wallet; + ASSERT_TRUE(wallet.SetHDSeedFromMnemonic(ABANDON_ART)); + EXPECT_FALSE(wallet.SetMnemonicEntropy(ones)); + EXPECT_FALSE(wallet.SetMnemonicEntropy(RawHDSeed())); // empty is not "installed" + + // Mismatched entropy -> no phrase. Install a 64-byte seed that is NOT the + // expansion of `zeros`, then attach `zeros` as entropy. + RawHDSeed otherSeed64; + ASSERT_TRUE(Bip39SeedFromEntropy(ones, otherSeed64)); + CWallet mismatched; + ASSERT_TRUE(mismatched.SetHDSeedFromHex(HexStr(otherSeed64.begin(), otherSeed64.end()))); + ASSERT_TRUE(mismatched.SetMnemonicEntropy(zeros)); + std::string phrase; + EXPECT_FALSE(mismatched.GetMnemonicPhrase(phrase)); + + // Matching entropy attached to a hex-restored wallet -> phrase available. + CWallet matched; + ASSERT_TRUE(matched.SetHDSeedFromHex(std::string(SEED64_HEX))); + ASSERT_TRUE(matched.SetMnemonicEntropy(zeros)); + ASSERT_TRUE(matched.GetMnemonicPhrase(phrase)); + EXPECT_EQ(phrase, std::string(ABANDON_ART)); +} diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index b73e5c3a9..1b902f156 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -2480,9 +2480,30 @@ void CWallet::GenerateNewSeed() RawHDSeed entropy; if (!GenerateMnemonicEntropy(256, entropy)) throw std::runtime_error(std::string(__func__) + ": -usemnemonic entropy generation failed"); - HDSeed seed(entropy); - if (!InstallHDSeed(seed, true, nCreationTime)) + + // Store the EXPANDED 64-byte BIP39 seed as the HD seed, with + // fMnemonic = false. Every binary -- old or new -- then feeds the stored + // bytes straight into derivation, so the key tree is identical + // everywhere and no CHDChain version bump or minversion fence is needed. + // The 32-byte entropy is kept in a separate, display-only record purely + // so the phrase can be reprinted. Addresses are unchanged from the + // previous format, which expanded the stored entropy on every read. + RawHDSeed seed64; + if (!Bip39SeedFromEntropy(entropy, seed64)) + throw std::runtime_error(std::string(__func__) + ": BIP39 seed expansion failed"); + HDSeed seed(seed64); + + // ORDER IS LOAD-BEARING: seed first, entropy second, never the reverse. + // A crash between the two leaves a wallet with a seed and no phrase -- + // recoverable via z_exportwallet, merely inconvenient. The reverse order + // would leave entropy with no seed; the next start would mint a + // DIFFERENT seed while the wallet still held a phrase for the old one. + // (GetMnemonicPhrase cross-checks the two and would refuse to print it, + // but do not rely on that here.) + if (!InstallHDSeed(seed, false, nCreationTime)) throw std::runtime_error(std::string(__func__) + ": installing the mnemonic HD seed failed"); + if (!SetMnemonicEntropy(entropy)) + throw std::runtime_error(std::string(__func__) + ": storing the mnemonic entropy failed"); return; } @@ -2713,10 +2734,29 @@ bool CWallet::SetHDSeedFromMnemonic(const std::string& phrase) if (!MnemonicToEntropy(phrase, entropy)) return false; - // Store the BIP39 entropy as the HDSeed (SilentDragonXLite's on-disk - // convention); the 64-byte seed is expanded from it on demand. - HDSeed seed(entropy); - return InstallHDSeed(seed, true, 1); // birthday = genesis for a restore + // Store the EXPANDED 64-byte BIP39 seed as the HD seed (fMnemonic = false); + // the entropy goes in its own record and is used only to reprint the phrase. + // Derivation therefore reads the stored bytes directly on any binary, and + // the resulting addresses are byte-identical to the previous format, which + // expanded the stored entropy on every derivation. SilentDragonXLite + // interop is unaffected: the same words still yield the same seed64. + RawHDSeed seed64; + if (!Bip39SeedFromEntropy(entropy, seed64)) + return false; + HDSeed seed(seed64); + + // Seed first, entropy second -- see the ordering note in GenerateNewSeed. + if (!InstallHDSeed(seed, false, 1)) // birthday = genesis for a restore + return false; + + // Non-fatal on a restore, unlike GenerateNewSeed: the user already holds the + // phrase (they just typed it), the seed is installed and the wallet is fully + // functional; only z_exportmnemonic is lost. + if (!SetMnemonicEntropy(entropy)) { + LogPrintf("%s: WARNING: HD seed installed but the mnemonic entropy record could not be " + "stored; z_exportmnemonic will be unavailable on this wallet\n", __func__); + } + return true; } bool CWallet::GetHDSeedForDerivation(HDSeed& seedOut) const