6 Commits
Author SHA1 Message Date
DanSandClaude Opus 5 da191e447d Merge branch 'dev' for the v1.3.0 release
72 commits since 1c3523aac (2026-07-23). Highlights:

  consensus  BLOCK_VALID_CONTEXT no longer overwrites the block validity level
             (6d282db21); dead CBOPRET coinbase price check removed (267e6f7ad);
             PROTOCOL_VERSION 2000000 -> 2000001, MIN_PEER_PROTO_VERSION held.
  sync       DRAGONX checkpoints extended to 3,226,000 -- past the RandomX
             activation at 2,838,976, so the skip-below-checkpoint path finally
             fires (7dc904c96).
  wallet     interrupted rescans no longer hide funds and no longer latch
             fAbortRescan (a6b6f80db); rescan progress is checkpointed when a scan
             is interrupted (c1040028e); salvaged wallets open in a degraded mode
             with hdchain-mismatch detection (5634aed75).
  stratum    stratummine gated behind an explicit flag and hardened (db42091ce);
             low-difficulty shares rejected before spending a RandomX hash
             (fa16e740b).
  release    version bumped to v1.3.0 (af7d9e230) and doc/release-process.md
             corrected -- it previously omitted signing entirely, which is why
             v1.1.0 and v1.2.0 were tagged but never became installable releases.

Soak before merge: ~86 node-hours on c1040028e across all seven seeds, plus the
primary -- the pool's block-template source -- running it in production with the
pool accepting shares. Fleet consensus verified at a common height throughout.

Not included, deliberately: branch audit-fixes-20260901 (two commits, both marked
UNTESTED) stays parked. The ported rpc-tests have never been run green and are not
a gate for this release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
2026-09-01 19:09:04 -05:00
DanSandClaude Opus 5 fc26e2e0a0 doc: stamp the man pages with the release version, not a build sha
The man pages were regenerated at e2e10f6ef from a binary built at af7d9e230 and
carry "v1.3.0-af7d9e230". The release will be tagged v1.3.0, so the shipped man
pages would contradict the tag they ship under -- the same provenance gap af7d9e230
itself was written to close.

A faithful regeneration cannot fix this before the tag exists: gen-manpages.sh reads
--version from the binary, genbuild.sh derives that from `git describe`, and that
only prints exactly "v1.3.0" once the annotated tag is in place. Regenerating after
tagging would mean committing on top of the tag and moving it. doc/release-process.md
anticipates this and documents hardcoding the version instead.

Content is untouched and needs no regeneration: all 150 daemon options in the man
page match `dragonxd --help` exactly, including the five -autoshield* flags.

Not changed: dragonx-tx.1 still describes itself as "hush-tx utility". That string
comes from the binary's own --help, so editing the man page would make it diverge
from what the tool actually prints. It is a binary-level branding fix, not a doc one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
2026-09-01 19:06:54 -05:00
DanSandClaude Opus 5 1d2d5f32d0 version: bump PROTOCOL_VERSION for the v1.3.0 consensus changes
doc/release-process.md requires PROTOCOL_VERSION to increase by 1 for any release
carrying a consensus change. This one does: 6d282db21 stops BLOCK_VALID_CONTEXT
overwriting the block validity level, 267e6f7ad drops the dead CBOPRET price
validation from the coinbase check, and pow.cpp is 157 lines lighter. The value
has not moved since 85c8d7f7d in March.

MIN_PEER_PROTO_VERSION stays at 2000000. It currently equals PROTOCOL_VERSION, so
raising it in step would disconnect every peer still announcing 2000000 -- which is
every user, because no DragonX release has ever been installable: the wallet's
updater pins an ed25519 key and sets kDaemonRequireSignature = true, and no release
has ever shipped a .sig. Bumping only the advertised version lets 1.3.0 nodes be
identified on the wire without partitioning the network on release day.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
2026-09-01 19:06:54 -05:00
DanSandClaude Opus 5 c1040028e4 wallet: checkpoint rescan progress when the scan is interrupted
An aborted or shutdown-interrupted rescan discarded all of its progress: nothing
advanced the on-disk best-block locator, so the next run started over. On a large
shielded wallet that is a multi-minute witness rebuild repeated from scratch.

