perf(net): hold wallet scans at connect, in the background and on tab entry

- Connect gate: after the node starts answering, wallet scans wait until the
  lock state is known and the node knows its tip (or the wallet has fewer
  than 500 transactions), at most 20 s. A node already behind latches the
  sync profile from the connect's getinfo. Meanwhile a list-only pass keeps
  the address list and the cached history up, and the session's first
  balance and per-address balances are read once even while catching up,
  so the wallet shows its last-synced values instead of 0.
- Background: minimized, hidden, or covered without focus (3 s debounce,
  any input cancels) polls only the chain state every 15 s. Wallet scans,
  the mining poll, price, nudges and auto-lock interaction tracking pause;
  auto-lock now fires while minimized. Leaving a held profile marks every
  timer due again.
- Tab entry marks the timers due instead of forcing scans; a stale list or
  a never-loaded history still loads on entry.
- The scan entry points own the hard gates, failed enumerations keep the
  previous lists behind a retry hold, and failed balance calls never
  overwrite known balances with 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 11:06:34 -05:00
co-authored by Claude Opus 5.5
parent 323143e111
commit f86cc4d6b7
26 changed files with 1083 additions and 159 deletions
+3
View File
@@ -122,6 +122,9 @@ inherits the GUI's environment.
| `FAKE_STUCK_SHUTDOWN` | 0 | the Sep 29 orphan: hold the datadir lock, no listener, ignore SIGTERM, never exit |
| `FAKE_WALLET_NOTES` | (off) | a note wallet: `addr:count:amount,...` shielded notes; enables `z_mergetoaddress` + its operations (dragonxd rules: confirmed unlocked notes, a batch locked on return, merged note next block) |
| `FAKE_MERGE_SECS` | 6 | how long a `z_mergetoaddress` operation runs before it succeeds |
| `FAKE_TIP_UNKNOWN_SECS` | 0 | `longestchain` reads 0 this long after `Done Loading` (a node that hasn't seen an UpdateTip yet) |
| `FAKE_LONGEST_AHEAD` / `FAKE_CATCHUP_SECS` | 0 / 0 | the network tip (`longestchain`, `headers`) is N blocks ahead, for that long after `Done Loading` (0 = for good) |
| `FAKE_TXCOUNT` | 42 | `getwalletinfo` `txcount` (the GUI's connect gate waits for the tip on wallets of 500+) |
| `FAKE_METHODS_ALL` | 0 | `1` = one `call m=<method> p=<params>` event per request (the GUI's RPC cadence; the `methods` lines keep only the top 10 counts) |
| `FAKE_HEIGHT`, `FAKE_REBUILD_TOTAL`, `FAKE_NOTES`, `FAKE_VERSION` | 3327145, 62583, 13632, 1030050 | cosmetic numbers |
+21 -4
View File
@@ -123,6 +123,10 @@ struct Config {
bool stuckShutdown = false; // the Sep 29 orphan: datadir lock held, no listener, never exits
bool mining = false; // internal miner on: `stop` must wait for cs_main first
bool rebuildSilent = false; // hold cs_main like a rebuild but log no rebuild markers at all
double tipUnknownSecs = 0; // longestchain reads 0 this long after "Done Loading" (no UpdateTip seen yet)
long longestAhead = 0; // the network tip is this many blocks ahead: the node is behind...
double catchupSecs = 0; // ...for this long after "Done Loading" (0 = for good)
long txcount = 42; // getwalletinfo txcount
};
Config g_cfg;
@@ -272,6 +276,7 @@ void event(const std::string& name, const std::string& extra = "")
std::mutex g_csMain; // the daemon's cs_main
std::atomic<long> g_height{0};
std::atomic<bool> g_warmup{true};
std::atomic<long long> g_loadedAtMs{0}; // when warmup ended ("Done Loading")
std::mutex g_warmupMx;
std::string g_warmupStatus = "Loading block index...";
std::atomic<bool> g_minerOn{false};
@@ -602,19 +607,26 @@ Canned cannedReply(const std::string& m, const json& params)
return params[0].is_string() ? params[0].get<std::string>() : params[0].dump();
};
// FAKE_TIP_UNKNOWN_SECS / FAKE_LONGEST_AHEAD / FAKE_CATCHUP_SECS: what the node knows of the network tip.
const double sinceLoaded = g_loadedAtMs.load() > 0 ? (unixMs() - g_loadedAtMs.load()) / 1000.0 : 0.0;
const bool tipUnknown = g_loadedAtMs.load() == 0 || sinceLoaded < g_cfg.tipUnknownSecs;
const long ahead = (g_cfg.catchupSecs > 0 && sinceLoaded >= g_cfg.catchupSecs) ? 0 : g_cfg.longestAhead;
const long longest = tipUnknown ? 0 : h + ahead;
const long headers = h + ahead;
if (m == "getinfo") {
c.result = {{"version", g_cfg.version}, {"protocolversion", 170012}, {"walletversion", 60000},
{"balance", 0.0}, {"blocks", h}, {"longestchain", h}, {"notarized", 0},
{"balance", 0.0}, {"blocks", h}, {"longestchain", longest}, {"notarized", 0},
{"tiptime", now}, {"difficulty", difficulty}, {"keypoololdest", now - 86400},
{"keypoolsize", 101}, {"paytxfee", 0.0}, {"timeoffset", 0}, {"connections", 2},
{"tls_connections", 2}, {"proxy", ""}, {"testnet", false}, {"relayfee", 0.000001},
{"errors", ""}, {"name", "DRAGONX"}, {"p2pport", 21768}, {"rpcport", g_cfg.rpcPort}};
} else if (m == "getblockchaininfo") {
c.result = {{"chain", "main"}, {"blocks", h}, {"synced", true}, {"headers", h},
c.result = {{"chain", "main"}, {"blocks", h}, {"synced", ahead == 0}, {"headers", headers},
{"bestblockhash", fakeHash(h)}, {"difficulty", difficulty}, {"verificationprogress", 1.0},
{"chainwork", std::string(48, '0') + "1f3a5c7e9b2d4f60"}, {"pruned", false},
{"valuePools", json::array()}, {"softforks", json::array()}, {"upgrades", json::object()},
{"longestchain", h}, {"notarized", 0}};
{"longestchain", longest}, {"notarized", 0}};
} else if (m == "getblockcount") {
c.result = h;
} else if (m == "getbestblockhash") {
@@ -631,7 +643,7 @@ Canned cannedReply(const std::string& m, const json& params)
{"size", 1765}};
} else if (m == "getwalletinfo") {
c.result = {{"walletversion", 60000}, {"balance", 0.0}, {"unconfirmed_balance", 0.0},
{"immature_balance", 0.0}, {"txcount", 42}, {"keypoololdest", now - 86400},
{"immature_balance", 0.0}, {"txcount", g_cfg.txcount}, {"keypoololdest", now - 86400},
{"keypoolsize", 101}, {"paytxfee", 0.0}};
} else if (m == "z_gettotalbalance") {
c.result = {{"transparent", "0.00"}, {"private", "1234.5678"}, {"total", "1234.5678"}};
@@ -1348,6 +1360,10 @@ void loadConfig(int argc, char** argv)
g_cfg.noiseSecs = std::max(1.0, envDouble("FAKE_NOISE_SECS", g_cfg.noiseSecs));
g_cfg.serverTimeoutSecs = std::max(1.0, envDouble("FAKE_SERVER_TIMEOUT_SECS", g_cfg.serverTimeoutSecs));
g_cfg.prefill = envLong("FAKE_PREFILL", g_cfg.prefill);
g_cfg.tipUnknownSecs = envDouble("FAKE_TIP_UNKNOWN_SECS", g_cfg.tipUnknownSecs);
g_cfg.longestAhead = std::max(0L, envLong("FAKE_LONGEST_AHEAD", g_cfg.longestAhead));
g_cfg.catchupSecs = envDouble("FAKE_CATCHUP_SECS", g_cfg.catchupSecs);
g_cfg.txcount = envLong("FAKE_TXCOUNT", g_cfg.txcount);
g_cfg.height = envLong("FAKE_HEIGHT", g_cfg.height);
g_cfg.rebuildTotal = std::max(1L, envLong("FAKE_REBUILD_TOTAL", g_cfg.rebuildTotal));
g_cfg.notes = envLong("FAKE_NOTES", g_cfg.notes);
@@ -1469,6 +1485,7 @@ int main(int argc, char** argv)
}
if (initOk) {
setInitMessage("Full Node Done Loading! :)");
g_loadedAtMs = unixMs();
g_warmup = false;
event("warmup-end");
tipThread = std::thread(updateTipLoop);