wallet: add default-on auto-shield-coinbase; drain miner coinbase into a z-addr

On this ac_private=1 chain miners accumulate one transparent coinbase UTXO per
block (the only transparent output the chain permits). It had to be shielded
manually via z_shieldcoinbase, and left unshielded it is the sole persistent
metadata leak on the chain and the source of miner UTXO-fragmentation send
failures.

Add AsyncRPCOperation_autoshieldcoinbase: a periodic, default-on wallet op
driven from CWallet::ChainTip alongside sweep/consolidation, draining matured
coinbase into a wallet-owned Sapling z-address in size-bounded batches.

- Enqueue-only driver (RunAutoShieldCoinbase): takes only cs_wallet in the
  notify context; all gathering runs on the async worker under LOCK2(cs_main,
  cs_wallet).
- Dedicated op (not a reuse of z_shieldcoinbase) so it never toggles mining.
- Default-ON but conditional: silent no-op on -disablewallet, external
  -mineraddress, non-mining, or locked wallets (explicit IsLocked() guard).
- Destination is reuse-then-create; -autoshieldaddress override is
  spend-key-validated at init so funds cannot be stranded.
- Sweep-model bookkeeping: advances nextAutoShield and clears the running flag
  on every terminal state; an op-id guard stops a stale op clobbering scheduler
  state; a self-guard stops cancel/re-enqueue churn.
- Sietch-padded output shape matches manual z_shieldcoinbase txns.

Config: -autoshield (default true), -autoshieldinterval, -autoshieldaddress,
-autoshieldfee (range-validated), -autoshieldminutxos.

Incorporates fixes from an 8-angle code review: CAmount fee type with init-time
range validation, op-id-guarded flag bookkeeping, driver self-guard, and a
tipHeight-consistent NU-activation guard.

Note: the fConsolidationRunning / nextConsolidation consolidation-scheduler
wedge is a pre-existing bug, left for a separate change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-21 02:06:09 -05:00
co-authored by Claude Opus 4.8
parent 1c3523aac1
commit 2ebbbc777c
6 changed files with 522 additions and 0 deletions
+72
View File
@@ -40,6 +40,7 @@
#include "coins.h"
#include "wallet/asyncrpcoperation_saplingconsolidation.h"
#include "wallet/asyncrpcoperation_sweep.h"
#include "wallet/asyncrpcoperation_autoshieldcoinbase.h"
#include <random>
#include <limits>
#include <thread>
@@ -555,6 +556,9 @@ void CWallet::ChainTip(const CBlockIndex *pindex,
if (fSweepEnabled) {
RunSaplingSweep(pindex->GetHeight());
}
if (fAutoShieldEnabled) {
RunAutoShieldCoinbase(pindex->GetHeight());
}
if (fTxDeleteEnabled) {
DeleteWalletTransactions(pindex);
}
@@ -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<AsyncRPCQueue> 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<AsyncRPCQueue> q = getAsyncRPCQueue();
std::shared_ptr<AsyncRPCOperation> 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<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);