Writes the locator ONCE, at the interrupt, rather than periodically through the
scan. That delivers the whole stated benefit -- "resume where it stopped" -- at
the cost of exactly one SetBestChain per interrupted scan. A periodic checkpoint
would additionally survive SIGKILL and power loss, a much weaker requirement, and
it is the part that carries all the cost: SetBestChainINTERNAL is O(whole wallet)
regardless of progress, so on the reported 5.3k-tx / 7.5k-note wallet a checkpoint
every 2500 blocks would plausibly cost more than the rebuild it was meant to save.
If someone later demonstrates a need for crash resilience mid-scan, add it then,
with a measured interval and fFlush=false -- not before.

Two guards, both necessary:

  - CONTIGUITY. The RPC entry points (rescan / importprivkey / z_importkey /
    z_importviewingkey) take a caller-supplied start height validated only against
    chainActive.Height(), never against the wallet's own persisted locator. A scan
    beginning above that locator must not checkpoint at all, or it would record
    the skipped range as scanned and hide any funds in it. Logged once when it
    applies, so an operator can see why an interrupt did not persist.

  - MONOTONICITY. A checkpoint may only advance the locator, never move it back.

The locator points at the last FULLY PROCESSED block -- the parent of the block we
were about to scan -- so resume restarts one block early. CChain::GetLocator pushes
its argument first and FindForkInGlobalIndex returns that same block, so resume
begins AT it; the one-block overlap is deliberate and idempotent, since AddToWallet
only takes its merge path when the transaction is already present.

No witness work is done at the interrupt, which answers the two "//TODO: should we
update witness caches?" comments this replaces: witnesses are re-derived from each
note's own witnessHeight independently of the locator, and witnessRootValidated is
never serialized, so every note is revalidated against hashFinalSaplingRoot on the
next start. A mid-scan checkpoint does capture notes at mixed witnessHeights -- the
in-loop BuildWitnessCache(pindex, true) returns before the extension phase and only
seeds new notes at their own transaction's height -- but that state is recoverable:
the post-loop BuildWitnessCache(tip, false) levels every note, and it now occurs at
most once per scan instead of hundreds of times.

Uses SetBestChainNoFlush so the checkpoint does not trigger a full BDB
txn_checkpoint over the whole cache, and reports failure rather than silently
leaving the operator to discover the replay.

Depends on the fLastRescanCompleted gate in the previous commit: without it
init.cpp would overwrite this checkpoint with a tip locator immediately after the
scan returns.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
2026-08-31 20:54:37 -05:00
DanSandClaude Opus 5 a6b6f80db0 wallet: stop an interrupted rescan from hiding funds, and un-latch fAbortRescan
Two independent fund-visibility bugs on the rescan path, both pre-existing.

1. AN INTERRUPTED RESCAN RECORDED ITSELF AS COMPLETE.

ScanForWalletTransactions returns a bare `int ret` (a found-tx count) on both its
abort and shutdown bail-outs, so the caller could not tell "finished" from
"stopped at block H". init.cpp then ran, unconditionally:

    pwalletMain->ScanForWalletTransactions(pindexRescan, true);
    pwalletMain->SetBestChain(chainActive.GetLocator());   // TIP locator

An interrupted scan therefore stamped the wallet as scanned all the way to the
chain tip. On the next start, the `chainActive.Tip() != pindexRescan` guard just
above sees no work to do and skips the rescan entirely, so every transaction in
the never-scanned range stays out of mapWallet -- permanently invisible to
getbalance and unspendable. That is exactly the case `-rescan` exists for: a key
import, right after ClearNoteWitnessCache has run.

Adds CWallet::fLastRescanCompleted, set false at scan entry and true only on the
normal exit, and gates the init.cpp locator write on it. Deliberately NOT
overloading the int return, which callers already use as a tx count.

2. fAbortRescan WAS NEVER RESET.

wallet.h declares it, AbortRescan() sets it true, and nothing in src/ ever sets
it false. The abortrescan RPC is live. After one call, for the remaining lifetime
of the process:
  - every ScanForWalletTransactions returns immediately, so re-running an import
    silently no-ops while the RPC still reports success;
  - BuildWitnessCache bails on the same flag on EVERY call, including the routine
    per-block extension from ChainTip.
