Merge autoshield-safety into dev: auto-shield coinbase + BIP39 by default

Brings the auto-shield-coinbase feature and the wallet seed work onto the release
line so both are exercised from dev rather than a side branch.

From upstream: auto-shield of matured coinbase into a wallet z-address, plus
fixes to two pre-existing scheduler wedges (consolidation dispatching every block
instead of once per interval, and a failing sweep stranding its running flag).

On top of that:
  * the auto-shield destination is chosen by re-deriving m/32'/coin'/i' from the
    seed and taking the lowest in-gap account the wallet holds a key for, rather
    than the first entry in std::set order. Key metadata is NOT evidence of
    provenance -- z_importwallet copies hdKeypath and seedFp verbatim from the
    import file -- so a crafted import could otherwise claim account 0 and
    capture every shielded reward;
  * HD seed/chain persistence is hardened: the chain record is written before the
    seed, a corrupt hdchain is loud rather than silently reverting derivation to
    the raw entropy, hdchain survives -salvagewallet, and the silent BIP39 ->
    random seed fallback is gone;
  * seed provenance is recorded, and -autoshield defaults ON only where that
    provenance says the seed is recoverable;
  * mnemonic wallets now store the EXPANDED 64-byte BIP39 seed with the 32-byte
    entropy in a separate display-only record. Derivation reads stored bytes
    directly on every binary, so key trees are identical and no CHDChain version
    bump or minversion fence is needed -- the format stays readable by earlier
    releases;
  * new wallets are created from a BIP39 phrase by default;
  * the plaintext hdseed record is erased when a wallet is encrypted. It was
    previously left behind, and CDB::Rewrite copies surviving records verbatim,
    so the unencrypted seed persisted on disk forever.

TWO DEFAULTS CHANGE for new wallets: auto-shielding (where the seed provenance is
known) and BIP39 seed generation. Existing wallets are untouched -- seed
generation is reachable only when a wallet has none, and all three key stores
refuse to replace an existing seed.

Testing state, stated plainly: every commit built clean and the branch was
verified on an isolated chain -- destination selection provably ignores an
imported foreign key, both provenance branches behave, a new-format wallet
reopened by a pre-change binary lists identical addresses, and restoring only the
24 words recovers the wallet. NOT yet tested: any of this against a funded wallet
on mainnet, or a soak of these defaults on a real node.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-23 11:52:42 -05:00
17 changed files with 1444 additions and 47 deletions

View File

@@ -243,6 +243,7 @@ BITCOIN_CORE_H = \
wallet/asyncrpcoperation_mergetoaddress.h \
wallet/asyncrpcoperation_saplingconsolidation.h \
wallet/asyncrpcoperation_sweep.h \
wallet/asyncrpcoperation_autoshieldcoinbase.h \
wallet/asyncrpcoperation_sendmany.h \
wallet/asyncrpcoperation_shieldcoinbase.h \
wallet/crypter.h \
@@ -321,6 +322,7 @@ libbitcoin_wallet_a_SOURCES = \
wallet/asyncrpcoperation_mergetoaddress.cpp \
wallet/asyncrpcoperation_saplingconsolidation.cpp \
wallet/asyncrpcoperation_sweep.cpp \
wallet/asyncrpcoperation_autoshieldcoinbase.cpp \
wallet/asyncrpcoperation_sendmany.cpp \
wallet/asyncrpcoperation_shieldcoinbase.cpp \
wallet/crypter.cpp \

View File

@@ -139,3 +139,103 @@ TEST(mnemonic_compat, RawEntropyDiffersFromMnemonicSeed)
const std::string entropyT = DeriveTAddrFromSeedBytes(zeros); // wrong (32-byte)
EXPECT_NE(seedT, entropyT);
}
// New storage form: the HD seed IS the expanded 64-byte BIP39 seed, the chain is
// NOT flagged mnemonic, and the phrase comes from the separate entropy record.
TEST(mnemonic_compat, ExpandedSeedIsStoredDirectly)
{
SelectParams(CBaseChainParams::MAIN);
CWallet wallet;
ASSERT_TRUE(wallet.SetHDSeedFromMnemonic(ABANDON_ART));
// Stored bytes == the 64-byte BIP39 seed, fed to derivation unchanged.
HDSeed stored;
ASSERT_TRUE(wallet.GetHDSeed(stored));
auto raw = stored.RawSeed();
EXPECT_EQ(raw.size(), (size_t)64);
EXPECT_EQ(HexStr(raw.begin(), raw.end()), std::string(SEED64_HEX));
// No CHDChain version bump / no mnemonic flag: an older binary reads this
// wallet and derives the same tree.
EXPECT_FALSE(wallet.GetHDChain().fMnemonicSeed);
EXPECT_LT(wallet.GetHDChain().nVersion, CHDChain::VERSION_HD_MNEMONIC);
HDSeed forDerivation;
ASSERT_TRUE(wallet.GetHDSeedForDerivation(forDerivation));
EXPECT_EQ(forDerivation.RawSeed(), raw);
// The phrase is still exportable, and IsMnemonicSeed() (which gates
// z_exportmnemonic) still says yes.
EXPECT_TRUE(wallet.IsMnemonicSeed());
EXPECT_TRUE(wallet.HaveMnemonicEntropy());
std::string exported;
ASSERT_TRUE(wallet.GetMnemonicPhrase(exported));
EXPECT_EQ(exported, std::string(ABANDON_ART));
}
// Backwards compatibility: a wallet in the OLD form (stored HD seed == 32-byte
// entropy, fMnemonicSeed = true) must still derive and still export its phrase.
TEST(mnemonic_compat, LegacyEntropySeedStillWorks)
{
SelectParams(CBaseChainParams::MAIN);
RawHDSeed zeros(32, 0), seed64;
ASSERT_TRUE(Bip39SeedFromEntropy(zeros, seed64));
CWallet wallet;
{
LOCK(wallet.cs_wallet);
RawHDSeed entropy(32, 0);
HDSeed legacy(entropy);
ASSERT_TRUE(wallet.InstallHDSeed(legacy, true, 1));
}
EXPECT_TRUE(wallet.GetHDChain().fMnemonicSeed);
EXPECT_FALSE(wallet.HaveMnemonicEntropy());
EXPECT_TRUE(wallet.IsMnemonicSeed());
// Still expanded on read -> same key tree as the new form.
HDSeed forDerivation;
ASSERT_TRUE(wallet.GetHDSeedForDerivation(forDerivation));
EXPECT_EQ(forDerivation.RawSeed(), seed64);
{
LOCK(wallet.cs_wallet);
EXPECT_EQ(EncodePaymentAddress(wallet.GenerateNewSaplingZKey()),
DeriveZAddrFromSeed64(seed64));
}
std::string exported;
ASSERT_TRUE(wallet.GetMnemonicPhrase(exported));
EXPECT_EQ(exported, std::string(ABANDON_ART));
}
// The entropy record refuses replacement, and a phrase that does not restore the
// installed seed is never printed.
TEST(mnemonic_compat, MnemonicEntropyGuards)
{
SelectParams(CBaseChainParams::MAIN);
RawHDSeed zeros(32, 0), ones(32, 1);
// Refuse-to-replace.
CWallet wallet;
ASSERT_TRUE(wallet.SetHDSeedFromMnemonic(ABANDON_ART));
EXPECT_FALSE(wallet.SetMnemonicEntropy(ones));
EXPECT_FALSE(wallet.SetMnemonicEntropy(RawHDSeed())); // empty is not "installed"
// Mismatched entropy -> no phrase. Install a 64-byte seed that is NOT the
// expansion of `zeros`, then attach `zeros` as entropy.
RawHDSeed otherSeed64;
ASSERT_TRUE(Bip39SeedFromEntropy(ones, otherSeed64));
CWallet mismatched;
ASSERT_TRUE(mismatched.SetHDSeedFromHex(HexStr(otherSeed64.begin(), otherSeed64.end())));
ASSERT_TRUE(mismatched.SetMnemonicEntropy(zeros));
std::string phrase;
EXPECT_FALSE(mismatched.GetMnemonicPhrase(phrase));
// Matching entropy attached to a hex-restored wallet -> phrase available.
CWallet matched;
ASSERT_TRUE(matched.SetHDSeedFromHex(std::string(SEED64_HEX)));
ASSERT_TRUE(matched.SetMnemonicEntropy(zeros));
ASSERT_TRUE(matched.GetMnemonicPhrase(phrase));
EXPECT_EQ(phrase, std::string(ABANDON_ART));
}

View File

