Add BIP39 seed phrases (SilentDragonXLite-compatible) and HD transparent keys

Derive transparent (t-addr) keys from the HD seed and add BIP39 mnemonic seed
phrases that are byte-for-byte compatible with SilentDragonXLite, so the same
24 words recover the same shielded and transparent addresses in either wallet.

HD transparent keys:
- Derive t-keys from the seed at m/44'/coin'/0'/0/i (were random CKeys).
- CHDChain gains a version-gated transparent counter; existing wallets load
  unchanged. GenerateNewKey routes through DeriveNewChildKey when enabled
  (-hdtransparent, default on).
- Restore from a seed hex via -hdseed with gap-limit pre-derivation; birthday
  pinned to genesis so the rescan is not clipped.

BIP39 seed phrases:
- Wire the vendored trezor BIP39 lib (src/crypto/bip39) into the build, fix its
  BIP39_WORDS guard, and disable the insecure mnemonic cache.
- Match SDXLite exactly: English wordlist, empty passphrase, PBKDF2 64-byte
  seed, coin type 141, ZIP-32 m/32'/141'/i' and BIP44 m/44'/141'/0'/0/i. Store
  the 32-byte entropy and expand to the 64-byte seed on demand.
- Restore via -mnemonic, create via -usemnemonic, reveal via z_exportmnemonic.

