fix(fullnode): reliable rescan completion + self-explaining shielded send errors
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 <noreply@anthropic.com>
This commit is contained in:
20
src/app.cpp
20
src/app.cpp
@@ -431,6 +431,21 @@ wallet::LiteRolloutDecision resolveLiteRolloutDecision(config::Settings& setting
|
|||||||
}
|
}
|
||||||
return decision;
|
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
|
} // namespace
|
||||||
|
|
||||||
void App::rebuildLiteWallet(bool force)
|
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
|
// Failures: route to the originating send UI when there is one (it shows
|
||||||
// its own error toast); otherwise surface a generic notification (this is
|
// its own error toast); otherwise surface a generic notification (this is
|
||||||
// how shield/merge/auto-shield failures become visible).
|
// 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)) {
|
if (!invokeSendResultCallback(opid, false, msg)) {
|
||||||
ui::Notifications::instance().error(msg);
|
ui::Notifications::instance().error(msg);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1144,6 +1144,26 @@ void App::refreshCoreData()
|
|||||||
state_.warmup_description.clear();
|
state_.warmup_description.clear();
|
||||||
connection_status_ = TR("connected");
|
connection_status_ = TR("connected");
|
||||||
VERBOSE_LOGF("[warmup] Daemon ready, warmup complete\n");
|
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);
|
NetworkRefreshService::applyConnectionInfoResult(state_, result.info);
|
||||||
// Trigger full data refresh now that daemon is ready
|
// Trigger full data refresh now that daemon is ready
|
||||||
refreshData();
|
refreshData();
|
||||||
|
|||||||
@@ -1293,6 +1293,7 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["send_tx_sent"] = "Transaction sent!";
|
strings_["send_tx_sent"] = "Transaction sent!";
|
||||||
strings_["send_tx_success"] = "Transaction sent successfully!";
|
strings_["send_tx_success"] = "Transaction sent successfully!";
|
||||||
strings_["send_status_unconfirmed"] = "Transaction status could not be confirmed";
|
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_copied"] = "TxID copied to clipboard";
|
||||||
strings_["send_txid_label"] = "TxID: %s";
|
strings_["send_txid_label"] = "TxID: %s";
|
||||||
strings_["send_valid_shielded"] = "Valid shielded address";
|
strings_["send_valid_shielded"] = "Valid shielded address";
|
||||||
|
|||||||
Reference in New Issue
Block a user