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
parent 84aefb5475
commit 4caf2fc68f
23 changed files with 1044 additions and 22 deletions

View File

@@ -0,0 +1,87 @@
# HD transparent keys
DragonX derives **transparent** (t-address) keys deterministically from the
wallet's HD seed, so they can be recovered from the seed alone — the same way
Sapling (shielded) keys already are.
## Derivation
Transparent keys are derived over secp256k1 using BIP32/BIP44:
```
m / 44' / coin_type' / 0' / 0 / i
```
* `coin_type` is `Params().BIP44CoinType()`**141** on mainnet, **1** on
test/regtest.
* Account is fixed at `0'` and the chain at `0` (external). The internal/change
chain (`1`) is **not** used: on this `ac_private=1` chain a non-coinbase
transparent output is consensus-invalid, so transparent change can never carry
value.
* `i` is `CHDChain.transparentChildCounter`, a monotonic index persisted in the
wallet so the same addresses regenerate after a seed-only restore.
Each derived key records its `hdKeypath` and the seed fingerprint (`seedFp`) in
its `CKeyMetadata`, matching the Sapling scheme.
## Why this matters on a private chain
On DragonX (`ac_private=1` from genesis) a normal user can never *receive* to a
transparent address — inbound t-payments are rejected by consensus. The only
thing that legitimately lands spendable value on a t-address is a **mining
coinbase** (plus notary/burn special cases). There is no "coinbase must be
shielded" rule, so mature coinbase is directly spendable.
So HD transparent keys exist to let a **miner recover coinbase rewards** that
were paid to wallet-derived t-addresses, using only the seed.
## Enabling / disabling
Controlled by `-hdtransparent` (default **on**). When on and the wallet has an
HD seed, every newly generated transparent key (receive address, change,
coinbase payout drawn from the keypool) is HD-derived.
```
-hdtransparent=0 # keep the legacy behaviour (random transparent keys)
```
## Backing up and restoring
* **Back up the seed.** `z_exportwallet <file>` writes the 32-byte HD seed as a
`# HDSeed=<hex>` line. Guard this value like a private key.
* **Restore into a fresh/empty wallet** by starting the node with:
```
-hdseed=<64-hex-character seed>
-hdtransparentgaplimit=<n> # HD transparent keys to pre-derive (default 1000)
```
On restore the node injects the seed, pre-derives `n` transparent keys with a
genesis birthday, and the normal startup rescan finds any coinbase paid to
them. Raise `-hdtransparentgaplimit` if the wallet minted more than `n`
distinct coinbase addresses.
> **Warning:** passing `-hdseed` on the command line exposes the seed to your
> shell history and the process list. Prefer putting it in `DRAGONX.conf` with
> tight file permissions, and remove it after the restore completes.
## Limitations (read before relying on recovery)
* **Legacy random keys are not recoverable.** Any transparent key created before
this feature (or with `-hdtransparent=0`) came from the CSPRNG, not the seed,
and the phrase/seed will **not** regenerate it. Keep `wallet.dat` /
`dumpwallet` backups for those. A wallet that predates the feature and then
enables it becomes a *mix* of random (old) and HD (new) keys.
* **Gap limit.** A rescan only discovers keys already present in the wallet.
Restore pre-derives `-hdtransparentgaplimit` keys; coinbase paid to an index
beyond that window is not found until you derive further and rescan again.
* **Scope.** Recovers transparent **coinbase** value only, per the consensus
rules above. Shielded funds are recovered separately via the Sapling HD keys.
## On-disk compatibility
The transparent counter is stored in `CHDChain` under a new serialization
version (`VERSION_HD_TRANSPARENT = 2`). Existing v1 `wallet.dat` records load
unchanged (the counter defaults to 0); the record is rewritten as v2 the first
time an HD transparent key is derived. Downgrading a v2 wallet to an older
binary is not supported.

90
doc/seed-phrase.md Normal file
View File