Verified by gtests including a known-answer BIP39 seed vector and z/t address
derivation checks (src/gtest/test_hdtransparent.cpp, test_mnemonic_compat.cpp).
Docs in doc/hd-transparent-keys.md and doc/seed-phrase.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-06 01:57:18 -05:00
co-authored by Claude Opus 4.8
parent 84aefb5475
commit 4caf2fc68f
23 changed files with 1044 additions and 22 deletions
+210 -10
View File
@@ -36,6 +36,7 @@
#include "utilmoneystr.h"
#include "zcash/Note.hpp"
#include "crypter.h"
#include "wallet/mnemonic.h"
#include "coins.h"
#include "wallet/asyncrpcoperation_saplingconsolidation.h"
#include "wallet/asyncrpcoperation_sweep.h"
@@ -131,7 +132,7 @@ SaplingPaymentAddress CWallet::GenerateNewSaplingZKey(bool addToWallet)
// Try to get the seed
HDSeed seed;
if (!GetHDSeed(seed))
if (!GetHDSeedForDerivation(seed))
throw std::runtime_error("CWallet::GenerateNewSaplingZKey(): HD seed not found");
auto m = libzcash::SaplingExtendedSpendingKey::Master(seed);
@@ -222,7 +223,20 @@ CPubKey CWallet::GenerateNewKey()
bool fCompressed = CanSupportFeature(FEATURE_COMPRPUBKEY); // default to compressed public keys if we want 0.6.0 wallets
CKey secret;
secret.MakeNewKey(fCompressed);
// Create new metadata
int64_t nCreationTime = GetTime();
CKeyMetadata metadata(nCreationTime);
// Derive the transparent key deterministically from the HD seed when the
// feature is enabled, so it can be recovered from the seed alone. Otherwise
// fall back to a random key (e.g. legacy wallets that have no HD seed).
if (IsHDTransparentEnabled()) {
DeriveNewChildKey(metadata, secret);
fCompressed = true; // BIP32-derived keys are always compressed
} else {
secret.MakeNewKey(fCompressed);
}
// Compressed public keys were introduced in version 0.6.0
if (fCompressed)
@@ -231,9 +245,7 @@ CPubKey CWallet::GenerateNewKey()
CPubKey pubkey = secret.GetPubKey();
assert(secret.VerifyPubKey(pubkey));
// Create new metadata
int64_t nCreationTime = GetTime();
mapKeyMetadata[pubkey.GetID()] = CKeyMetadata(nCreationTime);
mapKeyMetadata[pubkey.GetID()] = metadata;
if (!nTimeFirstKey || nCreationTime < nTimeFirstKey)
nTimeFirstKey = nCreationTime;
@@ -242,6 +254,57 @@ CPubKey CWallet::GenerateNewKey()
return pubkey;
}
// Derive a new transparent key from the HD seed along the BIP44 external chain
// m/44'/coin_type'/0'/0/i. The child index is taken from (and advances)
// hdChain.transparentChildCounter, which is persisted so the same keys can be
// regenerated after a seed-only restore. Mirrors GenerateNewSaplingZKey.
void CWallet::DeriveNewChildKey(CKeyMetadata& metadata, CKey& secretRet)
{
AssertLockHeld(cs_wallet); // mapKeyMetadata / hdChain
HDSeed seed;
if (!GetHDSeedForDerivation(seed))
throw std::runtime_error("CWallet::DeriveNewChildKey(): HD seed not found");
RawHDSeed rawSeed = seed.RawSeed();
CExtKey masterKey; // m
CExtKey purposeKey; // m/44'
CExtKey coinTypeKey; // m/44'/coin_type'
CExtKey accountKey; // m/44'/coin_type'/0'
CExtKey externalChainKey; // m/44'/coin_type'/0'/0
CExtKey childKey; // m/44'/coin_type'/0'/0/i
masterKey.SetMaster(rawSeed.data(), rawSeed.size());
uint32_t bip44CoinType = Params().BIP44CoinType();
// BIP44 path, single account (0'), external chain (0). On this ac_private=1
// chain the internal/change chain can never hold value, so it is unused.
masterKey.Derive(purposeKey, 44 | BIP32_HARDENED_KEY_LIMIT);
purposeKey.Derive(coinTypeKey, bip44CoinType | BIP32_HARDENED_KEY_LIMIT);
coinTypeKey.Derive(accountKey, 0 | BIP32_HARDENED_KEY_LIMIT);
accountKey.Derive(externalChainKey, 0);
// Derive the next child index, skipping any key already in the wallet.
do {
externalChainKey.Derive(childKey, hdChain.transparentChildCounter);
metadata.hdKeypath = "m/44'/" + std::to_string(bip44CoinType) + "'/0'/0/" + std::to_string(hdChain.transparentChildCounter);
metadata.seedFp = hdChain.seedFp;
hdChain.transparentChildCounter++;
} while (HaveKey(childKey.key.GetPubKey().GetID()));
secretRet = childKey.key;
// Bump a legacy v1 chain to v2 so the transparent counter gets persisted.
if (hdChain.nVersion < CHDChain::VERSION_HD_TRANSPARENT)
hdChain.nVersion = CHDChain::VERSION_HD_TRANSPARENT;
// Persist the advanced counter so restarts / restores don't reuse indices.
if (fFileBacked && !CWalletDB(strWalletFile).WriteHDChain(hdChain))
throw std::runtime_error("CWallet::DeriveNewChildKey(): Writing HD chain model failed");
}
bool CWallet::AddKeyPubKey(const CKey& secret, const CPubKey &pubkey)
{
AssertLockHeld(cs_wallet); // mapKeyMetadata
@@ -2290,18 +2353,39 @@ CAmount CWallet::GetChange(const CTransaction& tx) const
bool CWallet::IsHDFullyEnabled() const
{
// Only Sapling addresses are HD for now
return false;
// Both Sapling and transparent addresses are HD when transparent HD is on.
return IsHDTransparentEnabled();
}
bool CWallet::IsHDTransparentEnabled() const
{
// Transparent keys are HD-derived when the wallet has an HD seed and the
// feature is enabled (default on). Legacy wallets keep any pre-existing
// random t-keys; only newly generated keys become HD (and those old random
// keys are NOT seed-recoverable, so wallet.dat backups remain necessary).
return !hdChain.seedFp.IsNull() && GetBoolArg("-hdtransparent", true);
}
void CWallet::GenerateNewSeed()
{
LOCK(cs_wallet);
auto seed = HDSeed::Random(HD_WALLET_SEED_LENGTH);
int64_t nCreationTime = GetTime();
// Opt-in: create the wallet from a fresh BIP39 mnemonic so its 24 words can
// be exported (z_exportmnemonic) and used in SilentDragonXLite.
if (GetBoolArg("-usemnemonic", false)) {
RawHDSeed entropy;
if (GenerateMnemonicEntropy(256, entropy)) {
HDSeed seed(entropy);
if (InstallHDSeed(seed, true, nCreationTime))
return;
}
LogPrintf("%s: -usemnemonic seed generation failed, falling back to a random seed\n", __func__);
}
auto seed = HDSeed::Random(HD_WALLET_SEED_LENGTH);
// If the wallet is encrypted and locked, this will fail.
if (!SetHDSeed(seed))
throw std::runtime_error(std::string(__func__) + ": SetHDSeed failed");
@@ -2310,7 +2394,7 @@ void CWallet::GenerateNewSeed()
// the child index counter in the database
// as a hdchain object
CHDChain newHdChain;
newHdChain.nVersion = CHDChain::VERSION_HD_BASE;
newHdChain.nVersion = CHDChain::VERSION_HD_TRANSPARENT;
newHdChain.seedFp = seed.Fingerprint();
newHdChain.nCreateTime = nCreationTime;
SetHDChain(newHdChain, false);
@@ -2374,6 +2458,122 @@ bool CWallet::LoadCryptedHDSeed(const uint256& seedFp, const std::vector<unsigne
return CCryptoKeyStore::SetCryptedHDSeed(seedFp, seed);
}
bool CWallet::InstallHDSeed(const HDSeed& seed, bool fMnemonic, int64_t nCreateTime)
{
AssertLockHeld(cs_wallet);
if (!SetHDSeed(seed))
return false;
CHDChain newHdChain;
newHdChain.nVersion = fMnemonic ? CHDChain::VERSION_HD_MNEMONIC
: CHDChain::VERSION_HD_TRANSPARENT;
newHdChain.seedFp = seed.Fingerprint();
newHdChain.nCreateTime = nCreateTime;
newHdChain.fMnemonicSeed = fMnemonic;
SetHDChain(newHdChain, false);
return true;
}
bool CWallet::SetHDSeedFromHex(const std::string& seedHex)
{
LOCK(cs_wallet);
// Refuse to clobber an existing seed (the keystore refuses too); restore
// must run on a fresh/empty wallet.
if (HaveHDSeed())
return false;
if (!IsHex(seedHex))
return false;
std::vector<unsigned char> raw = ParseHex(seedHex);
// 32 = legacy raw seed; 64 = BIP39-derived seed (as exported by a mnemonic
// wallet). Either is used directly for derivation (fMnemonicSeed = false).
if (raw.size() != 32 && raw.size() != 64)
return false;
RawHDSeed rawSeed(raw.begin(), raw.end());
HDSeed seed(rawSeed);
return InstallHDSeed(seed, false, 1); // birthday = genesis for a restore
}
bool CWallet::SetHDSeedFromMnemonic(const std::string& phrase)
{
LOCK(cs_wallet);
if (HaveHDSeed())
return false;
RawHDSeed entropy;
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
}
bool CWallet::GetHDSeedForDerivation(HDSeed& seedOut) const
{
HDSeed stored;
if (!GetHDSeed(stored))
return false;
if (!hdChain.fMnemonicSeed) {
seedOut = stored; // legacy / hex seed: fed to derivation directly
return true;
}
// Mnemonic wallet: the stored seed is the 32-byte BIP39 entropy. Expand it
// to the 64-byte BIP39 seed exactly as SilentDragonXLite does.
RawHDSeed seed64;
if (!Bip39SeedFromEntropy(stored.RawSeed(), seed64))
return false;
seedOut = HDSeed(seed64);
return true;
}
bool CWallet::GetMnemonicPhrase(std::string& phraseOut) const
{
if (!hdChain.fMnemonicSeed)
return false;
HDSeed stored;
if (!GetHDSeed(stored)) // fails on an encrypted+locked wallet
return false;
return EntropyToMnemonic(stored.RawSeed(), phraseOut);
}
void CWallet::TopUpHDTransparentKeys(unsigned int count, int64_t nBirthday)
{
LOCK(cs_wallet);
if (!IsHDTransparentEnabled())
return;
for (unsigned int i = 0; i < count; i++) {
CKey secret;
CKeyMetadata metadata(nBirthday);
DeriveNewChildKey(metadata, secret);
CPubKey pubkey = secret.GetPubKey();
assert(secret.VerifyPubKey(pubkey));
mapKeyMetadata[pubkey.GetID()] = metadata;
// Keep the birthday floor at nBirthday so the rescan is not clipped
// (derived keys are stamped nBirthday, not "now", precisely for this).
if (!nTimeFirstKey || nBirthday < nTimeFirstKey)
nTimeFirstKey = nBirthday;
if (!AddKeyPubKey(secret, pubkey))
throw std::runtime_error("CWallet::TopUpHDTransparentKeys(): AddKeyPubKey failed");
}
}
void CWalletTx::SetSaplingNoteData(mapSaplingNoteData_t &noteData)
{
mapSaplingNoteData.clear();