@@ -471,7 +471,7 @@ std::string HelpMessage(HelpMessageMode mode)
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; cross-wallet restore parity is mainnet-only -- testnet/regtest derive a different HD coin_type). 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("-usemnemonic", strprintf(_("Create new wallets from a fresh BIP39 seed phrase so the 24 words can be exported (z_exportmnemonic) and used in SilentDragonXLite. Set to 0 for a raw random seed with no recovery phrase; existing wallets are never changed (default: %u)"), 1));
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)"));
@@ -489,6 +489,12 @@ std::string HelpMessage(HelpMessageMode mode)
strUsage += HelpMessageOpt("-zsweepexternal", _("Enable sweeping to an external wallet (default false)"));
strUsage += HelpMessageOpt("-zsweepexclude", _("Addresses to exclude from sweeping (default none)"));
strUsage += HelpMessageOpt("-autoshield", _("Automatically shield matured coinbase (mining rewards) into a seed-derived wallet z-address (default: true for wallets created or restored by this software, false when the HD seed provenance is unknown). No-op when not mining or wallet is locked."));
strUsage += HelpMessageOpt("-autoshieldinterval", strprintf(_("Block interval between automatic coinbase-shielding rounds (default: %i, min 5)"), 25));
strUsage += HelpMessageOpt("-autoshieldaddress=<zaddr>", _("Destination Sapling z-address for auto-shielded coinbase (default: reuse or create a wallet z-address). Must be spendable by this wallet."));
strUsage += HelpMessageOpt("-autoshieldfee", strprintf(_("Fee in puposhis for automatic coinbase-shielding transactions (default: %i)"), 10000));
strUsage += HelpMessageOpt("-autoshieldminutxos", strprintf(_("Only auto-shield once at least this many matured coinbase UTXOs exist (default: %i)"), 1));
strUsage += HelpMessageOpt("-deletetx", _("Enable Old Transaction Deletion"));
strUsage += HelpMessageOpt("-deleteinterval", strprintf(_("Delete transaction every <n> blocks during inital block download (default: %i)"), DEFAULT_TX_DELETE_INTERVAL));
strUsage += HelpMessageOpt("-keeptxnum", strprintf(_("Keep the last <n> transactions (default: %i)"), DEFAULT_TX_RETENTION_LASTTX));
@@ -2301,6 +2307,23 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
if (!pwalletMain->HaveHDSeed())
{
// Does this wallet predate the seed we are about to install? If so,
// that seed cannot appear in any backup the user already holds.
// Checked BEFORE installing, and before the restore path pre-derives
// its gap of keys.
bool fWalletHadContent = false;
{
LOCK(pwalletMain->cs_wallet);
std::set<CKeyID> setExistingKeys;
pwalletMain->GetKeys(setExistingKeys); // keystore.h:60 / crypter.h:212
std::set<libzcash::SaplingPaymentAddress> setExistingZAddrs;
pwalletMain->GetSaplingPaymentAddresses(setExistingZAddrs); // keystore.h:226-238
fWalletHadContent = !setExistingKeys.empty() ||
!setExistingZAddrs.empty() ||
!pwalletMain->mapWallet.empty() || // wallet.h:1041
pwalletMain->IsCrypted(); // crypter.h:174
}
std::string mnemonic = GetArg("-mnemonic", "");
std::string hdSeedHex = GetArg("-hdseed", "");
bool restoring = false;
@@ -2332,6 +2355,19 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
pwalletMain->GenerateNewSeed();
}
pwalletMain->SetHDSeedOrigin(restoring
? CWallet::HDSEED_ORIGIN_RESTORED
: (fWalletHadContent ? CWallet::HDSEED_ORIGIN_RETROFIT
: CWallet::HDSEED_ORIGIN_CREATED));
if (pwalletMain->hdSeedOrigin == CWallet::HDSEED_ORIGIN_RETROFIT)
{
LogPrintf("%s: WARNING: generated a new HD seed for a wallet that already held keys or "
"transactions. This seed is in NO backup you made before now.\n", __func__);
InitWarning(_("A new HD seed was generated for this pre-existing wallet. Any backup you "
"made before now does not contain it: back the wallet up again "
"(z_exportwallet) before receiving funds to newly derived addresses."));
}
if (restoring)
{
// Pre-derive keys (birthday = genesis) so the startup rescan finds
@@ -2350,6 +2386,15 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
LogPrintf("%s: pre-derived %d transparent and %d sapling keys for restore rescan\n", __func__, (int)tGap, (int)zGap);
}
}
else if (pwalletMain->hdSeedOrigin == CWallet::HDSEED_ORIGIN_UNRECORDED)
{
// The seed was installed by a build that predates this record, so
// we cannot tell whether it was minted onto a pre-existing wallet
// (and is therefore absent from the user's older backups). Assume
// the worst; the user can still opt in explicitly.
pwalletMain->SetHDSeedOrigin(CWallet::HDSEED_ORIGIN_UNKNOWN);
LogPrintf("%s: HD seed predates seed-provenance recording; recorded origin as unknown\n", __func__);
}
//Set Sapling Consolidation
pwalletMain->fSaplingConsolidationEnabled = GetBoolArg("-consolidation", false);
@@ -2451,6 +2496,69 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
}
}
//Set Automatic Coinbase Shielding (default ON, conditional: self-guards
//on nodes where it cannot act - no owned coinbase, external mineraddress,
//or locked wallet). Closes the transparent-coinbase leak for miners.
// Default ON only when this wallet's HD seed provenance says the
// destination is genuinely recoverable. Autoshield sends mined coinbase to
// a seed-derived z-address (resolveDestination), so for a seed retrofitted
// onto a pre-existing wallet - or one predating provenance recording - we
// cannot assume the user holds it. Those wallets opt in with -autoshield=1
// after backing the seed up.
const bool fAutoShieldSeedKnown =
(pwalletMain->hdSeedOrigin == CWallet::HDSEED_ORIGIN_CREATED ||
pwalletMain->hdSeedOrigin == CWallet::HDSEED_ORIGIN_RESTORED);
pwalletMain->fAutoShieldEnabled = GetBoolArg("-autoshield", fAutoShieldSeedKnown);
if (!fAutoShieldSeedKnown && !mapArgs.count("-autoshield")) {
LogPrintf("%s: autoshield left OFF by default: HD seed origin %d is not known-recoverable. "
"Back the seed up (z_exportwallet, or z_exportmnemonic on a mnemonic wallet) and "
"pass -autoshield=1 to enable.\n", __func__, pwalletMain->hdSeedOrigin);
}
if (pwalletMain->fAutoShieldEnabled) {
int autoShieldInterval = GetArg("-autoshieldinterval", 25);
if (autoShieldInterval < 5) {
fprintf(stderr,"%s: Invalid autoshield interval of %d < 5, setting to default of 25\n", __func__, autoShieldInterval);
autoShieldInterval = 25;
}
pwalletMain->autoShieldInterval = autoShieldInterval;
pwalletMain->nextAutoShield = pwalletMain->autoShieldInterval + chainActive.Height();
// Validate the fee: floor it above the relay minimum and cap it to
// guard against a fat-finger (e.g. -autoshieldfee=5000000000) that
// would otherwise build an over-fee or malformed shield tx that
// fails mempool admission every round.
CAmount autoShieldFee = GetArg("-autoshieldfee", 10000);
const CAmount AUTOSHIELD_MIN_FEE = 1000; // comfortably above minRelayTxFee for a small tx
const CAmount AUTOSHIELD_MAX_FEE = 10000000; // 0.1 DRGX; no sane autoshield fee exceeds this
if (autoShieldFee < AUTOSHIELD_MIN_FEE || autoShieldFee > AUTOSHIELD_MAX_FEE) {
fprintf(stderr,"%s: -autoshieldfee=%lld out of range [%lld,%lld], using default 10000\n",
__func__, (long long)autoShieldFee, (long long)AUTOSHIELD_MIN_FEE, (long long)AUTOSHIELD_MAX_FEE);
autoShieldFee = 10000;
}
pwalletMain->autoShieldFee = autoShieldFee;
pwalletMain->autoShieldMinUtxos = GetArg("-autoshieldminutxos", 1);
if (pwalletMain->autoShieldMinUtxos < 1) {
pwalletMain->autoShieldMinUtxos = 1;
}
LogPrintf("%s: autoshield enabled, nextAutoShield=%d interval=%d\n", __func__, pwalletMain->nextAutoShield, pwalletMain->autoShieldInterval);
//Optional explicit destination z-address. Must be a Sapling zaddr the
//wallet can spend, else the shielded coinbase would be unrecoverable.
std::string autoShieldAddress = GetArg("-autoshieldaddress", "");
if (!autoShieldAddress.empty()) {
auto zdest = DecodePaymentAddress(autoShieldAddress);
if (!IsValidPaymentAddress(zdest) ||
boost::get<libzcash::SaplingPaymentAddress>(&zdest) == nullptr) {
return InitError("Invalid -autoshieldaddress: must be a Sapling z-address");
}
auto hasSpendingKey = boost::apply_visitor(HaveSpendingKeyForPaymentAddress(pwalletMain), zdest);
if (!hasSpendingKey) {
return InitError("Wallet must hold the spending key of -autoshieldaddress (else shielded coinbase would be unrecoverable)");
}
pwalletMain->autoShieldAddress = autoShieldAddress;
}
}
//Set Transaction Deletion Options
fTxDeleteEnabled = GetBoolArg("-deletetx", false);
fTxConflictDeleteEnabled = GetBoolArg("-deleteconflicttx", true);

View File

