diff --git a/src/Makefile.am b/src/Makefile.am index 86d199a52..1ba627e21 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -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 \ diff --git a/src/init.cpp b/src/init.cpp index a359c7200..89243cf1e 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -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 wallet z-address (default: true). 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=", _("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 blocks during inital block download (default: %i)"), DEFAULT_TX_DELETE_INTERVAL)); strUsage += HelpMessageOpt("-keeptxnum", strprintf(_("Keep the last transactions (default: %i)"), DEFAULT_TX_RETENTION_LASTTX)); @@ -2451,6 +2457,55 @@ 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. + pwalletMain->fAutoShieldEnabled = GetBoolArg("-autoshield", true); + 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(&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); diff --git a/src/wallet/asyncrpcoperation_autoshieldcoinbase.cpp b/src/wallet/asyncrpcoperation_autoshieldcoinbase.cpp new file mode 100644 index 000000000..60c9eda17 --- /dev/null +++ b/src/wallet/asyncrpcoperation_autoshieldcoinbase.cpp @@ -0,0 +1,315 @@ +// 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_)); +} + +// Enumerate wallet-owned Sapling addresses and pick a spendable one; if none +// exists, generate a fresh one (needs an unlocked wallet, which the caller has +// already ensured). Caller must hold cs_wallet. +bool AsyncRPCOperation_autoshieldcoinbase::resolveDestination( + libzcash::SaplingPaymentAddress& destOut, std::string& destStrOut) { + + // 1. Explicit -autoshieldaddress override (validated + spend-key-checked at init) + if (!pwalletMain->autoShieldAddress.empty()) { + auto decoded = DecodePaymentAddress(pwalletMain->autoShieldAddress); + if (boost::get(&decoded) != nullptr) { + destOut = boost::get(decoded); + destStrOut = pwalletMain->autoShieldAddress; + return true; + } + LogPrintf("%s: configured -autoshieldaddress is not a valid Sapling address\n", getId()); + return false; + } + + // 2. Reuse the first spendable wallet-owned Sapling address (std::set order + // is deterministic, so this is stable across rounds/restarts). + std::set addrs; + pwalletMain->GetSaplingPaymentAddresses(addrs); + for (const auto& a : addrs) { + libzcash::SaplingExtendedSpendingKey extsk; + if (pwalletMain->GetSaplingExtendedSpendingKey(a, extsk)) { + destOut = a; + destStrOut = EncodePaymentAddress(a); + // Cache it so we keep reusing the same address. + pwalletMain->autoShieldAddress = destStrOut; + return true; + } + } + + // 3. No spendable z-addr yet: create one (requires unlocked wallet / HD seed). + if (pwalletMain->IsLocked()) { + return false; + } + try { + destOut = pwalletMain->GenerateNewSaplingZKey(); + destStrOut = EncodePaymentAddress(destOut); + pwalletMain->autoShieldAddress = destStrOut; + LogPrintf("%s: generated new autoshield destination z-address %s\n", getId(), destStrOut); + 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 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 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(&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(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; +} diff --git a/src/wallet/asyncrpcoperation_autoshieldcoinbase.h b/src/wallet/asyncrpcoperation_autoshieldcoinbase.h new file mode 100644 index 000000000..98dd5c734 --- /dev/null +++ b/src/wallet/asyncrpcoperation_autoshieldcoinbase.h @@ -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 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 */ diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index c762b0b97..cab7b4373 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -40,6 +40,7 @@ #include "coins.h" #include "wallet/asyncrpcoperation_saplingconsolidation.h" #include "wallet/asyncrpcoperation_sweep.h" +#include "wallet/asyncrpcoperation_autoshieldcoinbase.h" #include #include #include @@ -555,6 +556,9 @@ void CWallet::ChainTip(const CBlockIndex *pindex, if (fSweepEnabled) { RunSaplingSweep(pindex->GetHeight()); } + if (fAutoShieldEnabled) { + RunAutoShieldCoinbase(pindex->GetHeight()); + } if (fTxDeleteEnabled) { DeleteWalletTransactions(pindex); } @@ -604,6 +608,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 q = getAsyncRPCQueue(); @@ -641,6 +651,11 @@ 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); std::shared_ptr q = getAsyncRPCQueue(); std::shared_ptr lastOperation = q->getOperationForId(saplingConsolidationOperationId); @@ -653,6 +668,63 @@ 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. + // NOTE: fConsolidationRunning is currently never set true (a pre-existing + // consolidation-scheduler bug); this half of the guard only becomes + // effective once that is fixed on the consolidation side. + 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 q = getAsyncRPCQueue(); + std::shared_ptr lastOperation = q->getOperationForId(saplingAutoShieldOperationId); + if (lastOperation != nullptr) { + lastOperation->cancel(); + } + std::shared_ptr 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); diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index a99c0a4ea..6018b0df1 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -809,6 +809,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 fAbortRescan{false}; // abort current rescan void AbortRescan() { fAbortRescan = true; } @@ -833,6 +839,19 @@ public: std::vector 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; + void ClearNoteWitnessCache(); int64_t NullifierCount(); @@ -1224,6 +1243,7 @@ public: const CBlock *pblock, boost::optional> 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);