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:
87
doc/hd-transparent-keys.md
Normal file
87
doc/hd-transparent-keys.md
Normal 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
90
doc/seed-phrase.md
Normal 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.
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
171
src/gtest/test_hdtransparent.cpp
Normal file
171
src/gtest/test_hdtransparent.cpp
Normal 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
|
||||
}
|
||||
}
|
||||
141
src/gtest/test_mnemonic_compat.cpp
Normal file
141
src/gtest/test_mnemonic_compat.cpp
Normal 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);
|
||||
}
|
||||
56
src/init.cpp
56
src/init.cpp
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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