Witness heights then diverge across notes while the chain advances, and
GetSaplingNoteWitnesses elects the majority root as the anchor and returns
boost::none for every note that disagrees. Those notes still show in the balance
but cannot be spent. Cleared at scan entry and consumed in the abort branch.

Also in BuildWitnessCache's abort branch: stop clearing fRescanning. A witness
rebuild is not a rescan and must not touch a flag ScanForWalletTransactions owns.

3. Both bail-out log lines had two format specifiers and one argument:

    LogPrintf("%s: Rescan aborted at block %d\n", pwalletMain->rescanHeight);

tinyformat's "too many conversion specifiers" guard is disabled in this tree
(tinyformat.h: `if(*fmt != '\0' && 0 ) // disabled due to complaints`), so this
does not throw -- verified by compiling the exact call against this tree's
tinyformat.h. It emits "3841207: Rescan aborted at block " with the height in the
__func__ slot, the real height dropped, and the trailing newline swallowed so the
next log line concatenates onto it. On the one path an operator has to diagnose
an interrupted rescan, the only record was corrupt. Fixed at both sites, and the
state updates moved above the log call.

4. Makes SetBestChainINTERNAL report whether the atomic write committed, so a
checkpointing caller can tell (six failure paths returned void). Adds
SetBestChainNoFlush, which opens the wallet DB with fFlushOnClose=false: the
default ctor makes ~CDB run a full BDB txn_checkpoint over the whole cache, which
is fine hourly but not from inside a scan -- the same flush wallet.cpp already
documents avoiding elsewhere "for performance reasons". Nothing calls it yet.

5. SetBestChainINTERNAL took each CWalletTx by value, deep-copying every note's
witness deque (WITNESS_CACHE_SIZE entries) per transaction per call, purely to
serialize it. Now by const reference.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
2026-08-31 20:52:40 -05:00
DanSandClaude Opus 5 3aac75e94f cli: report the port actually dialled, not a stale global
dragonx-cli resolves -rpcport correctly -- CallRPC does
  int port = GetArg("-rpcport", BaseParams().RPCPort());
and dials that port. But both connection-failure messages printed
ASSETCHAINS_RPCPORT instead, a separate global defined at the top of
bitcoin-cli.cpp, initialised to the mainnet default 21769, and never assigned
anywhere in the CLI.

So every failure claimed port 21769 regardless of what was asked for:

  $ dragonx-cli -rpcport=21799 getblockcount
  error: couldn't connect to server at port 21769

That reads as "your -rpcport was ignored", which is a much more alarming and
much more misleading diagnosis than "nothing is listening yet". It cost an hour
of debugging on a node that was simply still loading a 15 GB txindex, and led to
the wrong conclusion that the CLI could be talking to the production daemon when
it was not.

Both sites now print the local `port`. Verified against the previous binary:

  new:  -rpcport=21799 -> "couldn't connect to server at port 21799"
        -rpcport=59999 -> "couldn't connect to server at port 59999"
  old:  both            -> "couldn't connect to server at port 21769"

and the CLI still reaches a live daemon on a non-default port.

