Compare commits
13 Commits
v1.1.0
...
autoshield
| Author | SHA1 | Date | |
|---|---|---|---|
| 9734402d7b | |||
| 20b2cbe830 | |||
| 2d7dd90c55 | |||
| 3caec548ae | |||
| 2a7fdcc1db | |||
| 143c33de48 | |||
| e2f88175ab | |||
| a494eabdce | |||
| dad162a89a | |||
| bb292a89cc | |||
| 4d72e5fc30 | |||
| ca730a5d98 | |||
| 2ebbbc777c |
@@ -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 \
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
110
src/init.cpp
110
src/init.cpp
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
407
src/wallet/asyncrpcoperation_autoshieldcoinbase.cpp
Normal file
407
src/wallet/asyncrpcoperation_autoshieldcoinbase.cpp
Normal 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;
|
||||
}
|
||||
58
src/wallet/asyncrpcoperation_autoshieldcoinbase.h
Normal file
58
src/wallet/asyncrpcoperation_autoshieldcoinbase.h
Normal 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 */
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user