@@ -0,0 +1,90 @@
# BIP39 seed phrases (SilentDragonXLite-compatible)
DragonX full-node wallets can be created from and restored to a **BIP39 24-word
seed phrase** that is **byte-for-byte compatible with SilentDragonXLite**: the
same words produce the same transparent (t-) and shielded (z-) addresses in
either wallet, so funds move between the light wallet and the full node with one
backup.
## What makes them compatible
Compatibility requires the mnemonic, the seed derivation, and every HD path to
match exactly. They do:
| Detail | Value (both wallets) |
|---|---|
| Word list | BIP39 English, 2048 words |
| Passphrase | empty (no "25th word") |
| Mnemonic → seed | PBKDF2-HMAC-SHA512, 2048 rounds, salt `"mnemonic"`, 64-byte output |
| Coin type | 141 (KMD SLIP-0044) |
| Shielded path | `m/32'/141'/i'` (ZIP-32) |
| Transparent path | `m/44'/141'/0'/0/i` (BIP44) |
The node stores the 32-byte BIP39 **entropy** (SilentDragonXLite's on-disk
convention) and expands it to the 64-byte seed on demand for derivation. The
node's vendored BIP39 library (`src/crypto/bip39`) is byte-identical to
SilentDragonXLite's `tiny-bip39` 0.6.2, and the derivation is anchored by a
known-answer test (`src/gtest/test_mnemonic_compat.cpp`).
## Restore from a phrase
Start the node once, on a **fresh/empty datadir**, with the phrase:
```
dragonxd -mnemonic="word1 word2 ... word24"
```
or, preferably (keeps the phrase out of your shell history and process list),
put it in `DRAGONX.conf` with tight permissions:
```
mnemonic=word1 word2 ... word24
```
On restore the node pre-derives keys and rescans from genesis to recover funds:
* `-hdtransparentgaplimit=<n>` — HD transparent keys to pre-derive (default 1000)
* `-mnemonicsaplinggap=<n>` — shielded addresses to pre-derive (default 100)
Raise these if the wallet used many addresses. Restore only works on a wallet
with no seed yet (a brand-new datadir); it refuses to overwrite an existing seed.
## Create a new phrase on the node
By default new node wallets use a random (non-mnemonic) seed. To create a new
wallet from a fresh 24-word phrase instead — so you can export it and use it in
SilentDragonXLite — start with:
```
dragonxd -usemnemonic
```
## Show / back up the phrase
For a mnemonic wallet (created with `-usemnemonic` or restored with `-mnemonic`):
```
dragonx-cli z_exportmnemonic
```
returns the 24 words and the seed fingerprint. The wallet must be unlocked.
Guard the phrase like a private key.
## Limitations
* **English + empty passphrase only.** Any other word list or a BIP39 passphrase
would break compatibility, so they are not accepted.
* **Legacy / random-seed wallets have no phrase.** A wallet created before this
feature (or without `-usemnemonic`) has a random seed; `z_exportmnemonic`
returns an error for it — use `z_exportwallet` to back up the raw seed. Such
wallets are not SilentDragonXLite-compatible.
* **Scope.** Recovers HD-derived shielded funds and transparent coinbase (see
[hd-transparent-keys.md](hd-transparent-keys.md) for why only coinbase lands on
t-addresses on this `ac_private=1` chain). Keys imported with `z_importkey` are
not seed-derived and are not recovered by the phrase.
## On-disk compatibility
Mnemonic wallets set `CHDChain` version 3 (`VERSION_HD_MNEMONIC`). Older wallet
records load unchanged. Downgrading a mnemonic wallet to an older binary is not
supported.

View File

@@ -325,6 +325,7 @@ libbitcoin_wallet_a_SOURCES = \
wallet/asyncrpcoperation_shieldcoinbase.cpp \
wallet/crypter.cpp \
wallet/db.cpp \
wallet/mnemonic.cpp \
zcash/Note.cpp \
transaction_builder.cpp \
wallet/rpcdump.cpp \
@@ -361,6 +362,23 @@ crypto_libbitcoin_crypto_a_SOURCES = \
crypto/sha512.cpp \
crypto/sha512.h
# Vendored trezor-crypto BIP39 (mnemonic seed phrases). Kept self-contained so
# the same 24 words are compatible with SilentDragonXLite (tiny-bip39 0.6.2).
crypto_libbitcoin_crypto_a_SOURCES += \
crypto/bip39/bip39.c \
crypto/bip39/bip39.h \
crypto/bip39/bip39_english.h \
crypto/bip39/pbkdf2.c \
crypto/bip39/pbkdf2.h \
crypto/bip39/hmac.c \
crypto/bip39/hmac.h \
crypto/bip39/sha2.c \
crypto/bip39/sha2.h \
crypto/bip39/memzero.c \
crypto/bip39/memzero.h \
crypto/bip39/options.h \
crypto/bip39/rand.h
if EXPERIMENTAL_ASM
crypto_libbitcoin_crypto_a_SOURCES += crypto/sha256_sse4.cpp
endif

