Compare commits
2 Commits
dev
...
hd-transpa
| Author | SHA1 | Date | |
|---|---|---|---|
| 4caf2fc68f | |||
| 84aefb5475 |
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
|
||||
|
||||
@@ -7,12 +7,13 @@ bin_PROGRAMS += hush-gtest
|
||||
# NOTE: the original test list used an invalid automake form (comment after a trailing
|
||||
# backslash, and `zcash_gtest_SOURCES +=` with no prior `=`), which is why the whole
|
||||
# gtest harness was disabled via a `#include`. Minimal valid set: the harness + the
|
||||
# UTXO-snapshot round-trip test. Re-add other gtest sources here as they are revived.
|
||||
# Re-add other gtest sources here as they are revived.
|
||||
hush_gtest_SOURCES = \
|
||||
gtest/main.cpp \
|
||||
gtest/utils.cpp \
|
||||
gtest/test_utxosnapshot.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)
|
||||
|
||||
@@ -69,17 +69,6 @@ public:
|
||||
double fTransactionsPerDay;
|
||||
};
|
||||
|
||||
/** Trusted UTXO-snapshot (assumeutxo-style) anchor. When `hash` is set, a node loading a
|
||||
* snapshot via -loadutxosnapshot must produce exactly this content hash at this height,
|
||||
* otherwise the snapshot is refused. Null hash = not configured (loading requires the
|
||||
* explicit -loadutxosnapshotunsafe override, e.g. for regtest/testing). Mirrors the
|
||||
* hardcoded-checkpoint trust model. */
|
||||
struct AssumeutxoData {
|
||||
int height;
|
||||
uint256 hash;
|
||||
bool IsNull() const { return hash.IsNull(); }
|
||||
};
|
||||
|
||||
enum Bech32Type {
|
||||
SAPLING_PAYMENT_ADDRESS,
|
||||
SAPLING_FULL_VIEWING_KEY,
|
||||
@@ -116,7 +105,6 @@ public:
|
||||
const std::string& Bech32HRP(Bech32Type type) const { return bech32HRPs[type]; }
|
||||
const std::vector<uint8_t>& FixedSeeds() const { return vFixedSeeds; }
|
||||
const CCheckpointData& Checkpoints() const { return checkpointData; }
|
||||
const AssumeutxoData& Assumeutxo() const { return assumeutxoData; }
|
||||
/** Return the founder's reward address and script for a given block height */
|
||||
std::string GetFoundersRewardAddressAtHeight(int height) const;
|
||||
CScript GetFoundersRewardScriptAtHeight(int height) const;
|
||||
@@ -156,7 +144,6 @@ protected:
|
||||
bool fMineBlocksOnDemand = false;
|
||||
bool fTestnetToBeDeprecatedFieldRPC = false;
|
||||
CCheckpointData checkpointData;
|
||||
AssumeutxoData assumeutxoData; // null by default; set per-network in chainparams.cpp once a snapshot hash is published
|
||||
std::vector<std::string> vFoundersRewardAddress;
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
// Copyright (c) 2024-2026 The DragonX developers
|
||||
// Distributed under the GPLv3 software license, see the accompanying
|
||||
// file COPYING or https://www.gnu.org/licenses/gpl-3.0.en.html
|
||||
//
|
||||
// Round-trip tests for the trusted UTXO snapshot (assumeutxo-style) dump/load core
|
||||
// (CCoinsViewDB::DumpSnapshot / LoadSnapshot). This exercises the highest-risk part of
|
||||
// the feature in isolation: that coins, Sapling commitment trees, the nullifier set, the
|
||||
// best block and the best Sapling anchor survive a serialize -> hash -> deserialize cycle
|
||||
// exactly, and that integrity/trust verification rejects tampered or wrong-hash snapshots.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include "chainparams.h"
|
||||
#include "coins.h"
|
||||
#include "txdb.h"
|
||||
#include "script/script.h"
|
||||
#include "uint256.h"
|
||||
#include "zcash/IncrementalMerkleTree.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
// Populate an in-memory chainstate DB directly via BatchWrite (mirrors how blocks persist
|
||||
// coins/anchors/nullifiers), so DumpSnapshot has a realistic mixed state to serialize.
|
||||
void PopulateChainstate(CCoinsViewDB &db, const uint256 &bestBlock,
|
||||
uint256 &anchorRootOut, const uint256 &nullifierIn)
|
||||
{
|
||||
// One unspent transparent output.
|
||||
CCoinsMap mapCoins;
|
||||
{
|
||||
uint256 txid = uint256S("0xaa00000000000000000000000000000000000000000000000000000000000001");
|
||||
CCoinsCacheEntry &e = mapCoins[txid];
|
||||
e.coins.fCoinBase = false;
|
||||
e.coins.nVersion = 1;
|
||||
e.coins.nHeight = 100;
|
||||
e.coins.vout.resize(1);
|
||||
e.coins.vout[0].nValue = 12345;
|
||||
e.coins.vout[0].scriptPubKey = CScript() << OP_TRUE;
|
||||
e.flags = CCoinsCacheEntry::DIRTY;
|
||||
}
|
||||
|
||||
// One Sapling commitment tree (anchor), keyed by its root.
|
||||
SaplingMerkleTree tree;
|
||||
tree.append(uint256S("0xbb00000000000000000000000000000000000000000000000000000000000002"));
|
||||
anchorRootOut = tree.root();
|
||||
CAnchorsSaplingMap mapSaplingAnchors;
|
||||
{
|
||||
CAnchorsSaplingCacheEntry &e = mapSaplingAnchors[anchorRootOut];
|
||||
e.entered = true;
|
||||
e.tree = tree;
|
||||
e.flags = CAnchorsSaplingCacheEntry::DIRTY;
|
||||
}
|
||||
|
||||
// One spent Sapling nullifier.
|
||||
CNullifiersMap mapSaplingNullifiers;
|
||||
{
|
||||
CNullifiersCacheEntry &e = mapSaplingNullifiers[nullifierIn];
|
||||
e.entered = true;
|
||||
e.flags = CNullifiersCacheEntry::DIRTY;
|
||||
}
|
||||
|
||||
CAnchorsSproutMap mapSproutAnchors; // empty
|
||||
CNullifiersMap mapSproutNullifiers; // empty
|
||||
ASSERT_TRUE(db.BatchWrite(mapCoins, bestBlock, uint256(), anchorRootOut,
|
||||
mapSproutAnchors, mapSaplingAnchors, mapSproutNullifiers, mapSaplingNullifiers));
|
||||
}
|
||||
|
||||
CUTXOSnapshotHeader MakeHeader(const uint256 &bestBlock, const uint256 &bestAnchor)
|
||||
{
|
||||
CUTXOSnapshotHeader h;
|
||||
h.nMagic = UTXO_SNAPSHOT_MAGIC;
|
||||
h.nVersion = UTXO_SNAPSHOT_VERSION;
|
||||
memcpy(&h.nNetworkMagic, Params().MessageStart(), 4);
|
||||
h.baseBlockHash = bestBlock;
|
||||
h.nHeight = 100;
|
||||
h.nChainTx = 1;
|
||||
h.fHasChainSaplingValue = 1;
|
||||
h.nChainSaplingValue = 999;
|
||||
h.bestSaplingAnchor = bestAnchor;
|
||||
return h;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UTXOSnapshot, RoundTripPreservesChainstate)
|
||||
{
|
||||
SelectParams(CBaseChainParams::REGTEST);
|
||||
|
||||
const uint256 bestBlock = uint256S("0xff00000000000000000000000000000000000000000000000000000000000009");
|
||||
const uint256 nullifier = uint256S("0xcc00000000000000000000000000000000000000000000000000000000000003");
|
||||
|
||||
CCoinsViewDB src(1 << 20, true); // in-memory
|
||||
uint256 anchorRoot;
|
||||
PopulateChainstate(src, bestBlock, anchorRoot, nullifier);
|
||||
|
||||
boost::filesystem::path path = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path();
|
||||
|
||||
CUTXOSnapshotHeader header = MakeHeader(bestBlock, anchorRoot);
|
||||
uint256 dumpHash; std::string err;
|
||||
ASSERT_TRUE(src.DumpSnapshot(path.string(), header, dumpHash, err)) << err;
|
||||
EXPECT_EQ(header.nCoins, 1u);
|
||||
EXPECT_EQ(header.nSaplingAnchors, 1u);
|
||||
EXPECT_EQ(header.nSaplingNullifiers, 1u);
|
||||
|
||||
// Load into a fresh in-memory DB (integrity check only, no trust hash).
|
||||
CCoinsViewDB dst(1 << 20, true);
|
||||
CUTXOSnapshotHeader loadedHeader; uint256 loadHash;
|
||||
ASSERT_TRUE(dst.LoadSnapshot(path.string(), uint256(), /*fRequireExpected=*/false, loadedHeader, loadHash, err)) << err;
|
||||
|
||||
// Hash is deterministic across dump and load.
|
||||
EXPECT_EQ(dumpHash, loadHash);
|
||||
EXPECT_EQ(loadedHeader.nHeight, 100);
|
||||
EXPECT_EQ(loadedHeader.baseBlockHash, bestBlock);
|
||||
|
||||
// Best block round-trips.
|
||||
EXPECT_EQ(dst.GetBestBlock(), bestBlock);
|
||||
|
||||
// Coins round-trip: the stored UTXO must come back intact. (We check the specific coin
|
||||
// directly rather than via GetStats(), which dereferences mapBlockIndex for the best block
|
||||
// — not populated in this pure unit test.) The full-content equivalence is already proven
|
||||
// by dumpHash == loadHash above.
|
||||
const uint256 txid = uint256S("0xaa00000000000000000000000000000000000000000000000000000000000001");
|
||||
CCoins c1, c2;
|
||||
ASSERT_TRUE(src.GetCoins(txid, c1));
|
||||
ASSERT_TRUE(dst.GetCoins(txid, c2));
|
||||
ASSERT_EQ(c2.vout.size(), 1u);
|
||||
EXPECT_EQ(c2.vout[0].nValue, c1.vout[0].nValue);
|
||||
EXPECT_TRUE(c2.vout[0].scriptPubKey == c1.vout[0].scriptPubKey);
|
||||
|
||||
// Sapling anchor (commitment tree) round-trips byte-exactly: the recovered tree's root
|
||||
// must equal the key it was stored under (this is the invariant ConnectBlock relies on).
|
||||
SaplingMerkleTree recovered;
|
||||
ASSERT_TRUE(dst.GetSaplingAnchorAt(anchorRoot, recovered));
|
||||
EXPECT_EQ(recovered.root(), anchorRoot);
|
||||
EXPECT_EQ(dst.GetBestAnchor(SAPLING), anchorRoot);
|
||||
|
||||
// Nullifier set round-trips.
|
||||
EXPECT_TRUE(dst.GetNullifier(nullifier, SAPLING));
|
||||
EXPECT_FALSE(dst.GetNullifier(uint256S("0xdead"), SAPLING));
|
||||
|
||||
boost::filesystem::remove(path);
|
||||
}
|
||||
|
||||
TEST(UTXOSnapshot, RejectsTrustHashMismatch)
|
||||
{
|
||||
SelectParams(CBaseChainParams::REGTEST);
|
||||
const uint256 bestBlock = uint256S("0xff0000000000000000000000000000000000000000000000000000000000000a");
|
||||
const uint256 nullifier = uint256S("0xcc0000000000000000000000000000000000000000000000000000000000000b");
|
||||
|
||||
CCoinsViewDB src(1 << 20, true);
|
||||
uint256 anchorRoot;
|
||||
PopulateChainstate(src, bestBlock, anchorRoot, nullifier);
|
||||
|
||||
boost::filesystem::path path = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path();
|
||||
CUTXOSnapshotHeader header = MakeHeader(bestBlock, anchorRoot);
|
||||
uint256 dumpHash; std::string err;
|
||||
ASSERT_TRUE(src.DumpSnapshot(path.string(), header, dumpHash, err)) << err;
|
||||
|
||||
// A wrong "trusted" hash must be refused.
|
||||
CCoinsViewDB dst(1 << 20, true);
|
||||
CUTXOSnapshotHeader h2; uint256 hh;
|
||||
uint256 wrong = uint256S("0x1234");
|
||||
EXPECT_FALSE(dst.LoadSnapshot(path.string(), wrong, /*fRequireExpected=*/true, h2, hh, err));
|
||||
// The correct hash must pass.
|
||||
EXPECT_TRUE(dst.LoadSnapshot(path.string(), dumpHash, /*fRequireExpected=*/true, h2, hh, err)) << err;
|
||||
|
||||
boost::filesystem::remove(path);
|
||||
}
|
||||
|
||||
TEST(UTXOSnapshot, RejectsCorruptedFile)
|
||||
{
|
||||
SelectParams(CBaseChainParams::REGTEST);
|
||||
const uint256 bestBlock = uint256S("0xff0000000000000000000000000000000000000000000000000000000000000c");
|
||||
const uint256 nullifier = uint256S("0xcc0000000000000000000000000000000000000000000000000000000000000d");
|
||||
|
||||
CCoinsViewDB src(1 << 20, true);
|
||||
uint256 anchorRoot;
|
||||
PopulateChainstate(src, bestBlock, anchorRoot, nullifier);
|
||||
|
||||
boost::filesystem::path path = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path();
|
||||
CUTXOSnapshotHeader header = MakeHeader(bestBlock, anchorRoot);
|
||||
uint256 dumpHash; std::string err;
|
||||
ASSERT_TRUE(src.DumpSnapshot(path.string(), header, dumpHash, err)) << err;
|
||||
|
||||
// Flip a byte near the end (inside the coins/anchor payload, before the trailing hash).
|
||||
{
|
||||
boost::filesystem::fstream f(path, std::ios::in | std::ios::out | std::ios::binary);
|
||||
f.seekg(0, std::ios::end);
|
||||
std::streamoff sz = f.tellg();
|
||||
ASSERT_GT(sz, 40);
|
||||
f.seekg(sz - 40);
|
||||
char c; f.read(&c, 1);
|
||||
f.seekp(sz - 40);
|
||||
c = (char)(c ^ 0xff);
|
||||
f.write(&c, 1);
|
||||
}
|
||||
|
||||
CCoinsViewDB dst(1 << 20, true);
|
||||
CUTXOSnapshotHeader h2; uint256 hh;
|
||||
EXPECT_FALSE(dst.LoadSnapshot(path.string(), uint256(), /*fRequireExpected=*/false, h2, hh, err));
|
||||
|
||||
boost::filesystem::remove(path);
|
||||
}
|
||||
96
src/init.cpp
96
src/init.cpp
@@ -390,8 +390,6 @@ std::string HelpMessage(HelpMessageMode mode)
|
||||
strUsage += HelpMessageOpt("-exportdir=<dir>", _("Specify directory to be used when exporting data"));
|
||||
strUsage += HelpMessageOpt("-dbcache=<n>", strprintf(_("Set database cache size in megabytes (%d to %d). Default: adaptive - uses most free RAM to speed up initial block download (far fewer UTXO flushes to disk) and automatically shrinks if other applications need memory, always leaving a reserve free. Setting a fixed value disables adaptive sizing."), nMinDbCache, nMaxDbCache));
|
||||
strUsage += HelpMessageOpt("-loadblock=<file>", _("Imports blocks from external blk000??.dat file") + " " + _("on startup"));
|
||||
strUsage += HelpMessageOpt("-loadutxosnapshot=<file>", _("On a fresh node (empty chainstate), load a trusted UTXO snapshot produced by 'dumptxoutset' and fast-forward the tip to its height, skipping replay of earlier blocks. Block headers up to that height must already be present (e.g. via header sync or bootstrap). Blocks above the snapshot are still fully validated."));
|
||||
strUsage += HelpMessageOpt("-loadutxosnapshotunsafe", _("Allow -loadutxosnapshot even when no trusted snapshot hash is hardcoded for this network (verifies file integrity only, not authenticity). Testing/regtest only."));
|
||||
strUsage += HelpMessageOpt("-maxdebugfilesize=<n>", strprintf(_("Set the max size of the debug.log file (default: %u)"), 15));
|
||||
strUsage += HelpMessageOpt("-maxorphantx=<n>", strprintf(_("Keep at most <n> unconnectable transactions in memory (default: %u)"), DEFAULT_MAX_ORPHAN_TRANSACTIONS));
|
||||
strUsage += HelpMessageOpt("-maxreorg=<n>", _("Specify the maximum length of a blockchain re-organization"));
|
||||
@@ -469,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)"));
|
||||
@@ -2099,44 +2103,6 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
|
||||
break;
|
||||
}
|
||||
|
||||
// Trusted UTXO snapshot fast-sync (assumeutxo-style). If -loadutxosnapshot is given
|
||||
// and the chainstate is still empty, load the verified snapshot and fast-forward the
|
||||
// tip to height H; blocks above H then sync with full PoW/script/Sapling validation.
|
||||
{
|
||||
std::string snapPath = GetArg("-loadutxosnapshot", "");
|
||||
if (!snapPath.empty()) {
|
||||
if (!pcoinsdbview->GetBestBlock().IsNull()) {
|
||||
LogPrintf("%s: -loadutxosnapshot ignored, chainstate is not empty\n", __func__);
|
||||
} else {
|
||||
const CChainParams::AssumeutxoData& au = chainparams.Assumeutxo();
|
||||
bool unsafe = GetBoolArg("-loadutxosnapshotunsafe", false);
|
||||
if (au.IsNull() && !unsafe) {
|
||||
strLoadError = _("-loadutxosnapshot: no trusted snapshot hash is configured for this network; refusing (use -loadutxosnapshotunsafe for testing only)");
|
||||
break;
|
||||
}
|
||||
CUTXOSnapshotHeader hdr; uint256 gotHash; std::string snapErr;
|
||||
bool requireExpected = !au.IsNull() && !unsafe;
|
||||
if (!pcoinsdbview->LoadSnapshot(snapPath, au.hash, requireExpected, hdr, gotHash, snapErr)) {
|
||||
strLoadError = strprintf(_("Failed to load UTXO snapshot: %s"), snapErr);
|
||||
break;
|
||||
}
|
||||
if (!au.IsNull() && hdr.nHeight != au.height) {
|
||||
strLoadError = _("UTXO snapshot height does not match the trusted value for this network");
|
||||
break;
|
||||
}
|
||||
pcoinsTip->SetBestBlock(hdr.baseBlockHash); // refresh cache view of the freshly-written chainstate
|
||||
std::string fixErr;
|
||||
if (!LoadSnapshotChainstate(hdr, fixErr)) {
|
||||
strLoadError = strprintf(_("Failed to activate UTXO snapshot tip: %s"), fixErr);
|
||||
break;
|
||||
}
|
||||
pblocktree->WriteAssumeutxoHeight(hdr.nHeight); // persist reorg-below-H guard across restarts
|
||||
LogPrintf("%s: loaded trusted UTXO snapshot at height %d (hash %s); syncing forward with full validation\n",
|
||||
__func__, hdr.nHeight, gotHash.GetHex());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HUSH_LOADINGBLOCKS = 0;
|
||||
// Check for changed -txindex state
|
||||
if (fTxIndex != GetBoolArg("-txindex", true)) {
|
||||
@@ -2306,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
|
||||
{
|
||||
|
||||
58
src/main.cpp
58
src/main.cpp
@@ -85,7 +85,6 @@ void hush_pricesupdate(int32_t height,CBlock *pblock);
|
||||
BlockMap mapBlockIndex;
|
||||
CChain chainActive;
|
||||
CBlockIndex *pindexBestHeader = NULL;
|
||||
int nAssumeutxoSnapshotHeight = -1; // height H of a loaded UTXO snapshot; reorgs below H are refused (-1 = none)
|
||||
static int64_t nTimeBestReceived = 0;
|
||||
CWaitableCriticalSection csBestBlock;
|
||||
CConditionVariable cvBlockChange;
|
||||
@@ -4201,45 +4200,6 @@ static void PruneBlockIndexCandidates() {
|
||||
assert(!setBlockIndexCandidates.empty());
|
||||
}
|
||||
|
||||
// Activate a trusted UTXO snapshot (assumeutxo-style) as the chain tip WITHOUT replaying blocks
|
||||
// 0..H. The chainstate has already been populated by CCoinsViewDB::LoadSnapshot(); here we mark the
|
||||
// snapshot's base block (height H) as fully validated and set it as the active tip. Blocks above H
|
||||
// then connect normally with full PoW + script + Sapling-proof validation. Requires that the block
|
||||
// HEADERS for height H are already present in mapBlockIndex (from prior header sync or bootstrap).
|
||||
// NOTE: below-H blocks have no body/undo data, so reorgs below H are impossible (see Stage D guard).
|
||||
bool LoadSnapshotChainstate(const CUTXOSnapshotHeader& header, std::string& strError)
|
||||
{
|
||||
LOCK(cs_main);
|
||||
BlockMap::iterator it = mapBlockIndex.find(header.baseBlockHash);
|
||||
if (it == mapBlockIndex.end() || it->second == NULL) {
|
||||
strError = "block header for the snapshot height is not present; sync headers (or use the bootstrap) before loading a UTXO snapshot";
|
||||
return false;
|
||||
}
|
||||
CBlockIndex* pindexH = it->second;
|
||||
if (pindexH->GetHeight() != header.nHeight) {
|
||||
strError = "snapshot base block height does not match its header index";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only nChainTx is consensus-relevant for tip selection; nTx must merely be non-zero so the
|
||||
// (nChainTx != 0) candidate-eligibility checks hold. Ancestors legitimately have nTx==0 here
|
||||
// because we never received their bodies — this is the assumeutxo trust assumption.
|
||||
if (pindexH->nTx == 0)
|
||||
pindexH->nTx = (header.nChainTx > 0 ? (unsigned int)header.nChainTx : 1);
|
||||
pindexH->nChainTx = (unsigned int)header.nChainTx;
|
||||
if (header.fHasChainSaplingValue)
|
||||
pindexH->nChainSaplingValue = header.nChainSaplingValue;
|
||||
|
||||
pindexH->RaiseValidity(BLOCK_VALID_SCRIPTS);
|
||||
nAssumeutxoSnapshotHeight = pindexH->GetHeight(); // arm the reorg-below-H guard (Stage D)
|
||||
setBlockIndexCandidates.insert(pindexH);
|
||||
chainActive.SetTip(pindexH);
|
||||
if (pindexBestHeader == NULL || pindexBestHeader->GetHeight() < pindexH->GetHeight())
|
||||
pindexBestHeader = pindexH;
|
||||
PruneBlockIndexCandidates();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to make some progress towards making pindexMostWork the active block.
|
||||
* pblock is either NULL or a pointer to a CBlock corresponding to pindexMostWork.
|
||||
@@ -4288,15 +4248,6 @@ static bool ActivateBestChainStep(bool fSkipdpow, CValidationState &state, CBloc
|
||||
return state.DoS(100, error("ActivateBestChainStep(): pindexOldTip->GetHeight().%d > notarizedht %d && pindexFork->GetHeight().%d is < notarizedht %d, so ignore it",(int32_t)pindexOldTip->GetHeight(),notarizedht,(int32_t)pindexFork->GetHeight(),notarizedht),
|
||||
REJECT_INVALID, "past-notarized-height");
|
||||
}
|
||||
// Refuse reorgs whose fork point is below a loaded UTXO snapshot height (Stage D): the node has
|
||||
// no block/undo data for 0..H, so disconnecting below H is impossible. Belt-and-suspenders on top
|
||||
// of checkpoint fork-rejection (H sits at/below the last hardcoded checkpoint).
|
||||
if ( nAssumeutxoSnapshotHeight >= 0 && pindexFork != 0 && pindexFork->GetHeight() < nAssumeutxoSnapshotHeight )
|
||||
{
|
||||
return state.DoS(100, error("ActivateBestChainStep(): reorg fork height %d is below the loaded UTXO snapshot height %d; refusing",
|
||||
(int32_t)pindexFork->GetHeight(), nAssumeutxoSnapshotHeight),
|
||||
REJECT_INVALID, "below-assumeutxo-snapshot");
|
||||
}
|
||||
|
||||
// - On ChainDB initialization, pindexOldTip will be null, so there are no removable blocks.
|
||||
// - If pindexMostWork is in a chain that doesn't have the same genesis block as our chain,
|
||||
@@ -6101,15 +6052,6 @@ bool static LoadBlockIndexDB()
|
||||
pblocktree->ReadReindexing(fReindexing);
|
||||
fReindex |= fReindexing;
|
||||
|
||||
// Restore the loaded-UTXO-snapshot height so the reorg-below-H guard survives restarts.
|
||||
{
|
||||
int snapHeight = -1;
|
||||
if (pblocktree->ReadAssumeutxoHeight(snapHeight) && snapHeight >= 0) {
|
||||
nAssumeutxoSnapshotHeight = snapHeight;
|
||||
LogPrintf("%s: loaded-from-UTXO-snapshot height is %d; reorgs below it are refused\n", __func__, snapHeight);
|
||||
}
|
||||
}
|
||||
|
||||
// Check whether we have a transaction index
|
||||
pblocktree->ReadFlag("txindex", fTxIndex);
|
||||
LogPrintf("%s: transaction index %s\n", __func__, fTxIndex ? "enabled" : "disabled");
|
||||
|
||||
11
src/main.h
11
src/main.h
@@ -956,19 +956,10 @@ extern CChain chainActive;
|
||||
/** Global variable that points to the active CCoinsView (protected by cs_main) */
|
||||
extern CCoinsViewCache *pcoinsTip;
|
||||
|
||||
/** Global variable that points to the coins database (chainstate/, protected by cs_main).
|
||||
* Exposed for the UTXO-snapshot (assumeutxo-style) dump/load paths. */
|
||||
/** Global variable that points to the coins database (chainstate/, protected by cs_main). */
|
||||
class CCoinsViewDB;
|
||||
extern CCoinsViewDB *pcoinsdbview;
|
||||
|
||||
/** Activate a trusted UTXO snapshot (already written to the chainstate DB by LoadSnapshot) as the
|
||||
* chain tip at its height H, without replaying blocks 0..H. Headers for H must already exist. */
|
||||
struct CUTXOSnapshotHeader;
|
||||
bool LoadSnapshotChainstate(const CUTXOSnapshotHeader& header, std::string& strError);
|
||||
/** Height H of a loaded UTXO snapshot (assumeutxo). Reorgs whose fork point is below H are refused
|
||||
* because the node has no block/undo data for 0..H. -1 means no snapshot is in effect. */
|
||||
extern int nAssumeutxoSnapshotHeight;
|
||||
|
||||
/** Global variable that points to the active block tree (protected by cs_main) */
|
||||
extern CBlockTreeDB *pblocktree;
|
||||
|
||||
|
||||
@@ -862,76 +862,6 @@ UniValue gettxoutsetinfo(const UniValue& params, bool fHelp, const CPubKey& mypk
|
||||
return ret;
|
||||
}
|
||||
|
||||
UniValue dumptxoutset(const UniValue& params, bool fHelp, const CPubKey& mypk)
|
||||
{
|
||||
if (fHelp || params.size() != 1)
|
||||
throw runtime_error(
|
||||
"dumptxoutset \"path\"\n"
|
||||
"\nWrite a trusted snapshot of the current chainstate (UTXO set + Sapling commitment\n"
|
||||
"trees, nullifier set and pool value) to disk. The snapshot can be loaded by a fresh\n"
|
||||
"node with -loadutxosnapshot=<file> to skip replaying the chain from genesis.\n"
|
||||
"\nThis is intended to be run at a final/checkpoint height; the node must be fully synced.\n"
|
||||
"\nArguments:\n"
|
||||
"1. \"path\" (string, required) path to write the snapshot file (must not already exist)\n"
|
||||
"\nResult:\n"
|
||||
"{\n"
|
||||
" \"height\": n, (numeric) snapshot height H\n"
|
||||
" \"base_hash\": \"hex\", (string) block hash at height H\n"
|
||||
" \"snapshot_hash\": \"hex\", (string) content hash to hardcode for verification\n"
|
||||
" \"coins\": n, (numeric) number of UTXO records\n"
|
||||
" \"sapling_anchors\": n, (numeric) number of Sapling anchor records\n"
|
||||
" \"sapling_nullifiers\": n, (numeric) number of Sapling nullifier records\n"
|
||||
" \"path\": \"...\" (string) the file written\n"
|
||||
"}\n"
|
||||
"\nExamples:\n"
|
||||
+ HelpExampleCli("dumptxoutset", "/path/to/dragonx-utxo.dat")
|
||||
+ HelpExampleRpc("dumptxoutset", "\"/path/to/dragonx-utxo.dat\"")
|
||||
);
|
||||
|
||||
boost::filesystem::path path = boost::filesystem::absolute(params[0].get_str());
|
||||
if (boost::filesystem::exists(path))
|
||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "path already exists, refusing to overwrite: " + path.string());
|
||||
|
||||
LOCK(cs_main);
|
||||
|
||||
if (pcoinsdbview == nullptr || pcoinsTip == nullptr)
|
||||
throw JSONRPCError(RPC_INTERNAL_ERROR, "chainstate not available");
|
||||
|
||||
// Flush so the on-disk chainstate matches the in-memory tip before we iterate it.
|
||||
FlushStateToDisk();
|
||||
|
||||
CBlockIndex *tip = chainActive.Tip();
|
||||
if (tip == nullptr)
|
||||
throw JSONRPCError(RPC_INTERNAL_ERROR, "no chain tip");
|
||||
|
||||
CUTXOSnapshotHeader header;
|
||||
header.nMagic = UTXO_SNAPSHOT_MAGIC;
|
||||
header.nVersion = UTXO_SNAPSHOT_VERSION;
|
||||
memcpy(&header.nNetworkMagic, Params().MessageStart(), 4);
|
||||
header.baseBlockHash = tip->GetBlockHash();
|
||||
header.nHeight = tip->GetHeight();
|
||||
header.nChainTx = tip->nChainTx;
|
||||
if (tip->nChainSaplingValue) {
|
||||
header.fHasChainSaplingValue = 1;
|
||||
header.nChainSaplingValue = *tip->nChainSaplingValue;
|
||||
}
|
||||
header.bestSaplingAnchor = pcoinsdbview->GetBestAnchor(SAPLING);
|
||||
|
||||
uint256 snapshotHash;
|
||||
std::string strError;
|
||||
if (!pcoinsdbview->DumpSnapshot(path.string(), header, snapshotHash, strError))
|
||||
throw JSONRPCError(RPC_INTERNAL_ERROR, "dumptxoutset failed: " + strError);
|
||||
|
||||
UniValue ret(UniValue::VOBJ);
|
||||
ret.push_back(Pair("height", (int64_t)header.nHeight));
|
||||
ret.push_back(Pair("base_hash", header.baseBlockHash.GetHex()));
|
||||
ret.push_back(Pair("snapshot_hash", snapshotHash.GetHex()));
|
||||
ret.push_back(Pair("coins", (int64_t)header.nCoins));
|
||||
ret.push_back(Pair("sapling_anchors", (int64_t)header.nSaplingAnchors));
|
||||
ret.push_back(Pair("sapling_nullifiers", (int64_t)header.nSaplingNullifiers));
|
||||
ret.push_back(Pair("path", path.string()));
|
||||
return ret;
|
||||
}
|
||||
|
||||
UniValue getblockmerkletree(const UniValue& params, bool fHelp, const CPubKey& mypk)
|
||||
{
|
||||
@@ -1924,7 +1854,6 @@ static const CRPCCommand commands[] =
|
||||
{ "blockchain", "getrawmempool", &getrawmempool, true },
|
||||
{ "blockchain", "gettxout", &gettxout, true },
|
||||
{ "blockchain", "gettxoutsetinfo", &gettxoutsetinfo, true },
|
||||
{ "blockchain", "dumptxoutset", &dumptxoutset, true },
|
||||
{ "blockchain", "verifychain", &verifychain, true },
|
||||
|
||||
/* Not shown in help */
|
||||
|
||||
@@ -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
|
||||
|
||||
235
src/txdb.cpp
235
src/txdb.cpp
@@ -271,233 +271,6 @@ bool CCoinsViewDB::GetStats(CCoinsStats &stats) const {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Helper: count entries in the coins DB whose key prefix matches `prefix`.
|
||||
// LevelDB returns keys in sorted order, so iteration is deterministic across nodes.
|
||||
static uint64_t CountByPrefix(CDBWrapper &db, char prefix)
|
||||
{
|
||||
boost::scoped_ptr<CDBIterator> pcursor(db.NewIterator());
|
||||
uint64_t n = 0;
|
||||
for (pcursor->Seek(prefix); pcursor->Valid(); pcursor->Next()) {
|
||||
boost::this_thread::interruption_point();
|
||||
std::pair<char, uint256> key;
|
||||
if (pcursor->GetKey(key) && key.first == prefix) n++;
|
||||
else break;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
bool CCoinsViewDB::DumpSnapshot(const std::string &path, CUTXOSnapshotHeader &header, uint256 &hashRet, std::string &strError) const
|
||||
{
|
||||
CDBWrapper *pdb = const_cast<CDBWrapper*>(&db);
|
||||
|
||||
// Counting pass (caller holds cs_main and has flushed, so the set is stable).
|
||||
header.nCoins = CountByPrefix(*pdb, DB_COINS);
|
||||
header.nSaplingAnchors = CountByPrefix(*pdb, DB_SAPLING_ANCHOR);
|
||||
header.nSaplingNullifiers = CountByPrefix(*pdb, DB_SAPLING_NULLIFIER);
|
||||
|
||||
FILE *f = fopen(path.c_str(), "wb");
|
||||
if (f == nullptr) { strError = "cannot open snapshot file for writing: " + path; return false; }
|
||||
CAutoFile fileout(f, SER_DISK, CLIENT_VERSION);
|
||||
|
||||
// The content hash is computed over the same logical object stream the loader will
|
||||
// reconstruct, so producer and consumer agree regardless of on-disk encoding.
|
||||
CHashWriter hasher(SER_GETHASH, PROTOCOL_VERSION);
|
||||
|
||||
fileout << header;
|
||||
hasher << header;
|
||||
|
||||
// Coins ('c')
|
||||
{
|
||||
boost::scoped_ptr<CDBIterator> pcursor(pdb->NewIterator());
|
||||
uint64_t n = 0;
|
||||
for (pcursor->Seek(DB_COINS); pcursor->Valid(); pcursor->Next()) {
|
||||
boost::this_thread::interruption_point();
|
||||
std::pair<char, uint256> key;
|
||||
CCoins coins;
|
||||
if (pcursor->GetKey(key) && key.first == DB_COINS) {
|
||||
if (!pcursor->GetValue(coins)) { strError = "failed reading coins record"; return false; }
|
||||
fileout << key.second; hasher << key.second;
|
||||
fileout << coins; hasher << coins;
|
||||
n++;
|
||||
} else break;
|
||||
}
|
||||
if (n != header.nCoins) { strError = "coin count changed during dump"; return false; }
|
||||
}
|
||||
|
||||
// Sapling anchors ('Z') — the commitment trees referenced by spends above H.
|
||||
{
|
||||
boost::scoped_ptr<CDBIterator> pcursor(pdb->NewIterator());
|
||||
uint64_t n = 0;
|
||||
for (pcursor->Seek(DB_SAPLING_ANCHOR); pcursor->Valid(); pcursor->Next()) {
|
||||
boost::this_thread::interruption_point();
|
||||
std::pair<char, uint256> key;
|
||||
SaplingMerkleTree tree;
|
||||
if (pcursor->GetKey(key) && key.first == DB_SAPLING_ANCHOR) {
|
||||
if (!pcursor->GetValue(tree)) { strError = "failed reading sapling anchor"; return false; }
|
||||
fileout << key.second; hasher << key.second;
|
||||
fileout << tree; hasher << tree;
|
||||
n++;
|
||||
} else break;
|
||||
}
|
||||
if (n != header.nSaplingAnchors) { strError = "sapling anchor count changed during dump"; return false; }
|
||||
}
|
||||
|
||||
// Sapling nullifiers ('S') — spent markers; value is always true, so only the key matters.
|
||||
{
|
||||
boost::scoped_ptr<CDBIterator> pcursor(pdb->NewIterator());
|
||||
uint64_t n = 0;
|
||||
for (pcursor->Seek(DB_SAPLING_NULLIFIER); pcursor->Valid(); pcursor->Next()) {
|
||||
boost::this_thread::interruption_point();
|
||||
std::pair<char, uint256> key;
|
||||
if (pcursor->GetKey(key) && key.first == DB_SAPLING_NULLIFIER) {
|
||||
fileout << key.second; hasher << key.second;
|
||||
n++;
|
||||
} else break;
|
||||
}
|
||||
if (n != header.nSaplingNullifiers) { strError = "sapling nullifier count changed during dump"; return false; }
|
||||
}
|
||||
|
||||
hashRet = hasher.GetHash();
|
||||
fileout << hashRet; // trailing content hash (not fed into the hasher)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CCoinsViewDB::LoadSnapshot(const std::string &path, const uint256 &expectedHash, bool fRequireExpected,
|
||||
CUTXOSnapshotHeader &headerRet, uint256 &hashRet, std::string &strError)
|
||||
{
|
||||
uint32_t netmagic = 0;
|
||||
memcpy(&netmagic, Params().MessageStart(), 4);
|
||||
|
||||
// ---- Pass 1: read + verify integrity (and the trusted hash) WITHOUT writing to the DB ----
|
||||
CUTXOSnapshotHeader header;
|
||||
uint256 computed;
|
||||
{
|
||||
FILE *f = fopen(path.c_str(), "rb");
|
||||
if (f == nullptr) { strError = "cannot open snapshot file: " + path; return false; }
|
||||
CAutoFile filein(f, SER_DISK, CLIENT_VERSION);
|
||||
CHashWriter hasher(SER_GETHASH, PROTOCOL_VERSION);
|
||||
try {
|
||||
filein >> header; hasher << header;
|
||||
if (header.nMagic != UTXO_SNAPSHOT_MAGIC) { strError = "not a DragonX UTXO snapshot (bad magic)"; return false; }
|
||||
if (header.nVersion != UTXO_SNAPSHOT_VERSION) { strError = "unsupported snapshot version"; return false; }
|
||||
if (header.nNetworkMagic != netmagic) { strError = "snapshot is for a different network"; return false; }
|
||||
|
||||
for (uint64_t i = 0; i < header.nCoins; i++) {
|
||||
boost::this_thread::interruption_point();
|
||||
uint256 txid; CCoins coins;
|
||||
filein >> txid; filein >> coins;
|
||||
hasher << txid; hasher << coins;
|
||||
}
|
||||
for (uint64_t i = 0; i < header.nSaplingAnchors; i++) {
|
||||
boost::this_thread::interruption_point();
|
||||
uint256 root; SaplingMerkleTree tree;
|
||||
filein >> root; filein >> tree;
|
||||
hasher << root; hasher << tree;
|
||||
}
|
||||
for (uint64_t i = 0; i < header.nSaplingNullifiers; i++) {
|
||||
boost::this_thread::interruption_point();
|
||||
uint256 nf;
|
||||
filein >> nf;
|
||||
hasher << nf;
|
||||
}
|
||||
uint256 stored;
|
||||
filein >> stored;
|
||||
computed = hasher.GetHash();
|
||||
if (computed != stored) { strError = "snapshot content hash mismatch (corrupt or truncated)"; return false; }
|
||||
} catch (const std::exception &e) {
|
||||
strError = std::string("error reading snapshot: ") + e.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (fRequireExpected && computed != expectedHash) {
|
||||
strError = "snapshot hash does not match the trusted value hardcoded for this network";
|
||||
return false;
|
||||
}
|
||||
hashRet = computed;
|
||||
headerRet = header;
|
||||
|
||||
// ---- Pass 2: apply to the (empty) chainstate DB in bounded batches ----
|
||||
const size_t CHUNK = 100000;
|
||||
CCoinsMap mapCoins;
|
||||
CAnchorsSproutMap mapSproutAnchors; // unused on this chain, always empty
|
||||
CAnchorsSaplingMap mapSaplingAnchors;
|
||||
CNullifiersMap mapSproutNullifiers; // unused, always empty
|
||||
CNullifiersMap mapSaplingNullifiers;
|
||||
{
|
||||
FILE *f = fopen(path.c_str(), "rb");
|
||||
if (f == nullptr) { strError = "cannot reopen snapshot file: " + path; return false; }
|
||||
CAutoFile filein(f, SER_DISK, CLIENT_VERSION);
|
||||
try {
|
||||
CUTXOSnapshotHeader hdr2;
|
||||
filein >> hdr2; // header already validated in pass 1
|
||||
|
||||
for (uint64_t i = 0; i < header.nCoins; i++) {
|
||||
boost::this_thread::interruption_point();
|
||||
uint256 txid; CCoins coins;
|
||||
filein >> txid; filein >> coins;
|
||||
CCoinsCacheEntry &e = mapCoins[txid];
|
||||
e.coins = coins;
|
||||
e.flags = CCoinsCacheEntry::DIRTY;
|
||||
if (mapCoins.size() >= CHUNK) {
|
||||
if (!BatchWrite(mapCoins, uint256(), uint256(), uint256(), mapSproutAnchors, mapSaplingAnchors, mapSproutNullifiers, mapSaplingNullifiers))
|
||||
{ strError = "batch write failed (coins)"; return false; }
|
||||
mapCoins.clear();
|
||||
}
|
||||
}
|
||||
if (!mapCoins.empty()) {
|
||||
if (!BatchWrite(mapCoins, uint256(), uint256(), uint256(), mapSproutAnchors, mapSaplingAnchors, mapSproutNullifiers, mapSaplingNullifiers))
|
||||
{ strError = "batch write failed (coins remainder)"; return false; }
|
||||
mapCoins.clear();
|
||||
}
|
||||
|
||||
for (uint64_t i = 0; i < header.nSaplingAnchors; i++) {
|
||||
boost::this_thread::interruption_point();
|
||||
uint256 root; SaplingMerkleTree tree;
|
||||
filein >> root; filein >> tree;
|
||||
CAnchorsSaplingCacheEntry &e = mapSaplingAnchors[root];
|
||||
e.entered = true;
|
||||
e.tree = tree;
|
||||
e.flags = CAnchorsSaplingCacheEntry::DIRTY;
|
||||
if (mapSaplingAnchors.size() >= CHUNK) {
|
||||
if (!BatchWrite(mapCoins, uint256(), uint256(), uint256(), mapSproutAnchors, mapSaplingAnchors, mapSproutNullifiers, mapSaplingNullifiers))
|
||||
{ strError = "batch write failed (anchors)"; return false; }
|
||||
mapSaplingAnchors.clear();
|
||||
}
|
||||
}
|
||||
if (!mapSaplingAnchors.empty()) {
|
||||
if (!BatchWrite(mapCoins, uint256(), uint256(), uint256(), mapSproutAnchors, mapSaplingAnchors, mapSproutNullifiers, mapSaplingNullifiers))
|
||||
{ strError = "batch write failed (anchors remainder)"; return false; }
|
||||
mapSaplingAnchors.clear();
|
||||
}
|
||||
|
||||
for (uint64_t i = 0; i < header.nSaplingNullifiers; i++) {
|
||||
boost::this_thread::interruption_point();
|
||||
uint256 nf;
|
||||
filein >> nf;
|
||||
CNullifiersCacheEntry &e = mapSaplingNullifiers[nf];
|
||||
e.entered = true;
|
||||
e.flags = CNullifiersCacheEntry::DIRTY;
|
||||
if (mapSaplingNullifiers.size() >= CHUNK) {
|
||||
if (!BatchWrite(mapCoins, uint256(), uint256(), uint256(), mapSproutAnchors, mapSaplingAnchors, mapSproutNullifiers, mapSaplingNullifiers))
|
||||
{ strError = "batch write failed (nullifiers)"; return false; }
|
||||
mapSaplingNullifiers.clear();
|
||||
}
|
||||
}
|
||||
} catch (const std::exception &e) {
|
||||
strError = std::string("error applying snapshot: ") + e.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Final write: flush any remaining nullifiers AND set the best-block / best-sapling-anchor
|
||||
// pointers, so GetBestBlock()==H and GetBestAnchor(SAPLING) resolve after load.
|
||||
if (!BatchWrite(mapCoins, header.baseBlockHash, uint256(), header.bestSaplingAnchor,
|
||||
mapSproutAnchors, mapSaplingAnchors, mapSproutNullifiers, mapSaplingNullifiers))
|
||||
{ strError = "final batch write failed"; return false; }
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CBlockTreeDB::WriteBatchSync(const std::vector<std::pair<int, const CBlockFileInfo*> >& fileInfo, int nLastFile, const std::vector<CBlockIndex*>& blockinfo) {
|
||||
CDBBatch batch(*this);
|
||||
if (fDebug)
|
||||
@@ -884,14 +657,6 @@ bool CBlockTreeDB::ReadFlag(const std::string &name, bool &fValue) const {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CBlockTreeDB::WriteAssumeutxoHeight(int nHeight) {
|
||||
return Write(std::make_pair(DB_FLAG, std::string("assumeutxoheight")), nHeight);
|
||||
}
|
||||
|
||||
bool CBlockTreeDB::ReadAssumeutxoHeight(int &nHeight) const {
|
||||
return Read(std::make_pair(DB_FLAG, std::string("assumeutxoheight")), nHeight);
|
||||
}
|
||||
|
||||
void hush_index2pubkey33(uint8_t *pubkey33,CBlockIndex *pindex,int32_t height);
|
||||
|
||||
bool CBlockTreeDB::blockOnchainActive(const uint256 &hash) {
|
||||
|
||||
71
src/txdb.h
71
src/txdb.h
@@ -56,61 +56,6 @@ static const int64_t nMaxDbCache = sizeof(void*) > 4 ? 16384 : 1024;
|
||||
//! min. -dbcache in (MiB)
|
||||
static const int64_t nMinDbCache = 4;
|
||||
|
||||
/** Magic + version for the trusted UTXO-snapshot (assumeutxo-style) file format. */
|
||||
static const uint32_t UTXO_SNAPSHOT_MAGIC = 0x58535844; // 'DXSX'
|
||||
static const uint8_t UTXO_SNAPSHOT_VERSION = 1;
|
||||
|
||||
/**
|
||||
* Header of a trusted chainstate snapshot taken at a final height H. On this private
|
||||
* chain the chainstate is more than transparent UTXOs, so the snapshot also carries the
|
||||
* Sapling commitment trees, the nullifier set, the best Sapling anchor and the pool value.
|
||||
*
|
||||
* File layout: [CUTXOSnapshotHeader]
|
||||
* nCoins × (uint256 txid, CCoins)
|
||||
* nSaplingAnchors × (uint256 root, SaplingMerkleTree)
|
||||
* nSaplingNullifiers × (uint256 nullifier)
|
||||
* uint256 contentHash // hash over everything above (NOT itself)
|
||||
*/
|
||||
struct CUTXOSnapshotHeader
|
||||
{
|
||||
uint32_t nMagic;
|
||||
uint8_t nVersion;
|
||||
uint32_t nNetworkMagic; // Params().MessageStart() as uint32 — prevents cross-network use
|
||||
uint256 baseBlockHash; // hash of block H (the snapshot tip)
|
||||
int32_t nHeight; // H
|
||||
uint64_t nChainTx; // cumulative tx count at H (needed for tip fix-up)
|
||||
uint8_t fHasChainSaplingValue;
|
||||
int64_t nChainSaplingValue; // cumulative Sapling pool value at H (valid iff fHasChainSaplingValue)
|
||||
uint256 bestSaplingAnchor; // best Sapling anchor root at H
|
||||
uint64_t nCoins;
|
||||
uint64_t nSaplingAnchors;
|
||||
uint64_t nSaplingNullifiers;
|
||||
|
||||
CUTXOSnapshotHeader() { SetNull(); }
|
||||
void SetNull() {
|
||||
nMagic = 0; nVersion = 0; nNetworkMagic = 0; baseBlockHash.SetNull();
|
||||
nHeight = 0; nChainTx = 0; fHasChainSaplingValue = 0; nChainSaplingValue = 0;
|
||||
bestSaplingAnchor.SetNull(); nCoins = 0; nSaplingAnchors = 0; nSaplingNullifiers = 0;
|
||||
}
|
||||
|
||||
ADD_SERIALIZE_METHODS;
|
||||
template <typename Stream, typename Operation>
|
||||
inline void SerializationOp(Stream& s, Operation ser_action) {
|
||||
READWRITE(nMagic);
|
||||
READWRITE(nVersion);
|
||||
READWRITE(nNetworkMagic);
|
||||
READWRITE(baseBlockHash);
|
||||
READWRITE(nHeight);
|
||||
READWRITE(nChainTx);
|
||||
READWRITE(fHasChainSaplingValue);
|
||||
READWRITE(nChainSaplingValue);
|
||||
READWRITE(bestSaplingAnchor);
|
||||
READWRITE(nCoins);
|
||||
READWRITE(nSaplingAnchors);
|
||||
READWRITE(nSaplingNullifiers);
|
||||
}
|
||||
};
|
||||
|
||||
/** CCoinsView backed by the coin database (chainstate/) */
|
||||
class CCoinsViewDB : public CCoinsView
|
||||
{
|
||||
@@ -136,19 +81,6 @@ public:
|
||||
CNullifiersMap &mapSproutNullifiers,
|
||||
CNullifiersMap &mapSaplingNullifiers);
|
||||
bool GetStats(CCoinsStats &stats) const;
|
||||
|
||||
//! Stream the full chainstate at the current tip into a snapshot file (assumeutxo-style
|
||||
//! producer). Caller fills the metadata fields of `header` (height, baseBlockHash, nChainTx,
|
||||
//! pool value, bestSaplingAnchor); this fills the counts, writes the file, and returns the
|
||||
//! content hash. Caller must hold cs_main and have flushed the cache to disk first.
|
||||
bool DumpSnapshot(const std::string &path, CUTXOSnapshotHeader &header, uint256 &hashRet, std::string &strError) const;
|
||||
|
||||
//! Load a snapshot file produced by DumpSnapshot into the (empty) chainstate DB. Two passes:
|
||||
//! pass 1 reads everything and verifies the internal content hash (and, if fRequireExpected,
|
||||
//! that it equals expectedHash) WITHOUT touching the DB; pass 2 writes coins/anchors/nullifiers
|
||||
//! plus the best-block / best-sapling-anchor pointers. Returns the header + computed hash.
|
||||
bool LoadSnapshot(const std::string &path, const uint256 &expectedHash, bool fRequireExpected,
|
||||
CUTXOSnapshotHeader &headerRet, uint256 &hashRet, std::string &strError);
|
||||
};
|
||||
|
||||
/** Access to the block database (blocks/index/) */
|
||||
@@ -185,9 +117,6 @@ public:
|
||||
bool ReadTimestampBlockIndex(const uint256 &hash, unsigned int &logicalTS) const;
|
||||
bool WriteFlag(const std::string &name, bool fValue);
|
||||
bool ReadFlag(const std::string &name, bool &fValue) const;
|
||||
//! Persist/restore the height of a loaded UTXO snapshot so the reorg-below-H guard survives restarts.
|
||||
bool WriteAssumeutxoHeight(int nHeight);
|
||||
bool ReadAssumeutxoHeight(int &nHeight) const;
|
||||
bool LoadBlockIndexGuts();
|
||||
bool blockOnchainActive(const uint256 &hash);
|
||||
UniValue Snapshot(int top);
|
||||
|
||||
@@ -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