wallet: resolve the autoshield destination at startup
pwalletMain->autoShieldAddress was only ever written by a round running in the
current process, so on any node without an explicit -autoshieldaddress,
z_autoshieldstatus reported an empty destination from startup until the first
round fired. disabled_reason was empty on that path too (rpcwallet.cpp), so the
RPC showed autoshield true, running false, no address and no explanation -- the
exact silent state z_autoshieldstatus was added to eliminate. On a restored
wallet, which pre-derives the whole -mnemonicsaplinggap window and therefore
always has an in-gap account to pick, the answer was known at startup and simply
not computed.
Split the read-only half of resolveDestination into a free function shared with
init: the configured override if set, else the lowest in-gap account
m/32'/coin'/i' the wallet already holds. init calls it once when autoshield is
enabled and no explicit address was given.
It deliberately does not generate a key. Deriving a fresh sapling account as a
side effect of populating a status field would mutate the wallet to make an RPC
prettier, so step 3 of resolveDestination -- the generation path, which must stay
inside the operation where an unlocked wallet is already established -- is left
where it was. A brand-new wallet holds nothing in-gap, so the field stays empty
there and disabled_reason now says why instead of being blank.
Behaviour is otherwise unchanged: same derivation, same lowest-index-wins rule,
same refusal to trust CKeyMetadata, same caching for the life of the process.
Verified on an isolated regtest chain, 12/12:
fresh wallet -> address "", reason "no destination resolved yet; one will
be derived from the HD seed on the first round"
after one round -> address set, reason empty
after RESTART -> address visible with NO block mined since (height 12 both
sides), and z_listaddresses still holds exactly 1 address,
so init derived nothing
-autoshieldaddress-> still overrides the derived destination, and the round
shields into it
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
20
src/init.cpp
20
src/init.cpp
@@ -60,6 +60,7 @@
|
|||||||
#include "wallet/wallet.h"
|
#include "wallet/wallet.h"
|
||||||
#include "wallet/walletdb.h"
|
#include "wallet/walletdb.h"
|
||||||
#include "wallet/asyncrpcoperation_saplingconsolidation.h"
|
#include "wallet/asyncrpcoperation_saplingconsolidation.h"
|
||||||
|
#include "wallet/asyncrpcoperation_autoshieldcoinbase.h"
|
||||||
#include "wallet/asyncrpcoperation_sweep.h"
|
#include "wallet/asyncrpcoperation_sweep.h"
|
||||||
#endif
|
#endif
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
@@ -2556,6 +2557,25 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
|
|||||||
return InitError("Wallet must hold the spending key of -autoshieldaddress (else shielded coinbase would be unrecoverable)");
|
return InitError("Wallet must hold the spending key of -autoshieldaddress (else shielded coinbase would be unrecoverable)");
|
||||||
}
|
}
|
||||||
pwalletMain->autoShieldAddress = autoShieldAddress;
|
pwalletMain->autoShieldAddress = autoShieldAddress;
|
||||||
|
} else {
|
||||||
|
// No explicit destination. Resolve the seed-derived one now, read-only,
|
||||||
|
// so z_autoshieldstatus can say where coinbase will go BEFORE the first
|
||||||
|
// round rather than reporting an empty string until one fires. This
|
||||||
|
// never generates a key: a fresh account must not be a side effect of
|
||||||
|
// populating a status field. A brand-new wallet holds nothing in-gap
|
||||||
|
// yet, so the field stays empty and the RPC explains why.
|
||||||
|
LOCK(pwalletMain->cs_wallet);
|
||||||
|
if (!pwalletMain->IsLocked()) {
|
||||||
|
libzcash::SaplingPaymentAddress destAddr;
|
||||||
|
std::string destStr;
|
||||||
|
uint32_t destAccount = AUTOSHIELD_ACCOUNT_NONE;
|
||||||
|
if (ResolveAutoShieldDestinationReadOnly(destAddr, destStr, destAccount)
|
||||||
|
== AutoShieldDestStatus::Resolved) {
|
||||||
|
pwalletMain->autoShieldAddress = destStr;
|
||||||
|
LogPrintf("%s: autoshield destination %s (seed-derived sapling account %u)\n",
|
||||||
|
__func__, destStr, (unsigned)destAccount);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,50 +105,34 @@ void AsyncRPCOperation_autoshieldcoinbase::main() {
|
|||||||
getId(), getStateAsString(), numTxCreated_, FormatMoney(amountShielded_));
|
getId(), getStateAsString(), numTxCreated_, FormatMoney(amountShielded_));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve the Sapling destination for auto-shielded coinbase.
|
// Read-only half of destination resolution, shared with init.cpp so the answer to
|
||||||
//
|
// "where will auto-shielding send?" is available before the first round runs rather
|
||||||
// Recoverability is the hard requirement: coinbase we shield must land in an
|
// than only after one has fired. Mutates nothing: no key generation, no caching.
|
||||||
// address that a bare -mnemonic/-hdseed restore of THIS wallet's seed re-derives
|
AutoShieldDestStatus ResolveAutoShieldDestinationReadOnly(
|
||||||
// on its own. A restore pre-derives exactly -mnemonicsaplinggap sapling accounts
|
libzcash::SaplingPaymentAddress& destOut, std::string& destStrOut, uint32_t& accountOut) {
|
||||||
// starting at index 0, with saplingAccountCounter reset to 0 (init.cpp:2349-2355),
|
accountOut = AUTOSHIELD_ACCOUNT_NONE;
|
||||||
// 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
|
// 1. Explicit -autoshieldaddress override (validated as a spendable Sapling
|
||||||
// zaddr at init.cpp:2494-2506). This also serves as the per-process cache
|
// zaddr in init.cpp). This doubles as the per-process cache for whatever
|
||||||
// for whatever step 2/3 resolved.
|
// the derivation below resolved on an earlier round.
|
||||||
if (!pwalletMain->autoShieldAddress.empty()) {
|
if (!pwalletMain->autoShieldAddress.empty()) {
|
||||||
auto decoded = DecodePaymentAddress(pwalletMain->autoShieldAddress);
|
auto decoded = DecodePaymentAddress(pwalletMain->autoShieldAddress);
|
||||||
if (boost::get<libzcash::SaplingPaymentAddress>(&decoded) != nullptr) {
|
if (boost::get<libzcash::SaplingPaymentAddress>(&decoded) != nullptr) {
|
||||||
destOut = boost::get<libzcash::SaplingPaymentAddress>(decoded);
|
destOut = boost::get<libzcash::SaplingPaymentAddress>(decoded);
|
||||||
destStrOut = pwalletMain->autoShieldAddress;
|
destStrOut = pwalletMain->autoShieldAddress;
|
||||||
return true;
|
return AutoShieldDestStatus::Resolved;
|
||||||
}
|
}
|
||||||
LogPrintf("%s: configured -autoshieldaddress is not a valid Sapling address\n", getId());
|
return AutoShieldDestStatus::InvalidOverride;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Walk the restore window m/32'/coin'/[0, gap)' derived from the seed.
|
// 2. Walk the restore window m/32'/coin'/[0, gap)' derived from the seed.
|
||||||
HDSeed seed;
|
HDSeed seed;
|
||||||
if (!pwalletMain->GetHDSeedForDerivation(seed)) {
|
if (!pwalletMain->GetHDSeedForDerivation(seed)) {
|
||||||
LogPrintf("%s: no HD seed available; refusing to pick an autoshield destination\n", getId());
|
return AutoShieldDestStatus::NoSeed;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mirror init.cpp:2349-2350's own clamp, and cap into the hardened index
|
// Mirror init.cpp's own clamp, and cap into the hardened index space so
|
||||||
// space so (i | ZIP32_HARDENED_KEY_LIMIT) below stays well formed.
|
// (i | ZIP32_HARDENED_KEY_LIMIT) below stays well formed.
|
||||||
int64_t gapArg = GetArg("-mnemonicsaplinggap", 100);
|
int64_t gapArg = GetArg("-mnemonicsaplinggap", 100);
|
||||||
if (gapArg < 0) {
|
if (gapArg < 0) {
|
||||||
gapArg = 0;
|
gapArg = 0;
|
||||||
@@ -179,14 +163,76 @@ bool AsyncRPCOperation_autoshieldcoinbase::resolveDestination(
|
|||||||
if (pwalletMain->GetSaplingExtendedSpendingKey(addr, held)) {
|
if (pwalletMain->GetSaplingExtendedSpendingKey(addr, held)) {
|
||||||
destOut = addr;
|
destOut = addr;
|
||||||
destStrOut = EncodePaymentAddress(addr);
|
destStrOut = EncodePaymentAddress(addr);
|
||||||
// Cache for the life of the process; step 1 short-circuits later
|
accountOut = i;
|
||||||
// rounds. Safe: we only cache post-validation.
|
return AutoShieldDestStatus::Resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AutoShieldDestStatus::NotFound;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
|
||||||
|
uint32_t account = AUTOSHIELD_ACCOUNT_NONE;
|
||||||
|
switch (ResolveAutoShieldDestinationReadOnly(destOut, destStrOut, account)) {
|
||||||
|
case AutoShieldDestStatus::Resolved:
|
||||||
|
// Cache for the life of the process; the override branch of the resolver
|
||||||
|
// short-circuits later rounds. Safe: we only cache post-validation.
|
||||||
pwalletMain->autoShieldAddress = destStrOut;
|
pwalletMain->autoShieldAddress = destStrOut;
|
||||||
LogPrintf("%s: autoshield destination %s (seed-derived sapling account %u, gap %u)\n",
|
if (account == AUTOSHIELD_ACCOUNT_NONE) {
|
||||||
getId(), destStrOut, (unsigned)i, (unsigned)saplingGap);
|
LogPrintf("%s: autoshield destination %s (configured)\n", getId(), destStrOut);
|
||||||
|
} else {
|
||||||
|
LogPrintf("%s: autoshield destination %s (seed-derived sapling account %u)\n",
|
||||||
|
getId(), destStrOut, (unsigned)account);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
|
case AutoShieldDestStatus::InvalidOverride:
|
||||||
|
LogPrintf("%s: configured -autoshieldaddress is not a valid Sapling address\n", getId());
|
||||||
|
return false;
|
||||||
|
case AutoShieldDestStatus::NoSeed:
|
||||||
|
LogPrintf("%s: no HD seed available; refusing to pick an autoshield destination\n", getId());
|
||||||
|
return false;
|
||||||
|
case AutoShieldDestStatus::NotFound:
|
||||||
|
break; // nothing in the window yet: fall through and derive one
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-establish the derivation context the resolver used, for step 3 below.
|
||||||
|
HDSeed seed;
|
||||||
|
if (!pwalletMain->GetHDSeedForDerivation(seed)) {
|
||||||
|
LogPrintf("%s: no HD seed available; refusing to pick an autoshield destination\n", getId());
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
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;
|
||||||
|
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);
|
||||||
|
|
||||||
// 3. Nothing usable in the window yet: derive the next account, but only if
|
// 3. Nothing usable in the window yet: derive the next account, but only if
|
||||||
// GenerateNewSaplingZKey will land INSIDE the window. It does NOT derive
|
// GenerateNewSaplingZKey will land INSIDE the window. It does NOT derive
|
||||||
|
|||||||
@@ -14,6 +14,26 @@
|
|||||||
// Default fee for automatic coinbase-shielding transactions
|
// Default fee for automatic coinbase-shielding transactions
|
||||||
static const CAmount DEFAULT_AUTOSHIELD_FEE = 10000;
|
static const CAmount DEFAULT_AUTOSHIELD_FEE = 10000;
|
||||||
|
|
||||||
|
// Sentinel for "not a derived account" (i.e. the configured -autoshieldaddress).
|
||||||
|
static const uint32_t AUTOSHIELD_ACCOUNT_NONE = UINT32_MAX;
|
||||||
|
|
||||||
|
enum class AutoShieldDestStatus {
|
||||||
|
Resolved, // destOut/destStrOut are set
|
||||||
|
NotFound, // no in-gap account held yet; the operation will derive one
|
||||||
|
InvalidOverride, // -autoshieldaddress is set but is not a Sapling address
|
||||||
|
NoSeed, // no HD seed available (e.g. locked wallet)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Resolve the auto-shield destination WITHOUT mutating the wallet: the configured
|
||||||
|
// -autoshieldaddress if set, else the lowest in-gap seed-derived account the wallet
|
||||||
|
// already holds. It deliberately does NOT generate a key, so init can call it purely
|
||||||
|
// to answer "where will this send?" -- deriving a fresh account as a side effect of
|
||||||
|
// populating a status field would be wrong. The operation's own resolveDestination
|
||||||
|
// falls through to generation when this returns NotFound.
|
||||||
|
// Caller must hold cs_wallet.
|
||||||
|
AutoShieldDestStatus ResolveAutoShieldDestinationReadOnly(
|
||||||
|
libzcash::SaplingPaymentAddress& destOut, std::string& destStrOut, uint32_t& accountOut);
|
||||||
|
|
||||||
// A periodic, wallet-local operation that drains matured *transparent* coinbase
|
// 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
|
// 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
|
// automatic sibling of the manual z_shieldcoinbase RPC and mirrors the dispatch
|
||||||
|
|||||||
@@ -3415,7 +3415,7 @@ UniValue z_autoshieldstatus(const UniValue& params, bool fHelp, const CPubKey& m
|
|||||||
why = strprintf("deferred while %s is running; rounds resume when it finishes",
|
why = strprintf("deferred while %s is running; rounds resume when it finishes",
|
||||||
pwalletMain->fSweepRunning ? "z_sweep" : "sapling consolidation");
|
pwalletMain->fSweepRunning ? "z_sweep" : "sapling consolidation");
|
||||||
} else if (pwalletMain->autoShieldAddress.empty()) {
|
} else if (pwalletMain->autoShieldAddress.empty()) {
|
||||||
why = "";
|
why = "no destination resolved yet; one will be derived from the HD seed on the first round";
|
||||||
}
|
}
|
||||||
ret.push_back(Pair("disabled_reason", why));
|
ret.push_back(Pair("disabled_reason", why));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user