View File

@@ -11,7 +11,9 @@ bin_PROGRAMS += hush-gtest
hush_gtest_SOURCES = \
gtest/main.cpp \
gtest/utils.cpp \
gtest/test_randomx_preverify.cpp
gtest/test_randomx_preverify.cpp \
gtest/test_hdtransparent.cpp \
gtest/test_mnemonic_compat.cpp
hush_gtest_CPPFLAGS = $(AM_CPPFLAGS) -DMULTICORE -fopenmp -DBINARY_OUTPUT -DCURVE_ALT_BN128 -DSTATIC $(BITCOIN_INCLUDES)
hush_gtest_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS)

View File

@@ -33,10 +33,13 @@
#include "rand.h"
#include "sha2.h"
#if USE_BIP39_CACHE
// BIP39_WORDS is used unconditionally by the wordlist helpers below, so it must
// be defined even when the BIP39 cache is disabled (upstream places it inside
// the cache block by mistake).
int BIP39_WORDS = 2048;
#if USE_BIP39_CACHE
static int bip39_cache_index = 0;
static CONFIDENTIAL struct {

View File

@@ -56,8 +56,10 @@
#endif
// implement BIP39 caching
// Disabled: caching keeps the plaintext mnemonic/passphrase/seed in a static
// process-lifetime buffer, which we do not want in a wallet daemon.
#ifndef USE_BIP39_CACHE
#define USE_BIP39_CACHE 1
#define USE_BIP39_CACHE 0
#define BIP39_CACHE_SIZE 4
#endif

View File

@@ -0,0 +1,171 @@
// 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
//
// Tests for HD-derived transparent keys (m/44'/coin'/0'/0/i) and the
// version-gated CHDChain serialization used to persist the transparent counter.
#include <gtest/gtest.h>
#include "key.h"
#include "chainparams.h"
#include "streams.h"
#include "uint256.h"
#include "util.h"
#include "version.h"
#include "wallet/wallet.h"
#include "wallet/walletdb.h"
#include "zcash/zip32.h"
// Build an in-memory wallet with a known seed + hdChain so that the
// HD-transparent path (IsHDTransparentEnabled) is active.
static void LoadSeedForTest(CWallet& wallet, const HDSeed& seed)
{
wallet.LoadHDSeed(seed);
CHDChain chain;
chain.nVersion = CHDChain::VERSION_HD_TRANSPARENT;
chain.seedFp = seed.Fingerprint();
chain.nCreateTime = 1;
wallet.SetHDChain(chain, true /* memonly */);
}
// Same seed must reproduce the same transparent addresses in the same order:
// this is the recovery guarantee that lets a seed-only restore find coinbase.
TEST(hdtransparent_tests, DeterministicFromSeed)
{
SelectParams(CBaseChainParams::MAIN);
RawHDSeed rawSeed(HD_WALLET_SEED_LENGTH, 0x42);
HDSeed seed(rawSeed);
std::vector<CKeyID> keysA;
{
CWallet wallet;
LoadSeedForTest(wallet, seed);
ASSERT_TRUE(wallet.IsHDTransparentEnabled());
LOCK(wallet.cs_wallet);
for (int i = 0; i < 5; i++) {
CPubKey pk = wallet.GenerateNewKey();
keysA.push_back(pk.GetID());
const CKeyMetadata& md = wallet.mapKeyMetadata[pk.GetID()];
EXPECT_EQ(md.seedFp, seed.Fingerprint());
EXPECT_EQ(md.hdKeypath, std::string("m/44'/141'/0'/0/") + std::to_string(i));
}
}
// Fresh wallet, same seed -> identical keys.
{
CWallet wallet;
LoadSeedForTest(wallet, seed);
LOCK(wallet.cs_wallet);
for (int i = 0; i < 5; i++) {
CPubKey pk = wallet.GenerateNewKey();
EXPECT_EQ(pk.GetID(), keysA[i]);
}
}
}
// Pin the exact derivation path so it can never silently change.
TEST(hdtransparent_tests, KnownDerivationPath)
{
SelectParams(CBaseChainParams::MAIN);
RawHDSeed rawSeed(HD_WALLET_SEED_LENGTH, 0x42);
HDSeed seed(rawSeed);
// Independently derive m/44'/141'/0'/0/0.
RawHDSeed raw = seed.RawSeed();
CExtKey m, purpose, coinType, account, external, child;
m.SetMaster(raw.data(), raw.size());
m.Derive(purpose, 44 | BIP32_HARDENED_KEY_LIMIT);
purpose.Derive(coinType, 141 | BIP32_HARDENED_KEY_LIMIT);
coinType.Derive(account, 0 | BIP32_HARDENED_KEY_LIMIT);
account.Derive(external, 0);
external.Derive(child, 0);
CKeyID expected = child.key.GetPubKey().GetID();
CWallet wallet;
LoadSeedForTest(wallet, seed);
LOCK(wallet.cs_wallet);
CPubKey pk = wallet.GenerateNewKey();
EXPECT_EQ(pk.GetID(), expected);
}
// A pre-existing v1 CHDChain record (no transparent counter) must still
// deserialize under v2 code, leaving transparentChildCounter at 0; and a v2
// record must round-trip the counter.
TEST(hdtransparent_tests, HDChainVersionCompat)
{
CHDChain v1;
v1.nVersion = CHDChain::VERSION_HD_BASE; // 1: transparentChildCounter not serialized
v1.seedFp = uint256S("0000000000000000000000000000000000000000000000000000000000000001");
v1.nCreateTime = 12345;
v1.saplingAccountCounter = 7;
CDataStream ss(SER_DISK, PROTOCOL_VERSION);
ss << v1;
CHDChain out; // default-constructed: SetNull() zeroes transparentChildCounter
ss >> out;
EXPECT_EQ(out.nVersion, +CHDChain::VERSION_HD_BASE); // unary + -> rvalue, avoid ODR-use of static const
EXPECT_EQ(out.seedFp, v1.seedFp);
EXPECT_EQ(out.nCreateTime, (int64_t)12345);
EXPECT_EQ(out.saplingAccountCounter, (uint32_t)7);
EXPECT_EQ(out.transparentChildCounter, (uint32_t)0);
CHDChain v2;
v2.nVersion = CHDChain::VERSION_HD_TRANSPARENT;
v2.saplingAccountCounter = 3;
v2.transparentChildCounter = 42;
CDataStream ss2(SER_DISK, PROTOCOL_VERSION);
ss2 << v2;
CHDChain out2;
ss2 >> out2;
EXPECT_EQ(out2.nVersion, +CHDChain::VERSION_HD_TRANSPARENT);
EXPECT_EQ(out2.saplingAccountCounter, (uint32_t)3);
EXPECT_EQ(out2.transparentChildCounter, (uint32_t)42);
}
// Restoring from a 32-byte seed hex reproduces the same keys as the source
// wallet, and refuses to run when a seed already exists.
TEST(hdtransparent_tests, RestoreFromSeedHex)
{
SelectParams(CBaseChainParams::MAIN);
RawHDSeed rawSeed(HD_WALLET_SEED_LENGTH, 0x7a);
HDSeed seed(rawSeed);
std::string seedHex = HexStr(seed.RawSeed());
// Source wallet: derive some keys.
std::vector<CKeyID> expected;
{
CWallet wallet;
LoadSeedForTest(wallet, seed);
LOCK(wallet.cs_wallet);
for (int i = 0; i < 3; i++)
expected.push_back(wallet.GenerateNewKey().GetID());
}
// Restored wallet: inject the seed hex, pre-derive, and compare.
{
CWallet wallet;
ASSERT_TRUE(wallet.SetHDSeedFromHex(seedHex));
// Second attempt must fail: a seed already exists.
EXPECT_FALSE(wallet.SetHDSeedFromHex(seedHex));
wallet.TopUpHDTransparentKeys(3, 1);
LOCK(wallet.cs_wallet);
for (int i = 0; i < 3; i++)
EXPECT_TRUE(wallet.HaveKey(expected[i]));
}
// Bad input is rejected.
{
CWallet wallet;
EXPECT_FALSE(wallet.SetHDSeedFromHex("nothex"));
EXPECT_FALSE(wallet.SetHDSeedFromHex("abcd")); // too short
}
}

View File

@@ -0,0 +1,141 @@
// 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
//
// Proves that a BIP39 seed phrase produces the SAME transparent and shielded
// addresses on the DragonX full node as in SilentDragonXLite. The proof chain:
// phrase -> entropy (round-trip) -> 64-byte BIP39 seed (known-answer)
// -> z/t addresses (wallet path == direct ZIP-32/BIP44 derivation).
// The 64-byte seed is anchored to the well-known BIP39 value for the all-zero
// "abandon...art" entropy with an EMPTY passphrase, which is exactly what
// SilentDragonXLite's tiny-bip39 0.6.2 feeds into the same coin_type=141 paths.
#include <gtest/gtest.h>
#include "chainparams.h"
#include "key.h"
#include "key_io.h"
#include "util.h"
#include "wallet/mnemonic.h"
#include "wallet/wallet.h"
#include "zcash/Address.hpp"
#include "zcash/zip32.h"
// The canonical 24-word phrase for 32 bytes of all-zero entropy.
static const char* ABANDON_ART =
"abandon abandon abandon abandon abandon abandon abandon abandon "
"abandon abandon abandon abandon abandon abandon abandon abandon "
"abandon abandon abandon abandon abandon abandon abandon art";
// The standard BIP39 seed for that phrase with an EMPTY passphrase
// (PBKDF2-HMAC-SHA512, 2048 rounds, salt "mnemonic"). Matches tiny-bip39.
static const char* SEED64_HEX =
"408b285c123836004f4b8842c89324c1f01382450c0d439af345ba7fc49acf70"
"5489c6fc77dbd4e3dc1dd8cc6bc9f043db8ada1e243c4a0eafb290d399480840";
// First shielded address for a 64-byte seed: m/32'/141'/0' default address.
static std::string DeriveZAddrFromSeed64(RawHDSeed seed64)
{
HDSeed s(seed64);
auto m = libzcash::SaplingExtendedSpendingKey::Master(s);
auto xsk = m.Derive(32 | ZIP32_HARDENED_KEY_LIMIT)
.Derive(141 | ZIP32_HARDENED_KEY_LIMIT)
.Derive(0 | ZIP32_HARDENED_KEY_LIMIT);
return EncodePaymentAddress(xsk.DefaultAddress());
}
// First transparent address for a BIP32 master over `seedBytes`:
// m/44'/141'/0'/0/0.
static std::string DeriveTAddrFromSeedBytes(RawHDSeed seedBytes)
{
CExtKey master, purpose, coinType, account, external, child;
master.SetMaster(seedBytes.data(), seedBytes.size());
master.Derive(purpose, 44 | BIP32_HARDENED_KEY_LIMIT);
purpose.Derive(coinType, 141 | BIP32_HARDENED_KEY_LIMIT);
coinType.Derive(account, 0 | BIP32_HARDENED_KEY_LIMIT);
account.Derive(external, 0);
external.Derive(child, 0);
return EncodeDestination(child.key.GetPubKey().GetID());
}
// The 64-byte seed derived from the mnemonic must equal the known BIP39 value.
// This is the cross-wallet anchor: SilentDragonXLite feeds the identical seed.
TEST(mnemonic_compat, Bip39SeedKnownAnswer)
{
RawHDSeed entropy(32, 0);
RawHDSeed seed64;
ASSERT_TRUE(Bip39SeedFromEntropy(entropy, seed64));
ASSERT_EQ(seed64.size(), (size_t)64);
EXPECT_EQ(HexStr(seed64.begin(), seed64.end()), std::string(SEED64_HEX));
}
TEST(mnemonic_compat, EntropyPhraseRoundTrip)
{
RawHDSeed zeros(32, 0);
std::string phrase;
ASSERT_TRUE(EntropyToMnemonic(zeros, phrase));
EXPECT_EQ(phrase, std::string(ABANDON_ART));
EXPECT_TRUE(MnemonicIsValid(ABANDON_ART));
RawHDSeed entropy;
ASSERT_TRUE(MnemonicToEntropy(ABANDON_ART, entropy));
EXPECT_EQ(entropy.size(), (size_t)32);
EXPECT_EQ(HexStr(entropy.begin(), entropy.end()), std::string(64, '0'));
// Bad checksum / unknown words are rejected.
EXPECT_FALSE(MnemonicIsValid("abandon abandon abandon"));
EXPECT_FALSE(MnemonicIsValid("clearly not valid bip39 words at all here"));
RawHDSeed junk;
EXPECT_FALSE(MnemonicToEntropy("clearly not valid bip39 words at all here", junk));
}
// The wallet's mnemonic derivation must reproduce the exact addresses obtained
// by driving ZIP-32 / BIP44 directly from the known 64-byte seed, and must be
// deterministic across wallets.
TEST(mnemonic_compat, WalletDerivesSdxliteAddresses)
{
SelectParams(CBaseChainParams::MAIN);
RawHDSeed zeros(32, 0), seed64;
ASSERT_TRUE(Bip39SeedFromEntropy(zeros, seed64));
const std::string expZ = DeriveZAddrFromSeed64(seed64);
const std::string expT = DeriveTAddrFromSeedBytes(seed64);
EXPECT_EQ(expZ.substr(0, 2), "zs"); // sapling HRP for mainnet
CWallet wallet;
ASSERT_TRUE(wallet.SetHDSeedFromMnemonic(ABANDON_ART));
ASSERT_TRUE(wallet.IsMnemonicSeed());
{
LOCK(wallet.cs_wallet);
EXPECT_EQ(EncodePaymentAddress(wallet.GenerateNewSaplingZKey()), expZ);
EXPECT_EQ(EncodeDestination(wallet.GenerateNewKey().GetID()), expT);
}
// Same phrase, fresh wallet -> identical first addresses.
CWallet wallet2;
ASSERT_TRUE(wallet2.SetHDSeedFromMnemonic(ABANDON_ART));
{
LOCK(wallet2.cs_wallet);
EXPECT_EQ(EncodePaymentAddress(wallet2.GenerateNewSaplingZKey()), expZ);
EXPECT_EQ(EncodeDestination(wallet2.GenerateNewKey().GetID()), expT);
}
// The phrase round-trips out of the wallet.
std::string exported;
ASSERT_TRUE(wallet.GetMnemonicPhrase(exported));
EXPECT_EQ(exported, std::string(ABANDON_ART));
}
// Negative: feeding the 32-byte entropy DIRECTLY as the seed (the classic
// interop bug) must produce a different address than the 64-byte BIP39 seed.
TEST(mnemonic_compat, RawEntropyDiffersFromMnemonicSeed)
{
SelectParams(CBaseChainParams::MAIN);
RawHDSeed zeros(32, 0), seed64;
ASSERT_TRUE(Bip39SeedFromEntropy(zeros, seed64));
const std::string seedT = DeriveTAddrFromSeedBytes(seed64); // correct (64-byte)
const std::string entropyT = DeriveTAddrFromSeedBytes(zeros); // wrong (32-byte)
EXPECT_NE(seedT, entropyT);
}

View File

@@ -467,6 +467,12 @@ std::string HelpMessage(HelpMessageMode mode)
strUsage += HelpMessageGroup(_("Wallet options:"));
strUsage += HelpMessageOpt("-disablewallet", _("Do not load the wallet and disable wallet RPC calls"));
strUsage += HelpMessageOpt("-keypool=<n>", strprintf(_("Set key pool size to <n> (default: %u)"), 100));
strUsage += HelpMessageOpt("-hdtransparent", strprintf(_("Derive transparent addresses from the HD seed so they can be recovered from it (default: %u)"), 1));
strUsage += HelpMessageOpt("-hdseed=<hex>", _("Restore a fresh/empty wallet from a 32- or 64-byte HD seed hex (the value shown in z_exportwallet's '# HDSeed=' line). WARNING: exposes the seed to your shell history and process list."));
strUsage += HelpMessageOpt("-mnemonic=<words>", _("Restore/create a fresh/empty wallet from a BIP39 seed phrase, compatible with SilentDragonXLite (English, no passphrase). WARNING: exposes the phrase to your shell history and process list; prefer DRAGONX.conf with tight permissions."));
strUsage += HelpMessageOpt("-usemnemonic", strprintf(_("Create new wallets from a fresh BIP39 seed phrase so the 24 words can be exported (z_exportmnemonic) and used in SilentDragonXLite (default: %u)"), 0));
strUsage += HelpMessageOpt("-hdtransparentgaplimit=<n>", strprintf(_("On -mnemonic/-hdseed restore, pre-derive this many HD transparent keys so a rescan can find coinbase paid to them (default: %u)"), 1000));
strUsage += HelpMessageOpt("-mnemonicsaplinggap=<n>", strprintf(_("On -mnemonic/-hdseed restore, pre-derive this many shielded (Sapling) addresses so a rescan can find notes sent to them (default: %u)"), 100));
strUsage += HelpMessageOpt("-consolidation", _("Enable auto Sapling note consolidation (default: false)"));
strUsage += HelpMessageOpt("-consolidationinterval", _("Block interval between consolidations (default: 25)"));
strUsage += HelpMessageOpt("-consolidatesaplingaddress=<zaddr>", _("Specify Sapling Address to Consolidate. (default: all)"));
@@ -2266,8 +2272,54 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
if (!pwalletMain->HaveHDSeed())
{
// generate a new HD seed
pwalletMain->GenerateNewSeed();
std::string mnemonic = GetArg("-mnemonic", "");
std::string hdSeedHex = GetArg("-hdseed", "");
bool restoring = false;
if (!mnemonic.empty() && !hdSeedHex.empty())
return InitError(_("Specify only one of -mnemonic or -hdseed, not both"));
if (!mnemonic.empty())
{
// Restore/create a wallet from a BIP39 seed phrase, byte-compatible
// with SilentDragonXLite. Must be a fresh/empty wallet.
if (!pwalletMain->SetHDSeedFromMnemonic(mnemonic))
return InitError(_("Invalid -mnemonic: expected a valid BIP39 English phrase on a fresh/empty wallet"));
LogPrintf("%s: restoring wallet from -mnemonic seed phrase\n", __func__);
restoring = true;
}
else if (!hdSeedHex.empty())
{
// Restore from a previously exported HD seed hex (z_exportwallet's
// "# HDSeed=" line): 32 bytes (raw) or 64 bytes (BIP39-derived).
if (!pwalletMain->SetHDSeedFromHex(hdSeedHex))
return InitError(_("Invalid -hdseed: expected a 32- or 64-hex-character seed on a fresh/empty wallet"));
LogPrintf("%s: restoring wallet from -hdseed\n", __func__);
restoring = true;
}
else
{
// generate a new HD seed
pwalletMain->GenerateNewSeed();
}
if (restoring)
{
// Pre-derive keys (birthday = genesis) so the startup rescan finds
// funds paid to them: transparent coinbase + shielded notes.
int64_t tGap = GetArg("-hdtransparentgaplimit", 1000);
if (tGap < 0) tGap = 0;
pwalletMain->TopUpHDTransparentKeys((unsigned int)tGap, 1);
int64_t zGap = GetArg("-mnemonicsaplinggap", 100);
if (zGap < 0) zGap = 0;
{
LOCK(pwalletMain->cs_wallet);
for (int i = 0; i < (int)zGap; i++)
pwalletMain->GenerateNewSaplingZKey();
}
LogPrintf("%s: pre-derived %d transparent and %d sapling keys for restore rescan\n", __func__, (int)tGap, (int)zGap);
}
}
//Set Sapling Consolidation

View File

@@ -39,6 +39,9 @@
*/
typedef std::vector<unsigned char, secure_allocator<unsigned char> > CPrivKey;
/** BIP32: child indices at or above this are hardened. */
const unsigned int BIP32_HARDENED_KEY_LIMIT = 0x80000000;
/** An encapsulated private key. */
class CKey
{

View File

@@ -474,6 +474,7 @@ static const CRPCCommand vRPCCommands[] =
{ "wallet", "z_listaddresses", &z_listaddresses, true },
{ "wallet", "z_listnullifiers", &z_listnullifiers, 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 },

View File

@@ -353,6 +353,7 @@ extern UniValue nspv_listccmoduleunspent(const UniValue& params, bool fHelp, con
extern UniValue getblocksubsidy(const UniValue& params, bool fHelp, const CPubKey& mypk);
extern UniValue z_exportkey(const UniValue& params, bool fHelp, const CPubKey& mypk); // in rpcdump.cpp
extern UniValue z_exportmnemonic(const UniValue& params, bool fHelp, const CPubKey& mypk); // in rpcdump.cpp
extern UniValue z_importkey(const UniValue& params, bool fHelp, const CPubKey& mypk); // in rpcdump.cpp
extern UniValue z_exportviewingkey(const UniValue& params, bool fHelp, const CPubKey& mypk); // in rpcdump.cpp
extern UniValue z_importviewingkey(const UniValue& params, bool fHelp, const CPubKey& mypk); // in rpcdump.cpp

View File

@@ -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");

View File

@@ -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");

View File

@@ -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
View 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
View 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

View File

@@ -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))

View File

@@ -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);

View File

@@ -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 },

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();

View File

@@ -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; }

View File

@@ -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;
}
};