Compare commits
8 Commits
dev
...
feature/au
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d72e5fc30 | |||
| ca730a5d98 | |||
| 2ebbbc777c | |||
| 1c3523aac1 | |||
| d0657d38b4 | |||
| ddf6a35680 | |||
| 9cb6424799 | |||
| 1c6f64b87e |
@@ -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 \
|
||||
|
||||
55
src/init.cpp
55
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=<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));
|
||||
@@ -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<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);
|
||||
|
||||
315
src/wallet/asyncrpcoperation_autoshieldcoinbase.cpp
Normal file
315
src/wallet/asyncrpcoperation_autoshieldcoinbase.cpp
Normal file
@@ -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<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. Reuse the first spendable wallet-owned Sapling address (std::set order
|
||||
// is deterministic, so this is stable across rounds/restarts).
|
||||
std::set<libzcash::SaplingPaymentAddress> 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<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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,19 @@ 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;
|
||||
|
||||
void ClearNoteWitnessCache();
|
||||
|
||||
int64_t NullifierCount();
|
||||
@@ -1224,6 +1247,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);
|
||||
|
||||
@@ -16,7 +16,10 @@ cd ..
|
||||
|
||||
./autogen.sh
|
||||
|
||||
CONFIG_SITE=$PWD/depends/x86_64-w64-mingw32/share/config.site CXXFLAGS="-DPTW32_STATIC_LIB -DCURL_STATICLIB -fopenmp -pthread" ./configure --prefix="${PREFIX}" --host=x86_64-w64-mingw32 --enable-static --disable-shared
|
||||
# -Wa,-mbig-obj: the daemon's large template/boost-heavy TUs (e.g. asyncrpcoperation.cpp) exceed the
|
||||
# standard PE/COFF ~32k-section limit, which makes GNU ld emit "dangerous relocation" on .pdata and
|
||||
# crash. The bigobj COFF variant lifts that limit (same flag Bitcoin Core sets for the mingw host).
|
||||
CONFIG_SITE=$PWD/depends/x86_64-w64-mingw32/share/config.site CXXFLAGS="-DPTW32_STATIC_LIB -DCURL_STATICLIB -fopenmp -pthread -Wa,-mbig-obj" ./configure --prefix="${PREFIX}" --host=x86_64-w64-mingw32 --enable-static --disable-shared
|
||||
|
||||
# Build CryptoConditions stuff
|
||||
WD=$PWD
|
||||
|
||||
Reference in New Issue
Block a user