From 6ff80354dfffc3874bb5fcda8eebea9ba70c21a3 Mon Sep 17 00:00:00 2001 From: DanS Date: Tue, 16 Jun 2026 22:43:24 -0500 Subject: [PATCH] fix(fullnode): reliable rescan completion + self-explaining shielded send errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related fixes for the post-bootstrap "send fails / rescan stuck at 99%" trap: 1) Rescan completion now keys off warmup-end. A -rescan runs entirely inside daemon warmup (every RPC returns -28 until it finishes), so warmup completing IS the rescan completing. The old detectors relied on getrescaninfo (which some daemons answer with "Method not found") or a "Done rescanning"/bench log line the daemon may never print, leaving the status bar stuck at 99% — so users killed the rescan before it finished. When warmup ends and a rescan was confirmed active, clear the rescan state, flip to 100%, refresh history/balance, and toast completion. 2) z_sendmany failures that mean stale shielded note data (shielded-requirements-not-met, missing sapling anchor, invalid sapling spend proof, bad-txns-sapling-*) now append a plain-language hint telling the user to run a full rescan, instead of surfacing only the raw daemon string. Co-Authored-By: Claude Opus 4.8 --- src/app.cpp | 20 +++++++++++++++++++- src/app_network.cpp | 20 ++++++++++++++++++++ src/util/i18n.cpp | 1 + 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/app.cpp b/src/app.cpp index 5176854..5dcb7cd 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -431,6 +431,21 @@ wallet::LiteRolloutDecision resolveLiteRolloutDecision(config::Settings& setting } return decision; } + +// True when a z_sendmany failure means the wallet's shielded note data (witnesses/anchors) is +// out of sync with the chain — the symptom after a bootstrap/reindex without a full rescan. These +// are consensus/proof-build rejections, not user mistakes, and the only fix is a full rescan. +bool sendErrorNeedsRescan(const std::string& raw) +{ + auto has = [&](const char* s) { return raw.find(s) != std::string::npos; }; + return has("shielded-requirements-not-met") || + has("shielded requirements not met") || + has("missing sapling anchor") || + has("Invalid sapling spend proof") || + has("Invalid output proof") || + has("bad-txns-sapling-") || + has("anchor"); +} } // namespace void App::rebuildLiteWallet(bool force) @@ -849,7 +864,10 @@ void App::update() // Failures: route to the originating send UI when there is one (it shows // its own error toast); otherwise surface a generic notification (this is // how shield/merge/auto-shield failures become visible). - for (const auto& [opid, msg] : parsed.failureByOpid) { + for (const auto& [opid, rawMsg] : parsed.failureByOpid) { + std::string msg = sendErrorNeedsRescan(rawMsg) + ? rawMsg + "\n\n" + TR("send_err_needs_rescan") + : rawMsg; if (!invokeSendResultCallback(opid, false, msg)) { ui::Notifications::instance().error(msg); } diff --git a/src/app_network.cpp b/src/app_network.cpp index 0c4d278..19194a0 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -1144,6 +1144,26 @@ void App::refreshCoreData() state_.warmup_description.clear(); connection_status_ = TR("connected"); VERBOSE_LOGF("[warmup] Daemon ready, warmup complete\n"); + + // A -rescan runs entirely INSIDE daemon warmup (every RPC returns -28 until the + // scan finishes), so warmup completing IS the rescan completing. This is the + // reliable completion signal: some daemons lack getrescaninfo (it returns + // "Method not found") or never print a "Done rescanning"/bench line, which left + // the older detectors stuck at 99% — the user would then kill it prematurely. + // rescan_confirmed_active_ ensures we actually observed this rescan running (set + // by the getrescaninfo / daemon-log pollers) before declaring it done. + if (state_.sync.rescanning && rescan_confirmed_active_) { + state_.sync.rescanning = false; + rescan_confirmed_active_ = false; + state_.sync.rescan_progress = 1.0f; + state_.sync.rescan_status.clear(); + // Notes/witnesses were rebuilt — force a fresh history + balance pull. + transactions_dirty_ = true; + last_tx_block_height_ = -1; + invalidateShieldedHistoryScanProgress(true); + ui::Notifications::instance().success("Blockchain rescan complete"); + } + NetworkRefreshService::applyConnectionInfoResult(state_, result.info); // Trigger full data refresh now that daemon is ready refreshData(); diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 5672c7b..e386bb1 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -1293,6 +1293,7 @@ void I18n::loadBuiltinEnglish() strings_["send_tx_sent"] = "Transaction sent!"; strings_["send_tx_success"] = "Transaction sent successfully!"; strings_["send_status_unconfirmed"] = "Transaction status could not be confirmed"; + strings_["send_err_needs_rescan"] = "Your wallet's shielded note data is out of date with the blockchain (this happens after a bootstrap or reindex). Run a full rescan via Settings -> Rescan Blockchain and let it finish completely, then try sending again."; strings_["send_txid_copied"] = "TxID copied to clipboard"; strings_["send_txid_label"] = "TxID: %s"; strings_["send_valid_shielded"] = "Valid shielded address";