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:
+210
-10
@@ -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 ¬eData)
|
||||
{
|
||||
mapSaplingNoteData.clear();
|
||||
|
||||
Reference in New Issue
Block a user