fix(net): stop re-running the connect sequence while the wallet is locked

Connected-but-locked fell through to tryConnect() on every Core tick
while the lock screen sat idle: new sockets, getinfo and getwalletinfo,
a forced address scan, a CoinGecko fetch and a wallet-index rewrite.
Its wallet scans also held cs_main ahead of the user's
walletpassphrase.

- While locked, poll chain state only (refreshCoreData without the
  balance). That keeps the sync height current and the 3-strike
  lost-connection check armed, so a node that exits while locked is
  still restarted. Pool mining keeps the lock screen down, so while it
  runs the throttled balance, the peers and the lock state (for an
  unlock done in the Console tab) keep refreshing too.
- Warmup polls count refused/reset connections toward the
  lost-connection check, so a node that died during warmup is
  restarted instead of leaving the loading or lock screen over it.
- The wallet salvage/degraded detectors run at connect and again when
  warmup ends: for encrypted wallets they relied on the repeated
  tryConnect.
- restartDaemonAfterEncryption records the wallet as encrypted and
  locked itself (the wizard's deferred path never did), and marks the
  main client disconnected so the reconnect path picks up the restarted
  node, aborting any in-flight call first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 19:52:09 -05:00
co-authored by Claude Opus 5.5
parent 575abd4bcc
commit 1cb03332e7
6 changed files with 69 additions and 3 deletions
+26 -2
View File
@@ -709,6 +709,7 @@ void App::onConnected()
connection_status_ = TR("connected");
detectWalletAutoRecovery(); // also runs every tryConnect tick — catches a salvage even if we never connect
detectWalletDegraded();
// Re-arm the empty-wallet-with-funded-sibling check for this (possibly switched) wallet: re-evaluate the
// on-disk state once it finishes loading + syncing. Deliberately DON'T touch empty_wallet_scan_in_flight_
@@ -1786,7 +1787,7 @@ void App::refreshData()
// Uses fast_worker_ when on Overview tab for lower latency.
// ============================================================================
void App::refreshCoreData()
void App::refreshCoreData(bool allowBalance)
{
if (!state_.connected) return;
@@ -1800,6 +1801,15 @@ void App::refreshCoreData()
return [this, result = std::move(result)]() {
if (result.ready) {
// Warmup finished — daemon is fully ready
consecutive_core_failures_ = 0;
// The salvage / degraded-wallet lines are logged during wallet load, inside warmup, so
// a connect that landed in warmup scanned too early. Look again now (both latch). The
// locked-wallet poll no longer re-runs tryConnect, which used to catch them for
// encrypted wallets.
if (!daemon_restarting_) {
detectWalletAutoRecovery();
detectWalletDegraded();
}
state_.warming_up = false;
state_.warmup_status.clear();
state_.warmup_description.clear();
@@ -1831,6 +1841,18 @@ void App::refreshCoreData()
// Trigger full data refresh now that daemon is ready
refreshData();
} else {
// A node that died during warmup answers with a refused/reset connection, not -28.
// Count those like the Core poll does, so a dead node is torn down and restarted
// instead of leaving the loading or lock screen over it indefinitely.
if (rpc::rpcKindMeansLost(result.errorKind)) {
if (++consecutive_core_failures_ >= 3 && state_.connected) {
consecutive_core_failures_ = 0;
handleLostConnection("Lost connection to daemon");
return;
}
} else {
consecutive_core_failures_ = 0; // -28 warmup / busy: the node is there
}
// Still warming up — update status
applyWarmupText(result.errorMessage);
VERBOSE_LOGF("[warmup] Still warming up: %s\n", result.errorMessage.c_str());
@@ -1855,7 +1877,9 @@ void App::refreshCoreData()
// large shielded wallet backs off automatically instead of re-scanning every couple of seconds.
// A wallet mutation (send/shield) forces the next poll through so the user's own action updates the
// balance immediately. Captured on the main thread to avoid reading state_ off the worker thread.
const bool includeBalance = !effectivelySyncing() &&
// allowBalance=false (the locked wallet's chain-state poll) skips it and leaves a forced refresh
// pending for after the unlock.
const bool includeBalance = allowBalance && !effectivelySyncing() &&
(force_balance_refresh_ || balanceRefreshDue());
if (includeBalance) force_balance_refresh_ = false;