ASSETCHAINS_RPCPORT is left defined; it is referenced by other translation units.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FU87LdsJZiZkfq1eXubpeo
2026-08-31 17:35:52 -05:00
8 changed files with 126 additions and 23 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
.SH NAME .SH NAME
dragonx-cli \- manual page for dragonx-cli v1.3.0 dragonx-cli \- manual page for dragonx-cli v1.3.0
.SH DESCRIPTION .SH DESCRIPTION
DragonX RPC client version v1.3.0\-af7d9e230 DragonX RPC client version v1.3.0
.PP .PP
In order to ensure you are adequately protecting your privacy when using In order to ensure you are adequately protecting your privacy when using
DragonX, please see <https://dragonx.is/security/>. DragonX, please see <https://dragonx.is/security/>.
+1 -1
View File
@@ -3,7 +3,7 @@
.SH NAME .SH NAME
dragonx-tx \- manual page for dragonx-tx v1.3.0 dragonx-tx \- manual page for dragonx-tx v1.3.0
.SH DESCRIPTION .SH DESCRIPTION
hush\-tx utility version v1.3.0\-af7d9e230 hush\-tx utility version v1.3.0
.SS "Usage:" .SS "Usage:"
.TP .TP
hush\-tx [options] <hex\-tx> [commands] hush\-tx [options] <hex\-tx> [commands]
+1 -1
View File
@@ -3,7 +3,7 @@
.SH NAME .SH NAME
dragonxd \- manual page for dragonxd v1.3.0 dragonxd \- manual page for dragonxd v1.3.0
.SH DESCRIPTION .SH DESCRIPTION
DragonX Daemon version v1.3.0\-af7d9e230 DragonX Daemon version v1.3.0
.PP .PP
In order to ensure you are adequately protecting your privacy when using In order to ensure you are adequately protecting your privacy when using
DragonX, please see <https://dragonx.is/security/>. DragonX, please see <https://dragonx.is/security/>.
+6 -2
View File
@@ -257,14 +257,18 @@ UniValue CallRPC(const std::string& strMethod, const UniValue& params)
event_base_dispatch(base.get()); event_base_dispatch(base.get());
if (response.status == 0) { if (response.status == 0) {
// Report the port we ACTUALLY dialled. ASSETCHAINS_RPCPORT is a separate global that is
// initialised to the mainnet default at the top of this file and never assigned here, so
// using it made every failure claim port 21769 no matter what -rpcport was given -- which
// reads as "your -rpcport was ignored" and sends you chasing a config bug that isn't there.
throw CConnectionFailed(strprintf("couldn't connect to server at port %d : %s (code %d)\n(make sure server is running and you are connecting to the correct RPC port)", throw CConnectionFailed(strprintf("couldn't connect to server at port %d : %s (code %d)\n(make sure server is running and you are connecting to the correct RPC port)",
ASSETCHAINS_RPCPORT, http_errorstring(response.error), response.error)); port, http_errorstring(response.error), response.error));
} else if (response.status == HTTP_UNAUTHORIZED) { } else if (response.status == HTTP_UNAUTHORIZED) {
throw std::runtime_error("incorrect rpcuser or rpcpassword (authorization failed)"); throw std::runtime_error("incorrect rpcuser or rpcpassword (authorization failed)");
} else if (response.status >= 400 && response.status != HTTP_BAD_REQUEST && response.status != HTTP_NOT_FOUND && response.status != HTTP_INTERNAL_SERVER_ERROR) { } else if (response.status >= 400 && response.status != HTTP_BAD_REQUEST && response.status != HTTP_NOT_FOUND && response.status != HTTP_INTERNAL_SERVER_ERROR) {
throw std::runtime_error(strprintf("server returned HTTP error %d", response.status)); throw std::runtime_error(strprintf("server returned HTTP error %d", response.status));
} else if (response.body.empty()) { } else if (response.body.empty()) {
throw std::runtime_error(strprintf("no response from server at port %d", ASSETCHAINS_RPCPORT )); throw std::runtime_error(strprintf("no response from server at port %d", port));
} }
// Parse reply // Parse reply
+13 -2
View File
@@ -2654,8 +2654,19 @@ bool AppInit2(boost::thread_group& threadGroup, CScheduler& scheduler)
nStart = GetTimeMillis(); nStart = GetTimeMillis();
pwalletMain->ScanForWalletTransactions(pindexRescan, true); pwalletMain->ScanForWalletTransactions(pindexRescan, true);
LogPrintf(" rescan %15dms\n", GetTimeMillis() - nStart); LogPrintf(" rescan %15dms\n", GetTimeMillis() - nStart);
pwalletMain->SetBestChain(chainActive.GetLocator()); // ONLY record "scanned to the tip" if the scan actually reached it. An aborted or
nWalletDBUpdated++; // shutdown-interrupted scan that stamps a tip locator tells the next startup there is
// nothing left to scan (the chainActive.Tip() != pindexRescan guard above then skips
// the rescan entirely), so every transaction in the unscanned range stays out of
// mapWallet permanently: invisible to getbalance and unspendable. The scan writes its
// own locator at the interrupt point instead.
if (pwalletMain->fLastRescanCompleted) {
pwalletMain->SetBestChain(chainActive.GetLocator());
nWalletDBUpdated++;
} else {
LogPrintf("Rescan did not complete; leaving the best-block locator at the scan's own "
"checkpoint so the remaining range is rescanned on the next start\n");
}
// Restore wallet transaction metadata after -zapwallettxes=1 // Restore wallet transaction metadata after -zapwallettxes=1
if (GetBoolArg("-zapwallettxes", false) && GetArg("-zapwallettxes", "1") != "2") if (GetBoolArg("-zapwallettxes", false) && GetArg("-zapwallettxes", "1") != "2")
+7 -1
View File
@@ -22,7 +22,13 @@
// network protocol versioning // network protocol versioning
// DragonX 1.0.0 - bumped to separate from old HUSH/DragonX nodes with RandomX bug // DragonX 1.0.0 - bumped to separate from old HUSH/DragonX nodes with RandomX bug
static const int PROTOCOL_VERSION = 2000000; // DragonX 1.3.0 - bumped for the consensus changes in this release (BLOCK_VALID_CONTEXT
// no longer overwrites the validity level, the dead CBOPRET coinbase
// price check is gone, and pow.cpp lost 157 lines). MIN_PEER_PROTO_VERSION
// is deliberately NOT raised: it currently equals this value, and raising
// it would disconnect every node still on v1.0.3 -- which is all users,
// since no release has ever been installable (unsigned archives).
static const int PROTOCOL_VERSION = 2000001;
//! initial proto version, to be increased after version/verack negotiation //! initial proto version, to be increased after version/verack negotiation
static const int INIT_PROTO_VERSION = 209; static const int INIT_PROTO_VERSION = 209;
//! In this version, 'getheaders' was introduced. //! In this version, 'getheaders' was introduced.
+74 -6
View File
@@ -801,10 +801,18 @@ bool CWallet::CommitAutomatedTx(const CTransaction& tx) {
void CWallet::SetBestChain(const CBlockLocator& loc) void CWallet::SetBestChain(const CBlockLocator& loc)
{ {
// Default ctor => fFlushOnClose=true => ~CDB runs a full BDB txn_checkpoint over the entire
// cache. Fine for the hourly/shutdown callers; ruinous inside a scan (see SetBestChainNoFlush).
CWalletDB walletdb(strWalletFile); CWalletDB walletdb(strWalletFile);
SetBestChainINTERNAL(walletdb, loc); SetBestChainINTERNAL(walletdb, loc);
} }
bool CWallet::SetBestChainNoFlush(const CBlockLocator& loc)
{
CWalletDB walletdb(strWalletFile, "r+", false);
return SetBestChainINTERNAL(walletdb, loc);
}
std::set<std::pair<libzcash::PaymentAddress, uint256>> CWallet::GetNullifiersForAddresses( std::set<std::pair<libzcash::PaymentAddress, uint256>> CWallet::GetNullifiersForAddresses(
const std::set<libzcash::PaymentAddress> & addresses) const std::set<libzcash::PaymentAddress> & addresses)
{ {
@@ -1436,8 +1444,9 @@ void CWallet::BuildWitnessCache(const CBlockIndex* pindex, bool witnessOnly)
return; return;
} }
if (pwalletMain->fAbortRescan) { if (pwalletMain->fAbortRescan) {
// Do NOT clear fRescanning here: a witness rebuild is not a rescan, and clearing it from
// this path desynchronises the flag from ScanForWalletTransactions, which owns it.
LogPrintf("%s: rescan aborted during witness rebuild\n", __func__); LogPrintf("%s: rescan aborted during witness rebuild\n", __func__);
pwalletMain->fRescanning = false;
return; return;
} }
int h = pbi->GetHeight(); int h = pbi->GetHeight();
@@ -3434,6 +3443,12 @@ int CWallet::ScanForWalletTransactions(CBlockIndex* pindexStart, bool fUpdate)
if(fZdebug) if(fZdebug)
LogPrintf("%s: fUpdate=%d now=%li\n",__func__,fUpdate,nNow); LogPrintf("%s: fUpdate=%d now=%li\n",__func__,fUpdate,nNow);
// fAbortRescan is sticky: nothing else in the tree ever clears it, so without this a single
// `abortrescan` RPC would disable every later scan AND every later BuildWitnessCache for the
// lifetime of the process (BuildWitnessCache bails on the same flag), freezing witness heights
// while the chain advances and progressively rendering notes unspendable.
pwalletMain->fAbortRescan = false;
pwalletMain->fLastRescanCompleted = false;
pwalletMain->fRescanning = true; pwalletMain->fRescanning = true;
CBlockIndex* pindex = pindexStart; CBlockIndex* pindex = pindexStart;
pwalletMain->rescanStartHeight = pindex->GetHeight(); pwalletMain->rescanStartHeight = pindex->GetHeight();
@@ -3448,6 +3463,51 @@ int CWallet::ScanForWalletTransactions(CBlockIndex* pindexStart, bool fUpdate)
pwalletMain->rescanHeight = pindex ? pindex->GetHeight() : 0; pwalletMain->rescanHeight = pindex ? pindex->GetHeight() : 0;
} }
// --- interrupt checkpoint setup -------------------------------------------------------
// Where does the wallet currently believe it has scanned to? A checkpoint may only ever
// ADVANCE that point, and only if this scan is contiguous with it. The RPC entry points
// (rescan / importprivkey / z_importkey / z_importviewingkey) take a caller-supplied start
// height validated only against chainActive.Height(), so a scan can legitimately begin far
// ABOVE the persisted locator -- writing a checkpoint from such a scan would mark the
// skipped range as scanned and hide any funds in it.
CBlockIndex* pindexPersisted = NULL;
{
CWalletDB walletdb(strWalletFile, "r+", false);
CBlockLocator locPersisted;
if (walletdb.ReadBestBlock(locPersisted))
pindexPersisted = FindForkInGlobalIndex(chainActive, locPersisted);
}
const bool fMayCheckpoint = pindexPersisted != NULL &&
pindexStart->GetHeight() <= pindexPersisted->GetHeight() + 1;
if (!fMayCheckpoint) {
LogPrintf("%s: scan starts at %d but the wallet is persisted at %d; progress will NOT be "
"checkpointed on interrupt (a non-contiguous scan cannot safely advance the locator)\n",
__func__, pindexStart->GetHeight(),
pindexPersisted ? pindexPersisted->GetHeight() : -1);
}
// Persist progress when the scan is cut short. `pindexStopped` is the block we were ABOUT to
// scan, so the last fully-processed block is its parent. Resume restarts AT the locator's own
// block (CChain::GetLocator pushes it first; FindForkInGlobalIndex returns it), giving one
// block of deliberate overlap -- idempotent, because AddToWallet only merges when the tx is
// already present. No witness work is needed: witnesses are re-derived from each note's own
// witnessHeight, and witnessRootValidated is in-memory-only so every note is revalidated
// against hashFinalSaplingRoot on the next start.
auto checkpointProgress = [&](const CBlockIndex* pindexStopped) {
if (!fMayCheckpoint || !pindexStopped || !pindexStopped->pprev)
return;
const CBlockIndex* pindexDone = pindexStopped->pprev;
if (pindexDone->GetHeight() <= pindexPersisted->GetHeight())
return; // never move the locator backwards
if (SetBestChainNoFlush(chainActive.GetLocator(pindexDone))) {
LogPrintf("%s: checkpointed scan progress at height %d\n", __func__, pindexDone->GetHeight());
} else {
LogPrintf("%s: FAILED to checkpoint scan progress at height %d; the scan will replay "
"from height %d on the next start\n", __func__, pindexDone->GetHeight(),
pindexPersisted->GetHeight());
}
};
ShowProgress(_("Rescanning..."), 0); // show rescan progress in GUI as dialog or on splashscreen, if -rescan on startup ShowProgress(_("Rescanning..."), 0); // show rescan progress in GUI as dialog or on splashscreen, if -rescan on startup
double dProgressStart = Checkpoints::GuessVerificationProgress(chainParams.Checkpoints(), pindex, false); double dProgressStart = Checkpoints::GuessVerificationProgress(chainParams.Checkpoints(), pindex, false);
double dProgressTip = Checkpoints::GuessVerificationProgress(chainParams.Checkpoints(), chainActive.LastTip(), false); double dProgressTip = Checkpoints::GuessVerificationProgress(chainParams.Checkpoints(), chainActive.LastTip(), false);
@@ -3456,15 +3516,20 @@ int CWallet::ScanForWalletTransactions(CBlockIndex* pindexStart, bool fUpdate)
{ {
pwalletMain->rescanHeight = pindex->GetHeight(); pwalletMain->rescanHeight = pindex->GetHeight();
if(pwalletMain->fAbortRescan) { if(pwalletMain->fAbortRescan) {
//TODO: should we update witness caches? // The witness caches do NOT need updating here: on resume each note's witnesses are
LogPrintf("%s: Rescan aborted at block %d\n", pwalletMain->rescanHeight); // re-derived from its own witnessHeight, independently of the locator, and
pwalletMain->fRescanning = false; // witnessRootValidated is in-memory-only so a full validation pass runs next boot.
// What DOES need saving is the locator -- see the checkpoint below.
pwalletMain->fRescanning = false;
pwalletMain->fAbortRescan = false; // consume it; see the note at scan entry
LogPrintf("%s: Rescan aborted at block %d\n", __func__, pwalletMain->rescanHeight);
checkpointProgress(pindex);
return ret; return ret;
} }
if (ShutdownRequested()) { if (ShutdownRequested()) {
//TODO: should we update witness caches?
LogPrintf("%s: Rescan interrupted by shutdown request at block %d\n", pwalletMain->rescanHeight);
pwalletMain->fRescanning = false; pwalletMain->fRescanning = false;
LogPrintf("%s: Rescan interrupted by shutdown request at block %d\n", __func__, pwalletMain->rescanHeight);
checkpointProgress(pindex);
return ret; return ret;
} }
@@ -3525,6 +3590,9 @@ int CWallet::ScanForWalletTransactions(CBlockIndex* pindexStart, bool fUpdate)
// we are no longer rescanning // we are no longer rescanning
pwalletMain->fRescanning = false; pwalletMain->fRescanning = false;
// Reached only by running the loop to the end of the active chain. Callers use this to decide
// whether it is honest to record the wallet as scanned up to the tip.
pwalletMain->fLastRescanCompleted = true;
return ret; return ret;
} }
+23 -9
View File
@@ -833,6 +833,10 @@ public:
bool fAutoShieldRunning = false; bool fAutoShieldRunning = false;
std::atomic<bool> fAbortRescan{false}; std::atomic<bool> fAbortRescan{false};
//! True only when the last ScanForWalletTransactions ran to completion. An aborted or
//! shutdown-interrupted scan leaves this false, so callers must not record the wallet as
//! scanned up to the chain tip -- doing so makes the unscanned range permanently invisible.
bool fLastRescanCompleted = false;
// abort current rescan // abort current rescan
void AbortRescan() { fAbortRescan = true; } void AbortRescan() { fAbortRescan = true; }
// Are we currently aborting a rescan? // Are we currently aborting a rescan?
@@ -904,17 +908,22 @@ protected:
*/ */
void DecrementNoteWitnesses(const CBlockIndex* pindex); void DecrementNoteWitnesses(const CBlockIndex* pindex);
//! Returns true only if the atomic write actually committed. Callers that checkpoint during a
//! long scan need to know: a silently-failing checkpoint would otherwise be retried forever at
//! full cost while never making progress.
template <typename WalletDB> template <typename WalletDB>
void SetBestChainINTERNAL(WalletDB& walletdb, const CBlockLocator& loc) { bool SetBestChainINTERNAL(WalletDB& walletdb, const CBlockLocator& loc) {
if (!walletdb.TxnBegin()) { if (!walletdb.TxnBegin()) {
// This needs to be done atomically, so don't do it at all // This needs to be done atomically, so don't do it at all
LogPrintf("SetBestChain(): Couldn't start atomic write\n"); LogPrintf("SetBestChain(): Couldn't start atomic write\n");
return; return false;
} }
try { try {
LOCK(cs_wallet); LOCK(cs_wallet);
for (std::pair<const uint256, CWalletTx>& wtxItem : mapWallet) { for (std::pair<const uint256, CWalletTx>& wtxItem : mapWallet) {
auto wtx = wtxItem.second; // By reference: a copy here deep-copies every note's witness deque
// (WITNESS_CACHE_SIZE entries) for every transaction, on every call.
const CWalletTx& wtx = wtxItem.second;
// We skip transactions for which mapSaplingNoteData // We skip transactions for which mapSaplingNoteData
// is empty. This covers transactions that have no Sapling data // is empty. This covers transactions that have no Sapling data
// (i.e. are purely transparent), as well as shielding and unshielding // (i.e. are purely transparent), as well as shielding and unshielding
@@ -923,32 +932,33 @@ protected:
if (!walletdb.WriteTx(wtxItem.first, wtx)) { if (!walletdb.WriteTx(wtxItem.first, wtx)) {
LogPrintf("SetBestChain(): Failed to write CWalletTx, aborting atomic write\n"); LogPrintf("SetBestChain(): Failed to write CWalletTx, aborting atomic write\n");
walletdb.TxnAbort(); walletdb.TxnAbort();
return; return false;
} }
} }
} }
if (!walletdb.WriteWitnessCacheSize(nWitnessCacheSize)) { if (!walletdb.WriteWitnessCacheSize(nWitnessCacheSize)) {
LogPrintf("SetBestChain(): Failed to write nWitnessCacheSize, aborting atomic write\n"); LogPrintf("SetBestChain(): Failed to write nWitnessCacheSize, aborting atomic write\n");
walletdb.TxnAbort(); walletdb.TxnAbort();
return; return false;
} }
if (!walletdb.WriteBestBlock(loc)) { if (!walletdb.WriteBestBlock(loc)) {
LogPrintf("SetBestChain(): Failed to write best block, aborting atomic write\n"); LogPrintf("SetBestChain(): Failed to write best block, aborting atomic write\n");
walletdb.TxnAbort(); walletdb.TxnAbort();
return; return false;
} }
} catch (const std::exception &exc) { } catch (const std::exception &exc) {
// Unexpected failure // Unexpected failure
LogPrintf("SetBestChain(): Unexpected error during atomic write:\n"); LogPrintf("SetBestChain(): Unexpected error during atomic write:\n");
LogPrintf("%s\n", exc.what()); LogPrintf("%s\n", exc.what());
walletdb.TxnAbort(); walletdb.TxnAbort();
return; return false;
} }
if (!walletdb.TxnCommit()) { if (!walletdb.TxnCommit()) {
// Couldn't commit all to db, but in-memory state is fine // Couldn't commit all to db, but in-memory state is fine
LogPrintf("SetBestChain(): Couldn't commit atomic write\n"); LogPrintf("SetBestChain(): Couldn't commit atomic write\n");
return; return false;
} }
return true;
} }
private: private:
@@ -1282,8 +1292,12 @@ public:
void RunSaplingConsolidation(int blockHeight); void RunSaplingConsolidation(int blockHeight);
void RunAutoShieldCoinbase(int blockHeight); void RunAutoShieldCoinbase(int blockHeight);
bool CommitAutomatedTx(const CTransaction& tx); bool CommitAutomatedTx(const CTransaction& tx);
/** Saves witness caches and best block locator to disk. */ /** Saves witness caches and best block locator to disk. Overrides CValidationInterface. */
void SetBestChain(const CBlockLocator& loc); void SetBestChain(const CBlockLocator& loc);
/** As SetBestChain, but for use INSIDE a long scan: opens the wallet DB with fFlushOnClose=false
* so the call does not trigger a full BDB txn_checkpoint over the whole cache (see the comment
* at CWallet::SetBestChain), and reports whether the write actually committed. */
bool SetBestChainNoFlush(const CBlockLocator& loc);
std::set<std::pair<libzcash::PaymentAddress, uint256>> GetNullifiersForAddresses(const std::set<libzcash::PaymentAddress> & addresses); std::set<std::pair<libzcash::PaymentAddress, uint256>> GetNullifiersForAddresses(const std::set<libzcash::PaymentAddress> & addresses);
bool IsNoteSaplingChange(const std::set<std::pair<libzcash::PaymentAddress, uint256>> & nullifierSet, const libzcash::PaymentAddress & address, const SaplingOutPoint & entry); bool IsNoteSaplingChange(const std::set<std::pair<libzcash::PaymentAddress, uint256>> & nullifierSet, const libzcash::PaymentAddress & address, const SaplingOutPoint & entry);