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:
@@ -312,7 +312,7 @@ bool AsyncRPCOperation_mergetoaddress::main_impl()
|
||||
// recoverable, while keeping it logically separate from the ZIP 32
|
||||
// Sapling key hierarchy, which the user might not be using.
|
||||
HDSeed seed;
|
||||
if (!pwalletMain->GetHDSeed(seed)) {
|
||||
if (!pwalletMain->GetHDSeedForDerivation(seed)) {
|
||||
throw JSONRPCError(
|
||||
RPC_WALLET_ERROR,
|
||||
"AsyncRPCOperation_sendmany: HD seed not found");
|
||||
|
||||
@@ -377,7 +377,7 @@ bool AsyncRPCOperation_sendmany::main_impl() {
|
||||
// recoverable, while keeping it logically separate from the ZIP 32
|
||||
// Sapling key hierarchy, which the user might not be using.
|
||||
HDSeed seed;
|
||||
if (!pwalletMain->GetHDSeed(seed)) {
|
||||
if (!pwalletMain->GetHDSeedForDerivation(seed)) {
|
||||
throw JSONRPCError(
|
||||
RPC_WALLET_ERROR,
|
||||
"AsyncRPCOperation_sendmany::main_impl(): HD seed not found");
|
||||
|
||||
@@ -197,7 +197,7 @@ bool ShieldToAddress::operator()(const libzcash::SaplingPaymentAddress &zaddr) c
|
||||
// recoverable, while keeping it logically separate from the ZIP 32
|
||||
// Sapling key hierarchy, which the user might not be using.
|
||||
HDSeed seed;
|
||||
if (!pwalletMain->GetHDSeed(seed)) {
|
||||
if (!pwalletMain->GetHDSeedForDerivation(seed)) {
|
||||
throw JSONRPCError(
|
||||
RPC_WALLET_ERROR,
|
||||
"CWallet::GenerateNewSaplingZKey(): HD seed not found");
|
||||
|
||||
99
src/wallet/mnemonic.cpp
Normal file
99
src/wallet/mnemonic.cpp
Normal file
@@ -0,0 +1,99 @@
|
||||
// Copyright (c) 2016-2024 The Hush developers
|
||||
// Distributed under the GPLv3 software license, see the accompanying
|
||||
// file COPYING or https://www.gnu.org/licenses/gpl-3.0.en.html
|
||||
#include "wallet/mnemonic.h"
|
||||
|
||||
#include "random.h"
|
||||
#include "support/cleanse.h"
|
||||
|
||||
#include <mutex>
|
||||
#include <cstring>
|
||||
|
||||
extern "C" {
|
||||
#include "crypto/bip39/bip39.h"
|
||||
}
|
||||
|
||||
// The vendored BIP39 library references random_buffer() (used by its
|
||||
// mnemonic_generate()). We do not compile trezor's insecure rand.c; instead we
|
||||
// route it to the node CSPRNG so any BIP39 randomness is cryptographically
|
||||
// sound. random_buffer is declared weak in rand.c, so this strong definition
|
||||
// is the one that links.
|
||||
extern "C" void random_buffer(uint8_t* buf, size_t len)
|
||||
{
|
||||
GetRandBytes(buf, (int)len);
|
||||
}
|
||||
|
||||
// mnemonic_from_data()/mnemonic_to_seed() use process-static scratch buffers,
|
||||
// so serialize all access behind one lock and copy results out immediately.
|
||||
static std::mutex cs_bip39;
|
||||
|
||||
bool MnemonicIsValid(const std::string& phrase)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(cs_bip39);
|
||||
return mnemonic_check(phrase.c_str()) != 0;
|
||||
}
|
||||
|
||||
bool MnemonicToEntropy(const std::string& phrase, RawHDSeed& entropyOut)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(cs_bip39);
|
||||
// Reject bad checksum / unknown words first.
|
||||
if (mnemonic_check(phrase.c_str()) == 0) {
|
||||
return false;
|
||||
}
|
||||
// mnemonic_to_entropy() writes 33 bytes (entropy || 1 checksum byte) and
|
||||
// returns the total bit count (words * 11).
|
||||
uint8_t buf[33];
|
||||
int totalBits = mnemonic_to_entropy(phrase.c_str(), buf);
|
||||
if (totalBits <= 0) {
|
||||
return false;
|
||||
}
|
||||
int words = totalBits / 11;
|
||||
if (words != 12 && words != 18 && words != 24) {
|
||||
memory_cleanse(buf, sizeof(buf));
|
||||
return false;
|
||||
}
|
||||
int entropyBytes = words * 4 / 3; // 12->16, 18->24, 24->32
|
||||
entropyOut.assign(buf, buf + entropyBytes);
|
||||
memory_cleanse(buf, sizeof(buf));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EntropyToMnemonic(const RawHDSeed& entropy, std::string& phraseOut)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(cs_bip39);
|
||||
const char* phrase = mnemonic_from_data(entropy.data(), (int)entropy.size());
|
||||
if (phrase == nullptr) {
|
||||
return false;
|
||||
}
|
||||
phraseOut.assign(phrase);
|
||||
mnemonic_clear(); // wipe the static buffer
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Bip39SeedFromEntropy(const RawHDSeed& entropy, RawHDSeed& seed64Out)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(cs_bip39);
|
||||
// Regenerate the canonical phrase from entropy (matches SDXLite's
|
||||
// Mnemonic::from_entropy(entropy).phrase()), then PBKDF2 with an EMPTY
|
||||
// passphrase to get the standard 64-byte BIP39 seed.
|
||||
const char* phrase = mnemonic_from_data(entropy.data(), (int)entropy.size());
|
||||
if (phrase == nullptr) {
|
||||
return false;
|
||||
}
|
||||
uint8_t seed[64];
|
||||
mnemonic_to_seed(phrase, "", seed, nullptr);
|
||||
mnemonic_clear();
|
||||
seed64Out.assign(seed, seed + 64);
|
||||
memory_cleanse(seed, sizeof(seed));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GenerateMnemonicEntropy(int bits, RawHDSeed& entropyOut)
|
||||
{
|
||||
if (bits != 128 && bits != 160 && bits != 192 && bits != 224 && bits != 256) {
|
||||
return false;
|
||||
}
|
||||
entropyOut.resize(bits / 8);
|
||||
GetRandBytes(entropyOut.data(), (int)entropyOut.size());
|
||||
return true;
|
||||
}
|
||||
39
src/wallet/mnemonic.h
Normal file
39
src/wallet/mnemonic.h
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2016-2024 The Hush developers
|
||||
// Distributed under the GPLv3 software license, see the accompanying
|
||||
// file COPYING or https://www.gnu.org/licenses/gpl-3.0.en.html
|
||||
#ifndef HUSH_WALLET_MNEMONIC_H
|
||||
#define HUSH_WALLET_MNEMONIC_H
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "zcash/zip32.h" // RawHDSeed
|
||||
|
||||
// Thin, thread-safe C++ wrapper over the vendored BIP39 (trezor-crypto) library.
|
||||
// It reproduces SilentDragonXLite's tiny-bip39 0.6.2 conventions EXACTLY so the
|
||||
// same 24 words yield the same addresses in both wallets:
|
||||
// - English wordlist only (byte-identical to tiny-bip39's english.txt)
|
||||
// - empty BIP39 passphrase (no "25th word")
|
||||
// - PBKDF2-HMAC-SHA512, 2048 rounds, 64-byte seed
|
||||
// - the seed is derived from the CANONICAL phrase regenerated from entropy,
|
||||
// matching SDXLite's Mnemonic::from_entropy(entropy).phrase() round-trip.
|
||||
|
||||
//! True if `phrase` is a valid BIP39 mnemonic (word list + checksum).
|
||||
bool MnemonicIsValid(const std::string& phrase);
|
||||
|
||||
//! Parse `phrase` into its BIP39 entropy (16/20/24/28/32 bytes). Validates the
|
||||
//! checksum first. Returns false on any invalid input.
|
||||
bool MnemonicToEntropy(const std::string& phrase, RawHDSeed& entropyOut);
|
||||
|
||||
//! Regenerate the canonical English mnemonic phrase from `entropy`.
|
||||
bool EntropyToMnemonic(const RawHDSeed& entropy, std::string& phraseOut);
|
||||
|
||||
//! Derive the 64-byte BIP39 seed used for HD derivation from `entropy`, exactly
|
||||
//! as SilentDragonXLite does: canonical phrase from entropy, then PBKDF2 with an
|
||||
//! empty passphrase.
|
||||
bool Bip39SeedFromEntropy(const RawHDSeed& entropy, RawHDSeed& seed64Out);
|
||||
|
||||
//! Generate fresh BIP39 entropy of `bits` (128/160/192/224/256) from the node
|
||||
//! CSPRNG, for creating a new mnemonic wallet.
|
||||
bool GenerateMnemonicEntropy(int bits, RawHDSeed& entropyOut);
|
||||
|
||||
#endif // HUSH_WALLET_MNEMONIC_H
|
||||
@@ -742,7 +742,9 @@ UniValue dumpwallet_impl(const UniValue& params, bool fHelp, bool fDumpZKeys)
|
||||
file << strprintf("# mined on %s\n", EncodeDumpTime(chainActive.Tip()->GetBlockTime()));
|
||||
{
|
||||
HDSeed hdSeed;
|
||||
pwalletMain->GetHDSeed(hdSeed);
|
||||
// Dump the 64-byte derivation seed (for mnemonic wallets this is the
|
||||
// expanded BIP39 seed), so re-importing the hex reproduces the same keys.
|
||||
pwalletMain->GetHDSeedForDerivation(hdSeed);
|
||||
auto rawSeed = hdSeed.RawSeed();
|
||||
file << strprintf("# HDSeed=%s fingerprint=%s", HexStr(rawSeed.begin(), rawSeed.end()), hdSeed.Fingerprint().GetHex());
|
||||
file << "\n";
|
||||
@@ -1026,6 +1028,50 @@ UniValue z_exportkey(const UniValue& params, bool fHelp, const CPubKey& mypk)
|
||||
return EncodeSpendingKey(sk.get());
|
||||
}
|
||||
|
||||
UniValue z_exportmnemonic(const UniValue& params, bool fHelp, const CPubKey& mypk)
|
||||
{
|
||||
if (!EnsureWalletIsAvailable(fHelp))
|
||||
return NullUniValue;
|
||||
|
||||
if (fHelp || params.size() != 0)
|
||||
throw runtime_error(
|
||||
"z_exportmnemonic\n"
|
||||
"\nReveal the wallet's BIP39 seed phrase (24 words).\n"
|
||||
"The phrase is byte-compatible with SilentDragonXLite: the same words\n"
|
||||
"restore the same transparent and shielded addresses in either wallet.\n"
|
||||
"Only works for wallets created or restored from a mnemonic (see the\n"
|
||||
"-mnemonic and -usemnemonic options). Requires the wallet be unlocked.\n"
|
||||
"\nResult:\n"
|
||||
"{\n"
|
||||
" \"mnemonic\" : \"word1 ... word24\", (string) the BIP39 seed phrase\n"
|
||||
" \"seedfp\" : \"hex\" (string) the HD seed fingerprint\n"
|
||||
"}\n"
|
||||
"\nExamples:\n"
|
||||
+ HelpExampleCli("z_exportmnemonic", "")
|
||||
+ HelpExampleRpc("z_exportmnemonic", "")
|
||||
);
|
||||
|
||||
LOCK2(cs_main, pwalletMain->cs_wallet);
|
||||
|
||||
EnsureWalletIsUnlocked();
|
||||
|
||||
if (!pwalletMain->IsMnemonicSeed()) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR,
|
||||
"This wallet's seed was not derived from a mnemonic, so no seed phrase is available. "
|
||||
"Use z_exportwallet to back up the raw HD seed instead.");
|
||||
}
|
||||
|
||||
std::string phrase;
|
||||
if (!pwalletMain->GetMnemonicPhrase(phrase)) {
|
||||
throw JSONRPCError(RPC_WALLET_ERROR, "Could not retrieve the seed phrase (is the wallet unlocked?)");
|
||||
}
|
||||
|
||||
UniValue ret(UniValue::VOBJ);
|
||||
ret.push_back(Pair("mnemonic", phrase));
|
||||
ret.push_back(Pair("seedfp", pwalletMain->GetHDChain().seedFp.GetHex()));
|
||||
return ret;
|
||||
}
|
||||
|
||||
UniValue z_exportviewingkey(const UniValue& params, bool fHelp, const CPubKey& mypk)
|
||||
{
|
||||
if (!EnsureWalletIsAvailable(fHelp))
|
||||
|
||||
@@ -305,7 +305,7 @@ void zsTxSendsToJSON(const CWalletTx& wtx, UniValue& sends, CAmount& totalSends,
|
||||
//Decrypt sapling outgoing t to z transaction using HDseed
|
||||
if (wtx.vShieldedSpend.size()==0) {
|
||||
HDSeed seed;
|
||||
if (pwalletMain->GetHDSeed(seed)) {
|
||||
if (pwalletMain->GetHDSeedForDerivation(seed)) {
|
||||
auto opt = libzcash::SaplingOutgoingPlaintext::decrypt(
|
||||
outputDesc.outCiphertext,ovkForShieldingFromTaddr(seed),outputDesc.cv,outputDesc.cm,outputDesc.ephemeralKey);
|
||||
|
||||
|
||||
@@ -6272,6 +6272,7 @@ extern UniValue importaddress(const UniValue& params, bool fHelp, const CPubKey&
|
||||
extern UniValue dumpwallet(const UniValue& params, bool fHelp, const CPubKey& mypk);
|
||||
extern UniValue importwallet(const UniValue& params, bool fHelp, const CPubKey& mypk);
|
||||
extern UniValue z_exportkey(const UniValue& params, bool fHelp, const CPubKey& mypk);
|
||||
extern UniValue z_exportmnemonic(const UniValue& params, bool fHelp, const CPubKey& mypk);
|
||||
extern UniValue z_importkey(const UniValue& params, bool fHelp, const CPubKey& mypk);
|
||||
extern UniValue z_exportviewingkey(const UniValue& params, bool fHelp, const CPubKey& mypk);
|
||||
extern UniValue z_importviewingkey(const UniValue& params, bool fHelp, const CPubKey& mypk);
|
||||
@@ -6351,6 +6352,7 @@ static const CRPCCommand commands[] =
|
||||
{ "wallet", "z_getnewaddress", &z_getnewaddress, true },
|
||||
{ "wallet", "z_listaddresses", &z_listaddresses, true },
|
||||
{ "wallet", "z_exportkey", &z_exportkey, true },
|
||||
{ "wallet", "z_exportmnemonic", &z_exportmnemonic, true },
|
||||
{ "wallet", "z_importkey", &z_importkey, true },
|
||||
{ "wallet", "z_exportviewingkey", &z_exportviewingkey, true },
|
||||
{ "wallet", "z_importviewingkey", &z_importviewingkey, true },
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1063,6 +1063,9 @@ public:
|
||||
* Generate a new key
|
||||
*/
|
||||
CPubKey GenerateNewKey();
|
||||
//! Derive a new transparent key from the HD seed along the BIP44 external
|
||||
//! chain m/44'/coin_type'/0'/0/i, advancing hdChain.transparentChildCounter.
|
||||
void DeriveNewChildKey(CKeyMetadata& metadata, CKey& secretRet);
|
||||
//! Adds a key to the store, and saves it to disk.
|
||||
bool AddKeyPubKey(const CKey& key, const CPubKey &pubkey);
|
||||
//! Adds a key to the store, without saving it to disk (used by LoadWallet)
|
||||
@@ -1294,6 +1297,10 @@ public:
|
||||
/* Returns true if HD is enabled for all address types, false if only for Sapling */
|
||||
bool IsHDFullyEnabled() const;
|
||||
|
||||
/* Returns true if transparent keys should be HD-derived from the seed.
|
||||
Requires an HD seed and the -hdtransparent option (default on). */
|
||||
bool IsHDTransparentEnabled() const;
|
||||
|
||||
/* Generates a new HD seed (will reset the chain child index counters)
|
||||
Sets the seed's version based on the current wallet version (so the
|
||||
caller must ensure the current wallet version is correct before calling
|
||||
@@ -1303,6 +1310,41 @@ public:
|
||||
bool SetHDSeed(const HDSeed& seed);
|
||||
bool SetCryptedHDSeed(const uint256& seedFp, const std::vector<unsigned char> &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
|
||||
64 bytes for a BIP39-derived seed. Only succeeds on a wallet that has no
|
||||
seed yet. Sets the chain birthday to genesis so a rescan finds all
|
||||
historical (coinbase) funds. Returns false on bad input or existing seed. */
|
||||
bool SetHDSeedFromHex(const std::string& seedHex);
|
||||
|
||||
/* Restore/create a wallet from a BIP39 mnemonic phrase, byte-compatible with
|
||||
SilentDragonXLite: stores the 32-byte entropy, marks the chain mnemonic,
|
||||
and derives the 64-byte BIP39 seed on demand. Only succeeds on a wallet
|
||||
with no seed yet. Returns false on an invalid phrase or existing seed. */
|
||||
bool SetHDSeedFromMnemonic(const std::string& phrase);
|
||||
|
||||
/* Return the wallet's 24-word BIP39 recovery phrase, if this is a mnemonic
|
||||
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; }
|
||||
|
||||
/* 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
|
||||
wallets it is the stored seed unchanged. Use this everywhere keys/OVKs are
|
||||
derived so behaviour matches SilentDragonXLite. */
|
||||
bool GetHDSeedForDerivation(HDSeed& seedOut) const;
|
||||
|
||||
/* Shared tail of the seed-install paths: stores `seed` and a fresh CHDChain
|
||||
(mnemonic vs raw) with the given birthday. Caller must hold cs_wallet. */
|
||||
bool InstallHDSeed(const HDSeed& seed, bool fMnemonic, int64_t nCreateTime);
|
||||
|
||||
/* Pre-derive `count` HD transparent keys (external chain) into the keystore,
|
||||
stamped with creation time `nBirthday`, so a subsequent rescan can find
|
||||
funds paid to them after a seed-only restore. */
|
||||
void TopUpHDTransparentKeys(unsigned int count, int64_t nBirthday);
|
||||
|
||||
/* Set the HD chain model (chain child index counters) */
|
||||
void SetHDChain(const CHDChain& chain, bool memonly);
|
||||
const CHDChain& GetHDChain() const { return hdChain; }
|
||||
|
||||
@@ -62,11 +62,24 @@ class CHDChain
|
||||
{
|
||||
public:
|
||||
static const int VERSION_HD_BASE = 1;
|
||||
static const int CURRENT_VERSION = VERSION_HD_BASE;
|
||||
// Version 2 adds the transparent (secp256k1/BIP44) external-chain counter.
|
||||
static const int VERSION_HD_TRANSPARENT = 2;
|
||||
// Version 3 marks a seed derived from a BIP39 mnemonic: the stored HDSeed is
|
||||
// the 32-byte BIP39 entropy, expanded to the 64-byte seed for derivation
|
||||
// (matches SilentDragonXLite's on-disk convention).
|
||||
static const int VERSION_HD_MNEMONIC = 3;
|
||||
static const int CURRENT_VERSION = VERSION_HD_MNEMONIC;
|
||||
int nVersion;
|
||||
uint256 seedFp;
|
||||
int64_t nCreateTime; // 0 means unknown
|
||||
uint32_t saplingAccountCounter;
|
||||
// Next index on the HD transparent external chain m/44'/coin'/0'/0/i.
|
||||
// Only serialized/consulted when nVersion >= VERSION_HD_TRANSPARENT.
|
||||
uint32_t transparentChildCounter;
|
||||
// True when the stored HDSeed is BIP39 entropy that must be expanded to the
|
||||
// 64-byte BIP39 seed before HD derivation. Only serialized when
|
||||
// nVersion >= VERSION_HD_MNEMONIC (false for all pre-existing wallets).
|
||||
bool fMnemonicSeed;
|
||||
|
||||
CHDChain() { SetNull(); }
|
||||
|
||||
@@ -79,6 +92,14 @@ public:
|
||||
READWRITE(seedFp);
|
||||
READWRITE(nCreateTime);
|
||||
READWRITE(saplingAccountCounter);
|
||||
// Version-gated so pre-existing v1 wallet.dat records still deserialize
|
||||
// (they simply leave the newer fields at their SetNull defaults).
|
||||
if (this->nVersion >= VERSION_HD_TRANSPARENT) {
|
||||
READWRITE(transparentChildCounter);
|
||||
}
|
||||
if (this->nVersion >= VERSION_HD_MNEMONIC) {
|
||||
READWRITE(fMnemonicSeed);
|
||||
}
|
||||
}
|
||||
|
||||
void SetNull()
|
||||
@@ -87,6 +108,8 @@ public:
|
||||
seedFp.SetNull();
|
||||
nCreateTime = 0;
|
||||
saplingAccountCounter = 0;
|
||||
transparentChildCounter = 0;
|
||||
fMnemonicSeed = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user