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:
2026-08-25 17:41:32 +02:00
co-authored by Claude Opus 5
parent 7a62fc4877
commit 358011bd54
4 changed files with 122 additions and 36 deletions
+20
View File
@@ -60,6 +60,7 @@
#include "wallet/wallet.h"
#include "wallet/walletdb.h"
#include "wallet/asyncrpcoperation_saplingconsolidation.h"
#include "wallet/asyncrpcoperation_autoshieldcoinbase.h"
#include "wallet/asyncrpcoperation_sweep.h"
#endif
#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)");
}
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);
}
}
}
}