@@ -68,6 +68,42 @@ bool CBasicKeyStore::GetHDSeed(HDSeed& seedOut) const
}
}
bool CBasicKeyStore::SetMnemonicEntropy(const RawHDSeed& entropy)
{
LOCK(cs_SpendingKeyStore);
if (entropy.empty()) {
// Never "install" nothing: HaveMnemonicEntropy() would stay false while
// the caller was told the call succeeded.
return false;
}
if (!mnemonicEntropy.empty()) {
// Same refuse-to-replace rule as SetHDSeed above, for a sharper reason:
// this is the printable form of the seed. If it could be swapped while
// hdSeed stayed put, the wallet would print a seed phrase that does not
// restore it -- strictly worse than printing none.
return false;
}
mnemonicEntropy = entropy;
return true;
}
bool CBasicKeyStore::HaveMnemonicEntropy() const
{
LOCK(cs_SpendingKeyStore);
return !mnemonicEntropy.empty();
}
bool CBasicKeyStore::GetMnemonicEntropy(RawHDSeed& entropyOut) const
{
LOCK(cs_SpendingKeyStore);
if (mnemonicEntropy.empty()) {
return false;
} else {
entropyOut = mnemonicEntropy;
return true;
}
}
bool CBasicKeyStore::AddKeyPubKey(const CKey& key, const CPubKey &pubkey)
{
LOCK(cs_KeyStore);

View File

@@ -117,6 +117,12 @@ class CBasicKeyStore : public CKeyStore
{
protected:
HDSeed hdSeed;
// BIP39 entropy for a mnemonic-recoverable wallet, kept BESIDE hdSeed, never
// instead of it. hdSeed holds the bytes actually fed to derivation (the
// expanded 64-byte BIP39 seed on new wallets); this record exists only so the
// seed phrase can be reprinted. Empty on legacy and hex-restored wallets,
// which is a normal state, not an error.
RawHDSeed mnemonicEntropy;
KeyMap mapKeys;
ScriptMap mapScripts;
WatchOnlySet setWatchOnly;
@@ -129,6 +135,18 @@ public:
bool SetHDSeed(const HDSeed& seed);
bool HaveHDSeed() const;
bool GetHDSeed(HDSeed& seedOut) const;
//! Mnemonic entropy: optional, present only on phrase-recoverable wallets.
//! Unlike the three seed accessors above -- which override pure virtuals on
//! CKeyStore (keystore.h:48-51) and therefore dispatch dynamically -- these
//! are plain non-virtual members: CKeyStore declares nothing for them and
//! nothing reaches the entropy through a base pointer. Every caller holds a
//! CWallet*, whose static type resolves to the CCryptoKeyStore overloads.
//! Do NOT add them to CKeyStore: that would force all four subclasses
//! (CBasicKeyStore, CCryptoKeyStore, CWallet, gtest's TestCCryptoKeyStore)
//! to implement them for zero call sites.
bool SetMnemonicEntropy(const RawHDSeed& entropy);
bool HaveMnemonicEntropy() const;
bool GetMnemonicEntropy(RawHDSeed& entropyOut) const;
bool AddKeyPubKey(const CKey& key, const CPubKey &pubkey);
bool HaveKey(const CKeyID &address) const

View File

@@ -0,0 +1,407 @@
// Copyright (c) 2016-2024 The Hush developers
// 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
#include "asyncrpcoperation_autoshieldcoinbase.h"
#include "asyncrpcoperation_shieldcoinbase.h" // for ShieldCoinbaseUTXO
#include "consensus/upgrades.h"
#include "hush_defs.h" // ASSETCHAINS_TIMELOCKGTE
#include "init.h"
#include "key_io.h"
#include "main.h"
#include "rpc/protocol.h"
#include "sync.h"
#include "tinyformat.h"
#include "transaction_builder.h"
#include "util.h"
#include "utilmoneystr.h"
#include "wallet.h"
// Sietch dummy zaddr generator (defined in wallet/rpcwallet.cpp)
extern std::string randomSietchZaddr();
// Serialized-size estimates for one spent input (kept in sync with rpcwallet.cpp)
static const size_t AUTOSHIELD_CTXIN_DUST_SIZE = 148;
static const size_t AUTOSHIELD_CTXIN_P2SH_SIZE = 400;
// Expire unmined autoshield txs after this many blocks, so a tx cannot straddle
// a network-upgrade activation.
static const int AUTOSHIELD_EXPIRY_DELTA = 15;
AsyncRPCOperation_autoshieldcoinbase::AsyncRPCOperation_autoshieldcoinbase(int targetHeight)
: targetHeight_(targetHeight) {}
AsyncRPCOperation_autoshieldcoinbase::~AsyncRPCOperation_autoshieldcoinbase() {}
void AsyncRPCOperation_autoshieldcoinbase::main() {
if (isCancelled()) {
// Only the CURRENT op owns the scheduler flag; a stale/cancelled op must
// not clear it out from under a freshly-enqueued successor.
if (pwalletMain) {
LOCK(pwalletMain->cs_wallet);
if (getId() == pwalletMain->saplingAutoShieldOperationId) {
pwalletMain->fAutoShieldRunning = false;
}
}
return;
}
set_state(OperationStatus::EXECUTING);
start_execution_clock();
bool success = false;
try {
success = main_impl();
} catch (const UniValue& objError) {
int code = find_value(objError, "code").get_int();
std::string message = find_value(objError, "message").get_str();
set_error_code(code);
set_error_message(message);
} catch (const runtime_error& e) {
set_error_code(-1);
set_error_message("runtime error: " + string(e.what()));
} catch (const logic_error& e) {
set_error_code(-1);
set_error_message("logic error: " + string(e.what()));
} catch (const exception& e) {
set_error_code(-1);
set_error_message("general exception: " + string(e.what()));
} catch (...) {
set_error_code(-2);
set_error_message("unknown error");
}
stop_execution_clock();
// ALWAYS advance the interval and clear the running flag, on success AND
// failure AND exception, so a failed/oversized/locked round still lets the
// next round fire. Only the CURRENT op does this bookkeeping: if a newer op
// has already superseded this one, leave its state untouched.
if (pwalletMain) {
LOCK2(cs_main, pwalletMain->cs_wallet);
if (getId() == pwalletMain->saplingAutoShieldOperationId) {
int tipHeight = (chainActive.Tip() != NULL) ? chainActive.Tip()->GetHeight() : targetHeight_;
pwalletMain->nextAutoShield = pwalletMain->autoShieldInterval + tipHeight;
pwalletMain->fAutoShieldRunning = false;
}
}
set_state(success ? OperationStatus::SUCCESS : OperationStatus::FAILED);
setResult();
LogPrintf("%s: autoshield operation finished (status=%s, txs=%d, shielded=%s)\n",
getId(), getStateAsString(), numTxCreated_, FormatMoney(amountShielded_));
}
// Resolve the Sapling destination for auto-shielded coinbase.
//
// Recoverability is the hard requirement: coinbase we shield must land in an
// address that a bare -mnemonic/-hdseed restore of THIS wallet's seed re-derives
// on its own. A restore pre-derives exactly -mnemonicsaplinggap sapling accounts
// starting at index 0, with saplingAccountCounter reset to 0 (init.cpp:2349-2355),
// so the only self-recoverable destinations are the default addresses of
// m/32'/<coin>'/i' for i < gap.
//
// We therefore DERIVE those accounts from the seed and pick the lowest index the
// wallet already holds. Deriving is the only authoritative test. In particular
// CKeyMetadata is NOT evidence of provenance: z_importkey / z_importwallet copy
// both hdKeypath and seedFp verbatim out of the import source
// (wallet.cpp:5522-5529 <- rpcdump.cpp:511-516), so a foreign key can claim any
// keypath and any seed fingerprint. Filtering on metadata would let an imported
// key win as "account 0" and silently receive every shielded reward.
//
// Caller must hold cs_wallet and must already have checked the wallet is unlocked.
bool AsyncRPCOperation_autoshieldcoinbase::resolveDestination(
libzcash::SaplingPaymentAddress& destOut, std::string& destStrOut) {
// 1. Explicit -autoshieldaddress override (validated as a spendable Sapling
// zaddr at init.cpp:2494-2506). This also serves as the per-process cache
// for whatever step 2/3 resolved.
if (!pwalletMain->autoShieldAddress.empty()) {
auto decoded = DecodePaymentAddress(pwalletMain->autoShieldAddress);
if (boost::get<libzcash::SaplingPaymentAddress>(&decoded) != nullptr) {
destOut = boost::get<libzcash::SaplingPaymentAddress>(decoded);
destStrOut = pwalletMain->autoShieldAddress;
return true;
}
LogPrintf("%s: configured -autoshieldaddress is not a valid Sapling address\n", getId());
return false;
}
// 2. Walk the restore window m/32'/coin'/[0, gap)' derived from the seed.
HDSeed seed;
if (!pwalletMain->GetHDSeedForDerivation(seed)) {
LogPrintf("%s: no HD seed available; refusing to pick an autoshield destination\n", getId());
return false;
}
// Mirror init.cpp:2349-2350's own clamp, and cap into the hardened index
// space so (i | ZIP32_HARDENED_KEY_LIMIT) below stays well formed.
int64_t gapArg = GetArg("-mnemonicsaplinggap", 100);
if (gapArg < 0) {
gapArg = 0;
}
if (gapArg > (int64_t)ZIP32_HARDENED_KEY_LIMIT) {
gapArg = (int64_t)ZIP32_HARDENED_KEY_LIMIT;
}
const uint32_t saplingGap = (uint32_t)gapArg;
// Same derivation path as CWallet::GenerateNewSaplingZKey (wallet.cpp:139-152).
const uint32_t bip44CoinType = Params().BIP44CoinType();
auto m = libzcash::SaplingExtendedSpendingKey::Master(seed);
auto m_32h = m.Derive(32 | ZIP32_HARDENED_KEY_LIMIT);
auto m_32h_cth = m_32h.Derive(bip44CoinType | ZIP32_HARDENED_KEY_LIMIT);
for (uint32_t i = 0; i < saplingGap; i++) {
auto xsk = m_32h_cth.Derive(i | ZIP32_HARDENED_KEY_LIMIT);
auto addr = xsk.DefaultAddress();
// Spendable AND registered: GetSaplingExtendedSpendingKey resolves
// addr -> ivk -> fvk -> spending key (keystore.cpp:215-223), so a hit
// means the wallet both recognises notes sent to `addr` and can spend
// them. Exactly the pair of properties the shield needs. Lowest index
// wins: stable for the life of the wallet and reproducible from the seed
// alone, unlike std::set order over the random diversifier
// (zcash/Address.hpp:95-98).
libzcash::SaplingExtendedSpendingKey held;
if (pwalletMain->GetSaplingExtendedSpendingKey(addr, held)) {
destOut = addr;
destStrOut = EncodePaymentAddress(addr);
// Cache for the life of the process; step 1 short-circuits later
// rounds. Safe: we only cache post-validation.
pwalletMain->autoShieldAddress = destStrOut;
LogPrintf("%s: autoshield destination %s (seed-derived sapling account %u, gap %u)\n",
getId(), destStrOut, (unsigned)i, (unsigned)saplingGap);
return true;
}
}
// 3. Nothing usable in the window yet: derive the next account, but only if
// GenerateNewSaplingZKey will land INSIDE the window. It does NOT derive
// at saplingAccountCounter: its do/while skips every index whose spending
// key we already hold (wallet.cpp:150-157), so a bare "counter < gap"
// test is unsound - counter 98 with gap 100 can still land on 100.
// Predict min{ i >= counter : we do not hold i } instead.
const uint32_t counter = pwalletMain->GetHDChain().saplingAccountCounter;
uint32_t predicted = saplingGap; // sentinel: "would land outside the window"
for (uint32_t i = counter; i < saplingGap; i++) {
auto xsk = m_32h_cth.Derive(i | ZIP32_HARDENED_KEY_LIMIT);
if (!pwalletMain->HaveSaplingSpendingKey(xsk.expsk.full_viewing_key())) {
predicted = i;
break;
}
}
if (predicted == saplingGap) {
LogPrintf("%s: no free sapling account below -mnemonicsaplinggap=%u (account counter is %u). "
"A newly derived z-address would NOT be re-derived by a seed restore, so the "
"shielded coinbase could not be recovered from the seed alone. Refusing to "
"autoshield this round. Fix: point -autoshieldaddress at an existing in-gap "
"wallet z-address, or raise -mnemonicsaplinggap here AND use the same value on "
"any future restore.\n",
getId(), (unsigned)saplingGap, (unsigned)counter);
return false;
}
if (pwalletMain->IsLocked()) {
LogPrintf("%s: wallet is locked; cannot derive an autoshield destination z-address\n", getId());
return false;
}
try {
auto expectedAddr = m_32h_cth.Derive(predicted | ZIP32_HARDENED_KEY_LIMIT).DefaultAddress();
libzcash::SaplingPaymentAddress newAddr = pwalletMain->GenerateNewSaplingZKey();
// Post-verify rather than trust the prediction: cheap, and it closes the
// whole class of "the counter moved further than expected" bugs.
if (!(newAddr == expectedAddr)) {
LogPrintf("%s: newly derived z-address is not sapling account %u as predicted "
"(counter %u -> %u); not using it as the autoshield destination\n",
getId(), (unsigned)predicted, (unsigned)counter,
(unsigned)pwalletMain->GetHDChain().saplingAccountCounter);
return false;
}
destOut = newAddr;
destStrOut = EncodePaymentAddress(newAddr);
pwalletMain->autoShieldAddress = destStrOut;
LogPrintf("%s: generated new autoshield destination z-address %s (seed-derived sapling account %u)\n",
getId(), destStrOut, (unsigned)predicted);
return true;
} catch (const std::exception& e) {
LogPrintf("%s: could not generate a destination z-address: %s\n", getId(), e.what());
return false;
}
}
bool AsyncRPCOperation_autoshieldcoinbase::main_impl() {
auto opid = getId();
LogPrintf("%s: Beginning asyncrpcoperation_autoshieldcoinbase.\n", opid);
auto consensusParams = Params().GetConsensus();
int tipHeight;
{
LOCK(cs_main);
tipHeight = (chainActive.Tip() != NULL) ? chainActive.Tip()->GetHeight() : targetHeight_;
}
// Don't create a tx that could be mined before, but expire after, a NU
// activation. Key this off tipHeight (the height we actually set the expiry
// from below), not the stale enqueue-time targetHeight_, so a queue delay
// cannot slip a straddling expiry past this guard.
auto nextActivationHeight = NextActivationHeight(tipHeight, consensusParams);
if (nextActivationHeight && tipHeight + AUTOSHIELD_EXPIRY_DELTA >= nextActivationHeight.get()) {
LogPrintf("%s: autoshield tx could expire across a NU activation. Skipping this round.\n", opid);
return true;
}
libzcash::SaplingPaymentAddress destZaddr;
std::string destStr;
std::vector<ShieldCoinbaseUTXO> inputs;
CAmount shieldedValue = 0;
unsigned int max_tx_size = MAX_TX_SIZE_AFTER_SAPLING;
{
LOCK2(cs_main, pwalletMain->cs_wallet);
// Defensive: the scheduler already skips while locked, but the wallet
// could have been locked between enqueue and execution.
if (pwalletMain->IsLocked()) {
LogPrintf("%s: wallet is locked, skipping autoshield round\n", opid);
return true;
}
if (!resolveDestination(destZaddr, destStr)) {
LogPrintf("%s: no spendable destination z-address available, skipping\n", opid);
return true;
}
// Gather matured, spendable coinbase UTXOs, byte-capped to a single tx.
// AvailableCoins with fOnlySpendable already excludes immature coinbase
// (< COINBASE_MATURITY) and outputs we don't own, so external
// -mineraddress / pool coinbase naturally yields zero inputs.
size_t estimatedTxSize = 2000; // header + sietch outputs headroom
std::vector<COutput> vecOutputs;
pwalletMain->AvailableCoins(vecOutputs, true, NULL, false, true);
for (const COutput& out : vecOutputs) {
if (!out.fSpendable || !out.tx->IsCoinBase()) {
continue;
}
CTxDestination address;
if (!ExtractDestination(out.tx->vout[out.i].scriptPubKey, address)) {
continue;
}
size_t increase = (boost::get<CScriptID>(&address) != nullptr)
? AUTOSHIELD_CTXIN_P2SH_SIZE : AUTOSHIELD_CTXIN_DUST_SIZE;
if (estimatedTxSize + increase >= max_tx_size) {
// Size-safe batch; the remainder is shielded next round.
LogPrintf("%s: reached per-tx size cap; deferring remaining coinbase to next round\n", opid);
break;
}
estimatedTxSize += increase;
ShieldCoinbaseUTXO utxo = { out.tx->GetHash(), out.i,
out.tx->vout[out.i].scriptPubKey,
out.tx->vout[out.i].nValue };
inputs.push_back(utxo);
shieldedValue += out.tx->vout[out.i].nValue;
}
}
CAmount fee = pwalletMain->autoShieldFee;
if (inputs.size() < (size_t)pwalletMain->autoShieldMinUtxos) {
LogPrintf("%s: %d matured coinbase utxo(s) < min %d, skipping this round\n",
opid, (int)inputs.size(), pwalletMain->autoShieldMinUtxos);
return true;
}
if (shieldedValue <= fee) {
LogPrintf("%s: matured coinbase value %s <= fee %s, skipping\n",
opid, FormatMoney(shieldedValue), FormatMoney(fee));
return true;
}
// Common outgoing viewing key derived from the HD seed, exactly as
// z_shieldcoinbase does for t->z (keeps the note recoverable).
HDSeed seed;
if (!pwalletMain->GetHDSeedForDerivation(seed)) {
LogPrintf("%s: HD seed not available, skipping\n", opid);
return true;
}
uint256 ovk = ovkForShieldingFromTaddr(seed);
// Build the t->z shield tx. Proof generation happens in Build() WITHOUT
// holding cs_wallet (mirrors the sweep op) so we don't stall wallet RPCs.
auto builder = TransactionBuilder(consensusParams, targetHeight_, pwalletMain);
builder.SetExpiryHeight(tipHeight + AUTOSHIELD_EXPIRY_DELTA);
builder.SetFee(fee);
for (const auto& t : inputs) {
if (t.amount >= ASSETCHAINS_TIMELOCKGTE) {
builder.SetLockTime((uint32_t)tipHeight);
builder.AddTransparentInput(COutPoint(t.txid, t.vout), t.scriptPubKey, t.amount, 0xfffffffe);
} else {
builder.AddTransparentInput(COutPoint(t.txid, t.vout), t.scriptPubKey, t.amount);
}
}
// All input value (less fee) goes back to our own z-address as change.
builder.SendChangeTo(destZaddr, ovk);
// Sietch padding: mirror z_shieldcoinbase's two dummy zouts so autoshield
// txs are structurally indistinguishable from manual coinbase shields.
for (int i = 0; i < 2; i++) {
auto zdust = DecodePaymentAddress(randomSietchZaddr());
if (IsValidPaymentAddress(zdust)) {
builder.AddSaplingOutput(ovk, boost::get<libzcash::SaplingPaymentAddress>(zdust), 0);
}
}
auto maybe_tx = builder.Build();
if (!maybe_tx) {
LogPrintf("%s: Failed to build autoshield transaction.\n", opid);
return false;
}
CTransaction tx = maybe_tx.get();
if (isCancelled()) {
LogPrintf("%s: Cancelled before commit.\n", opid);
return false;
}
if (pwalletMain->CommitAutomatedTx(tx)) {
LogPrintf("%s: shielded %s coinbase (%d utxos) into %s via txid=%s\n",
opid, FormatMoney(shieldedValue - fee), (int)inputs.size(),
destStr, tx.GetHash().ToString());
amountShielded_ += shieldedValue - fee;
shieldTxIds_.push_back(tx.GetHash().ToString());
numTxCreated_++;
return true;
}
LogPrintf("%s: autoshield tx FAILED in CommitTransaction, txid=%s\n", opid, tx.GetHash().ToString());
return false;
}
void AsyncRPCOperation_autoshieldcoinbase::setResult() {
UniValue res(UniValue::VOBJ);
res.push_back(Pair("num_tx_created", numTxCreated_));
res.push_back(Pair("amount_shielded", FormatMoney(amountShielded_)));
UniValue txIds(UniValue::VARR);
for (const std::string& txId : shieldTxIds_) {
txIds.push_back(txId);
}
res.push_back(Pair("shield_txids", txIds));
set_result(res);
}
void AsyncRPCOperation_autoshieldcoinbase::cancel() {
set_state(OperationStatus::CANCELLED);
}
UniValue AsyncRPCOperation_autoshieldcoinbase::getStatus() const {
UniValue v = AsyncRPCOperation::getStatus();
UniValue obj = v.get_obj();
obj.push_back(Pair("method", "autoshieldcoinbase"));
obj.push_back(Pair("target_height", targetHeight_));
return obj;
}

View File

@@ -0,0 +1,58 @@
// Copyright (c) 2016-2024 The Hush developers
// 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
#ifndef ASYNCRPCOPERATION_AUTOSHIELDCOINBASE_H
#define ASYNCRPCOPERATION_AUTOSHIELDCOINBASE_H
#include "amount.h"
#include "asyncrpcoperation.h"
#include "univalue.h"
#include "zcash/Address.hpp"
#include "zcash/zip32.h"
// Default fee for automatic coinbase-shielding transactions
static const CAmount DEFAULT_AUTOSHIELD_FEE = 10000;
// A periodic, wallet-local operation that drains matured *transparent* coinbase
// UTXOs into a wallet-owned Sapling z-address in size-bounded batches. It is the
// automatic sibling of the manual z_shieldcoinbase RPC and mirrors the dispatch
// model of AsyncRPCOperation_sweep (self-gathers on the async worker thread,
// commits via CWallet::CommitAutomatedTx). It never mints a transparent output,
// so it respects the ac_private=1 transparent-output ban, and it deliberately
// does NOT toggle mining (unlike z_shieldcoinbase) so it can run every interval
// on a mining node without thrashing the miner.
class AsyncRPCOperation_autoshieldcoinbase : public AsyncRPCOperation
{
public:
AsyncRPCOperation_autoshieldcoinbase(int targetHeight);
virtual ~AsyncRPCOperation_autoshieldcoinbase();
// We don't want to be copied or moved around
AsyncRPCOperation_autoshieldcoinbase(AsyncRPCOperation_autoshieldcoinbase const&) = delete;
AsyncRPCOperation_autoshieldcoinbase(AsyncRPCOperation_autoshieldcoinbase&&) = delete;
AsyncRPCOperation_autoshieldcoinbase& operator=(AsyncRPCOperation_autoshieldcoinbase const&) = delete;
AsyncRPCOperation_autoshieldcoinbase& operator=(AsyncRPCOperation_autoshieldcoinbase&&) = delete;
virtual void main();
virtual void cancel();
virtual UniValue getStatus() const;
private:
int targetHeight_;
int numTxCreated_ = 0;
CAmount amountShielded_ = 0;
std::vector<std::string> shieldTxIds_;
bool main_impl();
// Resolve a spendable, wallet-owned Sapling destination: the configured
// -autoshieldaddress if set, else the first spendable z-addr the wallet
// holds, else a freshly generated one (requires an unlocked wallet).
// Returns false if none is available (e.g. locked wallet with no z-addr).
bool resolveDestination(libzcash::SaplingPaymentAddress& destOut, std::string& destStrOut);
void setResult();
};
#endif /* ASYNCRPCOPERATION_AUTOSHIELDCOINBASE_H */

View File

@@ -28,8 +28,17 @@ AsyncRPCOperation_saplingconsolidation::AsyncRPCOperation_saplingconsolidation(i
AsyncRPCOperation_saplingconsolidation::~AsyncRPCOperation_saplingconsolidation() {}
void AsyncRPCOperation_saplingconsolidation::main() {
if (isCancelled())
if (isCancelled()) {
// Only the current op owns the scheduler flag; a stale/cancelled op must
// not clear it out from under a freshly-enqueued successor.
if (pwalletMain) {
LOCK(pwalletMain->cs_wallet);
if (getId() == pwalletMain->saplingConsolidationOperationId) {
pwalletMain->fConsolidationRunning = false;
}
}
return;
}
set_state(OperationStatus::EXECUTING);
start_execution_clock();
@@ -76,6 +85,21 @@ void AsyncRPCOperation_saplingconsolidation::main() {
LogPrintf("%s", s);
unlock_notes(); // clean up
LogPrint("zrpc", "%s: consolidation input notes unlocked\n", getId());
// Advance the interval and clear the running flag on EVERY terminal state
// (success, failure, exception) so consolidation runs once per interval
// instead of every block, and a failed round still lets the next one fire.
// Only the CURRENT op does this bookkeeping. This fixes the pre-existing
// wedge where nextConsolidation never advanced and fConsolidationRunning
// was never set/reset.
if (pwalletMain) {
LOCK2(cs_main, pwalletMain->cs_wallet);
if (getId() == pwalletMain->saplingConsolidationOperationId) {
int tipHeight = (chainActive.Tip() != NULL) ? chainActive.Tip()->GetHeight() : targetHeight_;
pwalletMain->nextConsolidation = pwalletMain->consolidationInterval + tipHeight;
pwalletMain->fConsolidationRunning = false;
}
}
}
bool AsyncRPCOperation_saplingconsolidation::main_impl() {

View File

@@ -27,8 +27,17 @@ AsyncRPCOperation_sweep::AsyncRPCOperation_sweep(int targetHeight, bool fromRpc)
AsyncRPCOperation_sweep::~AsyncRPCOperation_sweep() {}
void AsyncRPCOperation_sweep::main() {
if (isCancelled())
if (isCancelled()) {
// Only the current op owns the scheduler flag; a stale/cancelled op must
// not clear it out from under a freshly-enqueued successor.
if (pwalletMain) {
LOCK(pwalletMain->cs_wallet);
if (getId() == pwalletMain->saplingSweepOperationId) {
pwalletMain->fSweepRunning = false;
}
}
return;
}
set_state(OperationStatus::EXECUTING);
start_execution_clock();
@@ -64,6 +73,23 @@ void AsyncRPCOperation_sweep::main() {
set_state(OperationStatus::FAILED);
}
// Scheduler bookkeeping, done here so it runs on success AND failure AND
// exception (main_impl's terminal code is skipped when it throws). Only the
// current op mutates scheduler state. Preserves the "keep draining every
// block until swept" model: on a successful-but-incomplete round we leave
// fSweepRunning set and nextSweep unadvanced so the next block continues.
// On completion OR on failure/exception we release fSweepRunning and back
// off one interval — critically, a persistently failing sweep no longer
// leaves fSweepRunning stuck true and wedges consolidation + autoshield.
if (pwalletMain) {
LOCK2(cs_main, pwalletMain->cs_wallet);
if (getId() == pwalletMain->saplingSweepOperationId && (!success || sweepComplete_)) {
int tipHeight = (chainActive.Tip() != NULL) ? chainActive.Tip()->GetHeight() : targetHeight_;
pwalletMain->nextSweep = pwalletMain->sweepInterval + tipHeight;
pwalletMain->fSweepRunning = false;
}
}
std::string s = strprintf("%s: Sweep operation finished. (status=%s", getId(), getStateAsString());
if (success) {
s += strprintf(", success)\n");
@@ -314,10 +340,11 @@ bool AsyncRPCOperation_sweep::main_impl() {
}
}
if (sweepComplete) {
pwalletMain->nextSweep = pwalletMain->sweepInterval + chainActive.Tip()->GetHeight();
pwalletMain->fSweepRunning = false;
}
// Record whether the wallet is fully swept; the scheduler bookkeeping
// (advancing nextSweep / clearing fSweepRunning) is done in main() so it
// also runs on the failure/exception/cancel paths and cannot wedge the
// shared fSweepRunning flag (which now also gates consolidation + autoshield).
sweepComplete_ = sweepComplete;
LogPrintf("%s: Created %d transactions with total output amount=%s, status=%d\n", getId(), numTxCreated, FormatMoney(amountSwept), (int)status);
setSweepResult(numTxCreated, amountSwept, sweepTxIds);

View File

@@ -34,6 +34,10 @@ public:
private:
int targetHeight_;
bool fromRPC_;
// Set by main_impl(): true iff there was nothing left to sweep this round.
// Read by main() to decide scheduler bookkeeping. Defaults false so an
// exception (which skips main_impl's assignment) is treated as "not done".
bool sweepComplete_ = false;
bool main_impl();

View File

@@ -159,6 +159,34 @@ static bool DecryptHDSeed(
return seed.Fingerprint() == seedFp;
}
uint256 MnemonicEntropyFingerprint(const RawHDSeed& entropy)
{
// The local copy is not gratuitous -- see the declaration in crypter.h.
// It is secure_allocator-backed, so it is memory_cleanse()d on destruction
// (support/allocators/secure.h:45-52).
RawHDSeed tmp(entropy);
return HDSeed(tmp).Fingerprint();
}
static bool DecryptMnemonicEntropy(
const CKeyingMaterial& vMasterKey,
const std::vector<unsigned char>& vchCryptedSecret,
const uint256& entropyFp,
RawHDSeed& entropyOut)
{
CKeyingMaterial vchSecret;
// Use the entropy's fingerprint as IV, mirroring DecryptHDSeed above.
if (!DecryptSecret(vMasterKey, vchCryptedSecret, entropyFp, vchSecret))
return false;
// RawHDSeed and CKeyingMaterial are the SAME type (both are
// std::vector<unsigned char, secure_allocator<unsigned char>>), so this is a
// plain copy of the same bytes, not a reinterpretation.
entropyOut = vchSecret;
return MnemonicEntropyFingerprint(entropyOut) == entropyFp;
}
static bool DecryptKey(const CKeyingMaterial& vMasterKey, const std::vector<unsigned char>& vchCryptedSecret, const CPubKey& vchPubKey, CKey& key)
{
CKeyingMaterial vchSecret;
@@ -233,6 +261,19 @@ bool CCryptoKeyStore::Unlock(const CKeyingMaterial& vMasterKeyIn)
keyPass = true;
}
}
// Deliberately NO arm here for cryptedMnemonicEntropy. This function is
// the "some keys decrypt but not all" corruption detector and a keyFail
// ends at the assert(false) below. The mnemonic entropy is an optional,
// non-spending, display-only record: legacy wallets, hex-restored
// wallets and every wallet predating this feature legitimately have a
// seed and no entropy, and a wallet whose every key decrypts while its
// entropy does not is not corrupt in any sense that should abort the
// process -- it simply cannot print its seed phrase. It is decrypted
// lazily in GetMnemonicEntropy() instead, so that case becomes a false
// return from one RPC while derivation and spending (which read the
// seed, not the entropy) carry on. Note the arm above caches nothing
// either -- `seed` is discarded; it only votes keyPass/keyFail -- so
// nothing is lost by omitting one here.
CryptedKeyMap::const_iterator mi = mapCryptedKeys.begin();
for (; mi != mapCryptedKeys.end(); ++mi)
{
@@ -344,6 +385,82 @@ bool CCryptoKeyStore::GetHDSeed(HDSeed& seedOut) const
return DecryptHDSeed(vMasterKey, cryptedHDSeed.second, cryptedHDSeed.first, seedOut);
}
bool CCryptoKeyStore::SetMnemonicEntropy(const RawHDSeed& entropy)
{
{
LOCK(cs_SpendingKeyStore);
if (!IsCrypted()) {
return CBasicKeyStore::SetMnemonicEntropy(entropy);
}
if (IsLocked())
return false;
if (entropy.empty())
return false;
std::vector<unsigned char> vchCryptedSecret;
// Use the entropy's fingerprint as IV
// TODO: Handle this properly when we make encryption a supported feature
auto entropyFp = MnemonicEntropyFingerprint(entropy);
// RawHDSeed IS CKeyingMaterial, so `entropy` binds directly here.
if (!EncryptSecret(vMasterKey, entropy, entropyFp, vchCryptedSecret))
return false;
// Virtual: this calls into CWallet to store the crypted entropy to disk.
if (!SetCryptedMnemonicEntropy(entropyFp, vchCryptedSecret))
return false;
}
return true;
}
bool CCryptoKeyStore::SetCryptedMnemonicEntropy(
const uint256& entropyFp,
const std::vector<unsigned char>& vchCryptedSecret)
{
{
LOCK(cs_SpendingKeyStore);
if (!IsCrypted()) {
return false;
}
if (!cryptedMnemonicEntropy.first.IsNull()) {
// Don't allow existing entropy to be changed, mirroring
// SetCryptedHDSeed: a phrase that no longer matches the installed
// seed is worse than no phrase at all.
return false;
}
cryptedMnemonicEntropy = std::make_pair(entropyFp, vchCryptedSecret);
}
return true;
}
bool CCryptoKeyStore::HaveMnemonicEntropy() const
{
LOCK(cs_SpendingKeyStore);
if (!IsCrypted())
return CBasicKeyStore::HaveMnemonicEntropy();
return !cryptedMnemonicEntropy.second.empty();
}
bool CCryptoKeyStore::GetMnemonicEntropy(RawHDSeed& entropyOut) const
{
LOCK(cs_SpendingKeyStore);
if (!IsCrypted())
return CBasicKeyStore::GetMnemonicEntropy(entropyOut);
if (cryptedMnemonicEntropy.second.empty())
return false;
// Decrypted lazily, on demand, and deliberately NOT in Unlock(): see the
// comment there for why the entropy must not vote in the keyPass/keyFail
// corruption detector.
return DecryptMnemonicEntropy(vMasterKey, cryptedMnemonicEntropy.second,
cryptedMnemonicEntropy.first, entropyOut);
}
bool CCryptoKeyStore::AddKeyPubKey(const CKey& key, const CPubKey &pubkey)
{
{
@@ -505,6 +622,30 @@ bool CCryptoKeyStore::EncryptKeys(CKeyingMaterial& vMasterKeyIn)
}
hdSeed = HDSeed();
}
if (!mnemonicEntropy.empty()) {
{
std::vector<unsigned char> vchCryptedSecret;
// Use the entropy's fingerprint as IV
// TODO: Handle this properly when we make encryption a supported feature
auto entropyFp = MnemonicEntropyFingerprint(mnemonicEntropy);
if (!EncryptSecret(vMasterKeyIn, mnemonicEntropy, entropyFp, vchCryptedSecret)) {
return false;
}
// Virtual: calls into CWallet to store the crypted entropy to disk.
if (!SetCryptedMnemonicEntropy(entropyFp, vchCryptedSecret)) {
return false;
}
}
// Drop the plaintext. swap() rather than `= RawHDSeed()`: assigning a
// shorter vector destroys the elements but KEEPS the capacity, so the
// old bytes would linger in the locked buffer. swap() hands the buffer
// to a temporary whose destructor deallocates it, and
// secure_allocator::deallocate memory_cleanse()s
// (support/allocators/secure.h:45-52). The `hdSeed = HDSeed();` above
// has the same weakness but cannot be fixed here: HDSeed's raw vector
// is private with no swap accessor (zip32.h:23-33).
RawHDSeed().swap(mnemonicEntropy);
}
BOOST_FOREACH(KeyMap::value_type& mKey, mapKeys)
{
const CKey &key = mKey.second;

View File

@@ -138,6 +138,21 @@ public:
}
};
/** Keystore which keeps the private keys encrypted.
* It derives from the basic key store, which is used if no encryption is active.
*/
//! Fingerprint of a BIP39 entropy blob, computed exactly as HDSeed::Fingerprint
//! does (BLAKE2b, ZCASH_HD_SEED_FP_PERSONAL). It is an IV / integrity tag and a
//! wallet.dat record key -- never a key-derivation input. Declared here rather
//! than duplicated because three call sites must produce identical bytes:
//! CCryptoKeyStore::SetMnemonicEntropy, CCryptoKeyStore::EncryptKeys, and
//! CWallet::SetMnemonicEntropy (which keys the plaintext record with it).
//!
//! It takes a copy internally on purpose: HDSeed's constructor takes a NON-const
//! RawHDSeed& (zip32.h:28), so HDSeed(entropy).Fingerprint() does not compile
//! against a const reference or a member read from a const method.
uint256 MnemonicEntropyFingerprint(const RawHDSeed& entropy);
/** Keystore which keeps the private keys encrypted.
* It derives from the basic key store, which is used if no encryption is active.
*/
@@ -145,6 +160,10 @@ class CCryptoKeyStore : public CBasicKeyStore
{
private:
std::pair<uint256, std::vector<unsigned char>> cryptedHDSeed;
// Encrypted mnemonic entropy, shaped exactly like cryptedHDSeed above:
// .first is the entropy's fingerprint (AES IV + integrity tag on decrypt),
// .second is the ciphertext.
std::pair<uint256, std::vector<unsigned char>> cryptedMnemonicEntropy;
CryptedKeyMap mapCryptedKeys;
//CryptedSproutSpendingKeyMap mapCryptedSproutSpendingKeys;
CryptedSaplingSpendingKeyMap mapCryptedSaplingSpendingKeys;
@@ -194,6 +213,14 @@ public:
bool SetHDSeed(const HDSeed& seed);
bool HaveHDSeed() const;
bool GetHDSeed(HDSeed& seedOut) const;
//! Mnemonic entropy, mirroring the four HD-seed members above.
//! SetCryptedMnemonicEntropy MUST stay virtual for the same reason
//! SetCryptedHDSeed is: CWallet overrides it to persist the record, and
//! SetMnemonicEntropy() below reaches that override through the vtable.
virtual bool SetCryptedMnemonicEntropy(const uint256& entropyFp, const std::vector<unsigned char> &vchCryptedSecret);
bool SetMnemonicEntropy(const RawHDSeed& entropy);
bool HaveMnemonicEntropy() const;
bool GetMnemonicEntropy(RawHDSeed& entropyOut) const;
virtual bool AddCryptedKey(const CPubKey &vchPubKey, const std::vector<unsigned char> &vchCryptedSecret);
bool AddKeyPubKey(const CKey& key, const CPubKey &pubkey);

View File

@@ -1039,8 +1039,10 @@ UniValue z_exportmnemonic(const UniValue& params, bool fHelp, const CPubKey& myp
"\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"
"New wallets get a seed phrase by default (-usemnemonic=0 opts out);\n"
"wallets restored with -mnemonic have one too. Wallets created before\n"
"this feature, or from a raw -hdseed, have no phrase -- use\n"
"z_exportwallet for those. Requires the wallet be unlocked.\n"
"\nResult:\n"
"{\n"
" \"mnemonic\" : \"word1 ... word24\", (string) the BIP39 seed phrase\n"

View File

@@ -40,6 +40,7 @@
#include "coins.h"
#include "wallet/asyncrpcoperation_saplingconsolidation.h"
#include "wallet/asyncrpcoperation_sweep.h"
#include "wallet/asyncrpcoperation_autoshieldcoinbase.h"
#include <random>
#include <limits>
#include <thread>
@@ -555,6 +556,9 @@ void CWallet::ChainTip(const CBlockIndex *pindex,
if (fSweepEnabled) {
RunSaplingSweep(pindex->GetHeight());
}
if (fAutoShieldEnabled) {
RunAutoShieldCoinbase(pindex->GetHeight());
}
if (fTxDeleteEnabled) {
DeleteWalletTransactions(pindex);
}
@@ -581,7 +585,13 @@ void CWallet::RunSaplingSweep(int blockHeight) {
if (blockHeight == 0)
return;
AssertLockHeld(cs_wallet);
// Take cs_wallet ourselves: ChainTip (the notify-thread caller) does NOT
// hold it here, and we mutate fSweepRunning/nextSweep/saplingSweepOperationId
// and enqueue below. Matches RunSaplingConsolidation/RunAutoShieldCoinbase.
// (The old AssertLockHeld(cs_wallet) was a no-op in release builds and thus
// masked an unsynchronized mutation.) cs_wallet is recursive, so this is
// safe even on any path that already holds it.
LOCK(cs_wallet);
if (!fSweepEnabled) {
return;
}
@@ -604,6 +614,12 @@ void CWallet::RunSaplingSweep(int blockHeight) {
return;
}
//Don't Run While auto-shield is running.
if (fAutoShieldRunning) {
LogPrintf("%s: not sweeping since autoshield is currently running at height=%d\n", __func__, blockHeight);
return;
}
fSweepRunning = true;
std::shared_ptr<AsyncRPCQueue> q = getAsyncRPCQueue();
@@ -634,6 +650,12 @@ void CWallet::RunSaplingConsolidation(int blockHeight) {
return;
}
// Self-guard: an op is already in flight (nextConsolidation only advances
// when it completes). Don't cancel + re-enqueue a fresh op every block.
if (fConsolidationRunning) {
return;
}
LogPrintf("%s: consolidation enabled at blockHeight=%d fSweepRunning=%d\n", __func__, blockHeight, fSweepRunning );
if (fSweepRunning) {
@@ -641,7 +663,13 @@ void CWallet::RunSaplingConsolidation(int blockHeight) {
return;
}
if (fAutoShieldRunning) {
LogPrintf("%s: not consolidating since autoshield is currently running at height=%d\n", __func__, blockHeight);
return;
}
LogPrintf("%s: creating consolidation operation at blockHeight=%d\n", __func__, blockHeight);
fConsolidationRunning = true;
std::shared_ptr<AsyncRPCQueue> q = getAsyncRPCQueue();
std::shared_ptr<AsyncRPCOperation> lastOperation = q->getOperationForId(saplingConsolidationOperationId);
if (lastOperation != nullptr) {
@@ -653,6 +681,60 @@ void CWallet::RunSaplingConsolidation(int blockHeight) {
q->addOperation(operation);
}
// Periodically drain matured transparent coinbase into a wallet-owned Sapling
// z-address. Default-ON but conditional: this is enqueue-only (all gathering
// happens on the async worker thread inside the op, which is why we must not
// take cs_main here — ChainTip runs from the wallet-notify context). It is a
// silent no-op wherever it cannot act (locked wallet, no owned coinbase,
// external -mineraddress), so it is safe to run on every node.
void CWallet::RunAutoShieldCoinbase(int blockHeight) {
// Sapling is always active from height 1 on DragonX+HACs.
if (blockHeight == 0)
return;
LOCK(cs_wallet);
if (!fAutoShieldEnabled) {
return;
}
if (nextAutoShield > blockHeight) {
return;
}
// Self-guard: an op is already in flight (nextAutoShield only advances when
// it completes). Don't cancel + re-enqueue a fresh op every block.
if (fAutoShieldRunning) {
return;
}
// Mutual exclusion: sweep/consolidation share the single async worker and
// cs_wallet; don't queue an autoshield in the same connected block.
if (fSweepRunning || fConsolidationRunning) {
LogPrintf("%s: not autoshielding since sweep/consolidation is running at height=%d\n", __func__, blockHeight);
return;
}
// Silent no-op while locked: we can neither sign the shield nor derive a
// destination z-addr. Advance the interval so we don't retry every block.
if (IsLocked()) {
LogPrintf("%s: wallet locked; matured coinbase will accumulate until unlocked (height=%d)\n", __func__, blockHeight);
nextAutoShield = autoShieldInterval + blockHeight;
return;
}
fAutoShieldRunning = true;
std::shared_ptr<AsyncRPCQueue> q = getAsyncRPCQueue();
std::shared_ptr<AsyncRPCOperation> lastOperation = q->getOperationForId(saplingAutoShieldOperationId);
if (lastOperation != nullptr) {
lastOperation->cancel();
}
std::shared_ptr<AsyncRPCOperation> operation(new AsyncRPCOperation_autoshieldcoinbase(blockHeight + 5));
saplingAutoShieldOperationId = operation->getId();
q->addOperation(operation);
}
bool CWallet::CommitAutomatedTx(const CTransaction& tx) {
CWalletTx wtx(this, tx);
CReserveKey reservekey(pwalletMain);
@@ -2388,30 +2470,47 @@ void CWallet::GenerateNewSeed()
// 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)) {
//
// NO SILENT FALLBACK. Falling back to a random seed here produced a wallet
// that looks mnemonic-capable but whose words can never be exported
// (z_exportmnemonic refuses non-mnemonic wallets, rpcdump.cpp:1031+) and
// that no seed phrase can restore. A user who asked for -usemnemonic must
// get that or a hard failure.
if (GetBoolArg("-usemnemonic", true)) {
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__);
if (!GenerateMnemonicEntropy(256, entropy))
throw std::runtime_error(std::string(__func__) + ": -usemnemonic entropy generation failed");
// Store the EXPANDED 64-byte BIP39 seed as the HD seed, with
// fMnemonic = false. Every binary -- old or new -- then feeds the stored
// bytes straight into derivation, so the key tree is identical
// everywhere and no CHDChain version bump or minversion fence is needed.
// The 32-byte entropy is kept in a separate, display-only record purely
// so the phrase can be reprinted. Addresses are unchanged from the
// previous format, which expanded the stored entropy on every read.
RawHDSeed seed64;
if (!Bip39SeedFromEntropy(entropy, seed64))
throw std::runtime_error(std::string(__func__) + ": BIP39 seed expansion failed");
HDSeed seed(seed64);
// ORDER IS LOAD-BEARING: seed first, entropy second, never the reverse.
// A crash between the two leaves a wallet with a seed and no phrase --
// recoverable via z_exportwallet, merely inconvenient. The reverse order
// would leave entropy with no seed; the next start would mint a
// DIFFERENT seed while the wallet still held a phrase for the old one.
// (GetMnemonicPhrase cross-checks the two and would refuse to print it,
// but do not rely on that here.)
if (!InstallHDSeed(seed, false, nCreationTime))
throw std::runtime_error(std::string(__func__) + ": installing the mnemonic HD seed failed");
if (!SetMnemonicEntropy(entropy))
throw std::runtime_error(std::string(__func__) + ": storing the mnemonic entropy failed");
return;
}
auto seed = HDSeed::Random(HD_WALLET_SEED_LENGTH);
// If the wallet is encrypted and locked, this will fail.
if (!SetHDSeed(seed))
auto seed = HDSeed::Random(HD_WALLET_SEED_LENGTH);
if (!InstallHDSeed(seed, false, nCreationTime))
throw std::runtime_error(std::string(__func__) + ": SetHDSeed failed");
// store the key creation time together with
// the child index counter in the database
// as a hdchain object
CHDChain newHdChain;
newHdChain.nVersion = CHDChain::VERSION_HD_TRANSPARENT;
newHdChain.seedFp = seed.Fingerprint();
newHdChain.nCreateTime = nCreationTime;
SetHDChain(newHdChain, false);
}
bool CWallet::SetHDSeed(const HDSeed& seed)
@@ -2445,14 +2544,38 @@ bool CWallet::SetCryptedHDSeed(const uint256& seedFp, const std::vector<unsigned
{
LOCK(cs_wallet);
if (pwalletdbEncryption)
return pwalletdbEncryption->WriteCryptedHDSeed(seedFp, vchCryptedSecret);
else
return CWalletDB(strWalletFile).WriteCryptedHDSeed(seedFp, vchCryptedSecret);
// Write the encrypted record, then drop the plaintext one. Both go
// through the same CWalletDB (and therefore the same transaction when
// EncryptWallet supplied pwalletdbEncryption), because CDB::Rewrite at
// the end of EncryptWallet copies every surviving record into the fresh
// file -- a leftover plaintext "hdseed" would keep the unencrypted seed
// on disk for the life of the wallet.
//
// The erase is deliberately best-effort: a hard failure here propagates
// into CCryptoKeyStore::EncryptKeys, which CWallet::EncryptWallet turns
// into assert(false) with half the keys encrypted in memory. A logged
// warning is strictly better than that.
if (pwalletdbEncryption) {
if (!pwalletdbEncryption->WriteCryptedHDSeed(seedFp, vchCryptedSecret))
return false;
if (!pwalletdbEncryption->EraseHDSeed(seedFp))
LogPrintf("%s: WARNING: could not erase the plaintext hdseed record; "
"the unencrypted HD seed may remain in wallet.dat\n", __func__);
return true;
} else {
CWalletDB walletdb(strWalletFile);
if (!walletdb.WriteCryptedHDSeed(seedFp, vchCryptedSecret))
return false;
if (!walletdb.EraseHDSeed(seedFp))
LogPrintf("%s: WARNING: could not erase the plaintext hdseed record; "
"the unencrypted HD seed may remain in wallet.dat\n", __func__);
return true;
}
}
return false;
}
void CWallet::SetHDChain(const CHDChain& chain, bool memonly)
{
LOCK(cs_wallet);
@@ -2462,6 +2585,20 @@ void CWallet::SetHDChain(const CHDChain& chain, bool memonly)
hdChain = chain;
}
void CWallet::SetHDSeedOrigin(int origin)
{
LOCK(cs_wallet);
hdSeedOrigin = origin;
// Deliberately non-fatal, unlike SetHDChain: losing this record must never
// stop a node from starting. The cost of a failed write is that the next
// start re-classifies the wallet, and re-classification of an already-seeded
// wallet yields HDSEED_ORIGIN_UNKNOWN, i.e. the safe answer.
if (fFileBacked && !CWalletDB(strWalletFile).WriteHDSeedOrigin((int64_t)origin))
LogPrintf("%s: WARNING: could not record HD seed origin %d in wallet.dat\n", __func__, origin);
}
bool CWallet::LoadHDSeed(const HDSeed& seed)
{
return CBasicKeyStore::SetHDSeed(seed);
@@ -2471,21 +2608,94 @@ bool CWallet::LoadCryptedHDSeed(const uint256& seedFp, const std::vector<unsigne
{
return CCryptoKeyStore::SetCryptedHDSeed(seedFp, seed);
}
bool CWallet::SetMnemonicEntropy(const RawHDSeed& entropy)
{
if (!CCryptoKeyStore::SetMnemonicEntropy(entropy)) {
return false;
}
if (!fFileBacked) {
return true;
}
{
LOCK(cs_wallet);
if (!IsCrypted()) {
// Keyed by fingerprint exactly as "hdseed" is, so ReadKeyValue can
// integrity-check it and EraseMnemonicEntropy can find it later.
return CWalletDB(strWalletFile).WriteMnemonicEntropy(
MnemonicEntropyFingerprint(entropy), entropy);
}
}
return true;
}
bool CWallet::SetCryptedMnemonicEntropy(const uint256& entropyFp, const std::vector<unsigned char>& vchCryptedSecret)
{
if (!CCryptoKeyStore::SetCryptedMnemonicEntropy(entropyFp, vchCryptedSecret)) {
return false;
}
if (!fFileBacked) {
return true;
}
{
LOCK(cs_wallet);
// Same write-then-erase discipline as SetCryptedHDSeed: CDB::Rewrite at
// the end of EncryptWallet copies every surviving record, so a leftover
// plaintext "mnementropy" would keep the seed phrase recoverable from an
// encrypted wallet.dat. The erase is best-effort for the same reason: a
// hard failure would propagate into EncryptKeys -> assert(false).
if (pwalletdbEncryption) {
if (!pwalletdbEncryption->WriteCryptedMnemonicEntropy(entropyFp, vchCryptedSecret))
return false;
if (!pwalletdbEncryption->EraseMnemonicEntropy(entropyFp))
LogPrintf("%s: WARNING: could not erase the plaintext mnementropy record\n", __func__);
return true;
} else {
CWalletDB walletdb(strWalletFile);
if (!walletdb.WriteCryptedMnemonicEntropy(entropyFp, vchCryptedSecret))
return false;
if (!walletdb.EraseMnemonicEntropy(entropyFp))
LogPrintf("%s: WARNING: could not erase the plaintext mnementropy record\n", __func__);
return true;
}
}
return false;
}
bool CWallet::LoadMnemonicEntropy(const RawHDSeed& entropy)
{
return CBasicKeyStore::SetMnemonicEntropy(entropy);
}
bool CWallet::LoadCryptedMnemonicEntropy(const uint256& entropyFp, const std::vector<unsigned char>& vchCryptedSecret)
{
return CCryptoKeyStore::SetCryptedMnemonicEntropy(entropyFp, vchCryptedSecret);
}
bool CWallet::InstallHDSeed(const HDSeed& seed, bool fMnemonic, int64_t nCreateTime)
{
AssertLockHeld(cs_wallet);
if (!SetHDSeed(seed))
return false;
// Chain BEFORE seed. A crash between the two records must never leave a
// wallet that holds a seed with no hdchain: on the next load hdChain would
// silently revert to its defaults, clearing fMnemonicSeed (which switches
// the derivation input, wallet.cpp:2615-2633) and resetting
// saplingAccountCounter. The opposite torn state — chain without seed — is
// harmless and self-healing: HaveHDSeed() is false, so init installs a seed
// again and overwrites the chain.
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);
SetHDChain(newHdChain, false); // throws if the write fails
if (!SetHDSeed(seed))
return false;
return true;
}
@@ -2524,10 +2734,29 @@ bool CWallet::SetHDSeedFromMnemonic(const std::string& phrase)
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
// Store the EXPANDED 64-byte BIP39 seed as the HD seed (fMnemonic = false);
// the entropy goes in its own record and is used only to reprint the phrase.
// Derivation therefore reads the stored bytes directly on any binary, and
// the resulting addresses are byte-identical to the previous format, which
// expanded the stored entropy on every derivation. SilentDragonXLite
// interop is unaffected: the same words still yield the same seed64.
RawHDSeed seed64;
if (!Bip39SeedFromEntropy(entropy, seed64))
return false;
HDSeed seed(seed64);
// Seed first, entropy second -- see the ordering note in GenerateNewSeed.
if (!InstallHDSeed(seed, false, 1)) // birthday = genesis for a restore
return false;
// Non-fatal on a restore, unlike GenerateNewSeed: the user already holds the
// phrase (they just typed it), the seed is installed and the wallet is fully
// functional; only z_exportmnemonic is lost.
if (!SetMnemonicEntropy(entropy)) {
LogPrintf("%s: WARNING: HD seed installed but the mnemonic entropy record could not be "
"stored; z_exportmnemonic will be unavailable on this wallet\n", __func__);
}
return true;
}
bool CWallet::GetHDSeedForDerivation(HDSeed& seedOut) const
@@ -2552,6 +2781,36 @@ bool CWallet::GetHDSeedForDerivation(HDSeed& seedOut) const
bool CWallet::GetMnemonicPhrase(std::string& phraseOut) const
{
// Preferred form: the HD seed is the EXPANDED 64-byte BIP39 seed and the
// entropy sits in its own record.
RawHDSeed entropy;
if (GetMnemonicEntropy(entropy)) { // false on an encrypted+locked wallet
// NEVER hand out a phrase that does not restore THIS wallet. Prove the
// entropy expands to the exact bytes derivation consumes; if it does
// not (a torn install, a wallet.dat edited by hand, an entropy record
// paired with a different seed), refuse rather than print a phrase that
// silently restores someone else's key tree. Costs one PBKDF2 on a
// user-initiated RPC.
RawHDSeed seed64;
if (!Bip39SeedFromEntropy(entropy, seed64))
return false;
HDSeed derivationSeed;
if (!GetHDSeedForDerivation(derivationSeed))
return false;
if (derivationSeed.RawSeed() != seed64) {
LogPrintf("%s: refusing to export a seed phrase: the stored mnemonic entropy does not "
"expand to this wallet's HD seed\n", __func__);
return false;
}
return EntropyToMnemonic(entropy, phraseOut);
}
// Legacy form (earlier builds of this branch): the stored HD seed IS the
// 32-byte BIP39 entropy, expanded on every derivation. Consistent by
// construction, so no cross-check is possible or needed.
if (!hdChain.fMnemonicSeed)
return false;

View File

@@ -784,10 +784,8 @@ private:
TxNullifiers mapTxSaplingNullifiers;
std::vector<CTransaction> pendingSaplingConsolidationTxs;
AsyncRPCOperationId saplingConsolidationOperationId;
std::vector<CTransaction> pendingSaplingSweepTxs;
AsyncRPCOperationId saplingSweepOperationId;
void AddToTransparentSpends(const COutPoint& outpoint, const uint256& wtxid);
void AddToSaplingSpends(const uint256& nullifier, const uint256& wtxid);
@@ -802,6 +800,9 @@ public:
int64_t nWitnessCacheSize;
bool needsRescan = false;
int nextConsolidation = 0;
// Id of the in-flight consolidation op; read by the op to confirm it is
// still the current one before mutating scheduler state.
AsyncRPCOperationId saplingConsolidationOperationId;
bool fSaplingConsolidationEnabled = false;
bool fConsolidationRunning = false;
@@ -809,6 +810,12 @@ public:
bool fSweepExternalEnabled = false;
bool fSweepRunning = false;
// Automatic coinbase shielding (t->z). Default ON but conditional: it is a
// silent no-op on nodes where it cannot act (no wallet, external
// -mineraddress, non-mining, or locked wallet). See RunAutoShieldCoinbase.
bool fAutoShieldEnabled = true;
bool fAutoShieldRunning = false;
std::atomic<bool> fAbortRescan{false};
// abort current rescan
void AbortRescan() { fAbortRescan = true; }
@@ -823,6 +830,9 @@ public:
int rescanStartHeight = 0;
int nextSweep = 0;
// Id of the in-flight sweep op; read by the op to confirm it is still the
// current one before mutating scheduler state.
AsyncRPCOperationId saplingSweepOperationId;
int amountSwept = 0;
int amountConsolidated = 0;
int sweepInterval = 10;
@@ -833,6 +843,31 @@ public:
std::vector<std::string> sweepExcludeAddresses;
std::string consolidationAddress = "";
int nextAutoShield = 0;
int autoShieldInterval = 25;
CAmount autoShieldFee = 10000;
// Minimum matured coinbase UTXOs before a round fires, to avoid per-interval
// fee churn on a single freshly-matured reward.
int autoShieldMinUtxos = 1;
// Configured destination z-addr override; also used to cache the resolved
// wallet-owned destination so we keep reusing one address.
std::string autoShieldAddress = "";
// Id of the in-flight autoshield op; read by the op to confirm it is still
// the current one before mutating scheduler state.
AsyncRPCOperationId saplingAutoShieldOperationId;
// Provenance of this wallet's HD seed, recorded once in wallet.dat the
// first time a build that knows about it opens the wallet. Features that
// move funds into addresses only the seed can re-derive must not turn
// themselves ON by default unless the user can actually restore that seed.
enum HDSeedOrigin {
HDSEED_ORIGIN_UNRECORDED = 0, // no record in wallet.dat yet
HDSEED_ORIGIN_CREATED = 1, // minted onto a brand-new empty wallet
HDSEED_ORIGIN_RESTORED = 2, // user supplied -mnemonic / -hdseed
HDSEED_ORIGIN_RETROFIT = 3, // minted onto a pre-existing seedless wallet
HDSEED_ORIGIN_UNKNOWN = 4, // seed predates this record
};
int hdSeedOrigin = HDSEED_ORIGIN_UNRECORDED;
void ClearNoteWitnessCache();
int64_t NullifierCount();
@@ -1224,6 +1259,7 @@ public:
const CBlock *pblock,
boost::optional<std::pair<SproutMerkleTree, SaplingMerkleTree>> added);
void RunSaplingConsolidation(int blockHeight);
void RunAutoShieldCoinbase(int blockHeight);
bool CommitAutomatedTx(const CTransaction& tx);
/** Saves witness caches and best block locator to disk. */
void SetBestChain(const CBlockLocator& loc);
@@ -1309,6 +1345,14 @@ public:
bool SetHDSeed(const HDSeed& seed);
bool SetCryptedHDSeed(const uint256& seedFp, const std::vector<unsigned char> &vchCryptedSecret);
/* Record this wallet's BIP39 entropy so its seed phrase can be reprinted.
Display-only: derivation never reads it (the HD seed holds the bytes that
are actually derived from). Refuses to replace an existing record.
SetCryptedMnemonicEntropy overrides the CCryptoKeyStore virtual so the
record reaches disk; SetMnemonicEntropy merely hides the base version,
which is safe because no call site holds a base pointer. */
bool SetMnemonicEntropy(const RawHDSeed& entropy);
bool SetCryptedMnemonicEntropy(const uint256& entropyFp, 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
@@ -1327,8 +1371,14 @@ public:
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; }
/* True if this wallet has a BIP39 seed phrase available. Two storage forms
qualify:
- current: the HD seed is the EXPANDED 64-byte BIP39 seed and the
entropy lives in its own record (HaveMnemonicEntropy());
- legacy: hdChain.fMnemonicSeed, where the stored HD seed IS the
32-byte entropy and is expanded on every derivation.
Gates z_exportmnemonic (rpcdump.cpp). */
bool IsMnemonicSeed() const { return hdChain.fMnemonicSeed || HaveMnemonicEntropy(); }
/* 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
@@ -1349,11 +1399,22 @@ public:
void SetHDChain(const CHDChain& chain, bool memonly);
const CHDChain& GetHDChain() const { return hdChain; }
/* Record (in memory and in wallet.dat) how this wallet's HD seed came to
exist. Best-effort: a failed write is logged, not fatal — the next start
simply re-classifies, and re-classification always errs toward
HDSEED_ORIGIN_UNKNOWN, which is the conservative answer. */
void SetHDSeedOrigin(int origin);
/* Set the current HD seed, without saving it to disk (used by LoadWallet) */
bool LoadHDSeed(const HDSeed& key);
/* Set the current encrypted HD seed, without saving it to disk (used by LoadWallet) */
bool LoadCryptedHDSeed(const uint256& seedFp, const std::vector<unsigned char>& seed);
/* Set the mnemonic entropy, without saving it to disk (used by LoadWallet) */
bool LoadMnemonicEntropy(const RawHDSeed& entropy);
/* Set the encrypted mnemonic entropy, without saving it to disk (used by LoadWallet) */
bool LoadCryptedMnemonicEntropy(const uint256& entropyFp, const std::vector<unsigned char>& vchCryptedSecret);
/* Find notes filtered by payment address, min depth, ability to spend */
void GetFilteredNotes(std::vector<SaplingNoteEntry>& saplingEntries,

View File

@@ -216,6 +216,12 @@ bool CWalletDB::WriteWitnessCacheSize(int64_t nWitnessCacheSize)
return Write(std::string("witnesscachesize"), nWitnessCacheSize);
}
bool CWalletDB::WriteHDSeedOrigin(int64_t nOrigin)
{
nWalletDBUpdated++;
return Write(std::string("hdseedorigin"), nOrigin);
}
bool CWalletDB::ReadPool(int64_t nPool, CKeyPool& keypool)
{
return Read(std::make_pair(std::string("pool"), nPool), keypool);
@@ -403,12 +409,15 @@ public:
bool fAnyUnordered;
int nFileVersion;
vector<uint256> vWalletUpgrade;
// True once a well-formed "hdchain" record has been loaded.
bool fHDChainRead;
CWalletScanState() {
nKeys = nCKeys = nKeyMeta = nZKeys = nCZKeys = nZKeyMeta = nSapZAddrs = 0;
fIsEncrypted = false;
fAnyUnordered = false;
nFileVersion = 0;
fHDChainRead = false;
}
};
@@ -833,9 +842,56 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
else if (strType == "hdchain")
{
CHDChain chain;
ssValue >> chain;
try {
ssValue >> chain;
} catch (...) {
// Do not let this land in the "user can live with it" bucket:
// report it, and leave wss.fHDChainRead false so LoadWallet
// turns it into DB_CORRUPT when a seed is present.
strErr = "Error reading wallet database: hdchain record is corrupt";
return false;
}
wss.fHDChainRead = true;
pwallet->SetHDChain(chain, true);
}
else if (strType == "hdseedorigin")
{
int64_t nOrigin = 0;
ssValue >> nOrigin;
pwallet->hdSeedOrigin = (int)nOrigin;
}
else if (strType == "mnementropy")
{
uint256 entropyFp;
RawHDSeed entropy;
ssKey >> entropyFp;
ssValue >> entropy;
if (MnemonicEntropyFingerprint(entropy) != entropyFp)
{
strErr = "Error reading wallet database: mnemonic entropy corrupt";
return false;
}
if (!pwallet->LoadMnemonicEntropy(entropy))
{
strErr = "Error reading wallet database: LoadMnemonicEntropy failed";
return false;
}
}
else if (strType == "cmnementropy")
{
uint256 entropyFp;
vector<unsigned char> vchCryptedSecret;
ssKey >> entropyFp;
ssValue >> vchCryptedSecret;
if (!pwallet->LoadCryptedMnemonicEntropy(entropyFp, vchCryptedSecret))
{
strErr = "Error reading wallet database: LoadCryptedMnemonicEntropy failed";
return false;
}
wss.fIsEncrypted = true;
}
} catch (...)
{
return false;
@@ -847,6 +903,10 @@ static bool IsKeyType(string strType)
{
return (strType == "key" || strType == "wkey" ||
strType == "hdseed" || strType == "chdseed" ||
// The mnemonic entropy must survive a keys-only salvage: without it
// a recovered wallet keeps its seed (and stays fully spendable) but
// silently loses the ability to reprint its seed phrase.
strType == "mnementropy" || strType == "cmnementropy" ||
strType == "zkey" || strType == "czkey" ||
strType == "sapzkey" || strType == "csapzkey" ||
strType == "vkey" ||
@@ -947,6 +1007,21 @@ DBErrors CWalletDB::LoadWallet(CWallet* pwallet)
if (fNoncriticalErrors && result == DB_LOAD_OK)
result = DB_NONCRITICAL_ERROR;
// A wallet that holds an HD seed but whose hdchain record is missing or
// unreadable is NOT safe to run. hdChain would fall back to its SetNull
// defaults (walletdb.h:105-113), which (a) clears fMnemonicSeed, switching
// HD derivation from the 64-byte BIP39 seed to the raw 32-byte entropy
// (CWallet::GetHDSeedForDerivation, wallet.cpp:2615-2633) -> an entirely
// different key tree, and (b) resets saplingAccountCounter to 0, so the
// next GenerateNewSaplingZKey walks back over accounts that already exist.
// Both are silent today (a bad hdchain read is only DB_NONCRITICAL_ERROR).
// Fail loud instead of quietly deriving into the wrong tree.
if (pwallet->HaveHDSeed() && !wss.fHDChainRead)
{
LogPrintf("Error loading wallet.dat: HD seed present but the hdchain record is missing or corrupt\n");
return DB_CORRUPT;
}
// Any wallet corruption at all: skip any rewriting or
// upgrading, we don't want to make it worse.
if (result != DB_LOAD_OK)
@@ -1240,7 +1315,13 @@ bool CWalletDB::Recover(CDBEnv& dbenv, const std::string& filename, bool fOnlyKe
fReadOK = ReadKeyValue(&dummyWallet, ssKey, ssValue,
wss, strType, strErr);
}
if (!IsKeyType(strType))
// "hdchain" is not a key type, but it must survive a keys-only
// salvage: a recovered wallet that keeps its seed while losing its
// hdchain silently derives from a different key tree (fMnemonicSeed
// cleared -> raw entropy instead of the 64-byte BIP39 seed) and
// re-issues sapling accounts from 0. CWalletDB::LoadWallet now
// refuses such a wallet outright, so preserve the record here.
if (!IsKeyType(strType) && strType != "hdchain")
continue;
if (!fReadOK)
{
@@ -1290,6 +1371,34 @@ bool CWalletDB::WriteCryptedHDSeed(const uint256& seedFp, const std::vector<unsi
return Write(std::make_pair(std::string("chdseed"), seedFp), vchCryptedSecret);
}
bool CWalletDB::EraseHDSeed(const uint256& seedFp)
{
nWalletDBUpdated++;
// CDB::Erase honours activeTxn, so when this runs inside EncryptWallet's
// transaction the erase commits or aborts atomically with the chdseed write.
// It also returns true for DB_NOTFOUND, so erasing a record that was never
// written (e.g. a wallet encrypted at creation time) is not a failure.
return Erase(std::make_pair(std::string("hdseed"), seedFp));
}
bool CWalletDB::WriteMnemonicEntropy(const uint256& entropyFp, const RawHDSeed& entropy)
{
nWalletDBUpdated++;
return Write(std::make_pair(std::string("mnementropy"), entropyFp), entropy);
}
bool CWalletDB::WriteCryptedMnemonicEntropy(const uint256& entropyFp, const std::vector<unsigned char>& vchCryptedSecret)
{
nWalletDBUpdated++;
return Write(std::make_pair(std::string("cmnementropy"), entropyFp), vchCryptedSecret);
}
bool CWalletDB::EraseMnemonicEntropy(const uint256& entropyFp)
{
nWalletDBUpdated++;
return Erase(std::make_pair(std::string("mnementropy"), entropyFp));
}
bool CWalletDB::WriteHDChain(const CHDChain& chain)
{
nWalletDBUpdated++;

View File

@@ -191,6 +191,9 @@ public:
bool WriteWitnessCacheSize(int64_t nWitnessCacheSize);
//! Record how this wallet's HD seed came to exist (CWallet::HDSeedOrigin).
bool WriteHDSeedOrigin(int64_t nOrigin);
bool ReadPool(int64_t nPool, CKeyPool& keypool);
bool WritePool(int64_t nPool, const CKeyPool& keypool);
bool ErasePool(int64_t nPool);
@@ -219,6 +222,17 @@ public:
bool WriteHDSeed(const HDSeed& seed);
bool WriteCryptedHDSeed(const uint256& seedFp, const std::vector<unsigned char>& vchCryptedSecret);
//! Remove the PLAINTEXT hdseed record. Must be called once the seed has been
//! written in encrypted form: CDB::Rewrite (invoked at the end of
//! CWallet::EncryptWallet) copies whatever records still exist into the new
//! file, so a leftover "hdseed" leaves the unencrypted seed on disk forever.
bool EraseHDSeed(const uint256& seedFp);
//! BIP39 entropy for a phrase-recoverable wallet. Display-only: derivation
//! never reads it. Record names are deliberately distinct prefixes from
//! "hdseed"/"chdseed" so they cannot collide.
bool WriteMnemonicEntropy(const uint256& entropyFp, const RawHDSeed& entropy);
bool WriteCryptedMnemonicEntropy(const uint256& entropyFp, const std::vector<unsigned char>& vchCryptedSecret);
bool EraseMnemonicEntropy(const uint256& entropyFp);
//! write the hdchain model (external chain child index counter)
bool WriteHDChain(const CHDChain& chain);