fix(sync): prioritize getblockchaininfo and pause chat scans while behind
The node kept falling behind the network near the tip because sync DETECTION was starved: getblockchaininfo was queued behind the O(mapWallet) wallet RPCs (z_gettotalbalance / z_listunspent), so longestchain went stale, the wallet decided it was "synced", and it resumed hammering cs_main — a feedback loop. - Issue getblockchaininfo FIRST each cycle and skip the balance/address/ tx refresh entirely while behind, so sync state (and kSyncProfile) updates before any heavy wallet scan runs. - Gate the two chat note scans (refreshChatNoteBudgetNode / fastScanChatMemos) on effectivelySyncing() and the active page, so chat memo scanning no longer competes with block connection during sync. - Windows debug.log tailer: reset the read offset when dragonxd truncates the log on startup (it was stranding at Block:0 with no witness/rescan progress). - Tests cover the getblockchaininfo-first ordering and behind-skip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -297,7 +297,31 @@ NetworkRefreshService::CoreRefreshResult NetworkRefreshService::collectCoreRefre
|
||||
bool blockOk = false;
|
||||
double balanceScanMs = 0.0;
|
||||
|
||||
if (includeBalance) {
|
||||
// getblockchaininfo FIRST — it's cheap, and the sync state it returns gates everything else
|
||||
// (the balance/address/tx throttles and kSyncProfile). Running it BEFORE the O(mapWallet) balance
|
||||
// scan keeps sync detection from being delayed behind (or, under contention, starved by) that
|
||||
// scan — the failure mode where the wallet kept reading "synced" while actually falling behind,
|
||||
// so kSyncProfile never engaged. See effectivelySyncing()'s sticky-behind latch.
|
||||
try {
|
||||
blockInfo = rpc.call("getblockchaininfo", json::array());
|
||||
blockOk = true;
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("BlockchainInfo error: %s\n", e.what());
|
||||
}
|
||||
|
||||
// If getblockchaininfo shows we're behind (same 2-block tolerance as applyCoreRefreshResult), skip
|
||||
// the balance scan this cycle regardless of includeBalance: the balance is incomplete mid-sync
|
||||
// anyway, and skipping it lets the sync-state update — and hence kSyncProfile — take effect at the
|
||||
// end of THIS (now-cheap) task instead of being delayed ~20s behind the scan. This is what lets the
|
||||
// sticky-behind latch engage promptly the first time the node falls behind.
|
||||
bool behind = false;
|
||||
if (blockOk && blockInfo.is_object()) {
|
||||
const long long b = blockInfo.value("blocks", 0LL);
|
||||
const long long lc = blockInfo.value("longestchain", 0LL);
|
||||
if (lc > 0 && b < lc - 2) behind = true;
|
||||
}
|
||||
|
||||
if (includeBalance && !behind) {
|
||||
// z_gettotalbalance is O(mapWallet) and holds the daemon's cs_main for its whole duration —
|
||||
// seconds on a large shielded wallet. Time it so the caller can throttle how often it polls
|
||||
// (balanceRefreshDue()), keeping balance scans from starving block connection.
|
||||
@@ -315,13 +339,6 @@ NetworkRefreshService::CoreRefreshResult NetworkRefreshService::collectCoreRefre
|
||||
std::chrono::steady_clock::now() - balanceStart).count();
|
||||
}
|
||||
|
||||
try {
|
||||
blockInfo = rpc.call("getblockchaininfo", json::array());
|
||||
blockOk = true;
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("BlockchainInfo error: %s\n", e.what());
|
||||
}
|
||||
|
||||
auto result = parseCoreRefreshResult(totalBalance, spendableBalance, balanceOk, blockInfo, blockOk);
|
||||
result.balanceScanMs = balanceScanMs;
|
||||
return result;
|
||||
@@ -603,6 +620,10 @@ NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddres
|
||||
const AddressRefreshSnapshot& snapshot)
|
||||
{
|
||||
AddressRefreshResult result;
|
||||
// Time the whole scan — z_listunspent (and per-address z_getbalance fallback) hold the daemon's
|
||||
// cs_main for the duration, seconds on a large shielded wallet. The measured cost feeds the
|
||||
// caller's adaptive throttle so the address poll can't starve block connection.
|
||||
const auto scanStart = std::chrono::steady_clock::now();
|
||||
|
||||
try {
|
||||
json zList = rpc.call("z_listaddresses", json::array());
|
||||
@@ -644,6 +665,22 @@ NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddres
|
||||
try {
|
||||
json unspent = rpc.call("z_listunspent", json::array({0, 9999999, false})); // minconf=0 → include 0-conf change
|
||||
applyShieldedBalancesFromUnspent(result.shieldedAddresses, unspent);
|
||||
// Retain a minimal view so a downstream consumer (chat note-budget) can reuse this scan instead
|
||||
// of issuing its own z_listunspent. rawconfirmations is the TRUE depth; `confirmations` is
|
||||
// dPoW-clamped to 1 and understates it.
|
||||
if (unspent.is_array()) {
|
||||
result.unspentNotes.reserve(unspent.size());
|
||||
for (const auto& nz : unspent) {
|
||||
if (!nz.is_object()) continue;
|
||||
UnspentNoteLite lite;
|
||||
lite.amount = nz.value("amount", 0.0);
|
||||
lite.locked = nz.value("locked", false);
|
||||
lite.confirmations = (nz.contains("rawconfirmations") && nz["rawconfirmations"].is_number_integer())
|
||||
? nz["rawconfirmations"].get<int>()
|
||||
: nz.value("confirmations", 0);
|
||||
result.unspentNotes.push_back(lite);
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("z_listunspent unavailable (%s), falling back to z_getbalance\n", e.what());
|
||||
for (auto& info : result.shieldedAddresses) {
|
||||
@@ -673,6 +710,8 @@ NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddres
|
||||
DEBUG_LOGF("listunspent error: %s\n", e.what());
|
||||
}
|
||||
|
||||
result.scanMs = std::chrono::duration<double, std::milli>(
|
||||
std::chrono::steady_clock::now() - scanStart).count();
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -864,6 +903,10 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
|
||||
result.blockHeight = currentBlockHeight;
|
||||
result.shieldedAddressCount = snapshot.shieldedAddresses.size();
|
||||
result.shieldedScanHeights = snapshot.shieldedScanHeights;
|
||||
// Time the whole scan — the per-address z_listreceivedbyaddress pass is O(mapWallet) and holds the
|
||||
// daemon's cs_main. The measured cost feeds the caller's adaptive throttle (txRefreshDue()) so the
|
||||
// routine full history rescan can't starve block connection on a large wallet.
|
||||
const auto scanStart = std::chrono::steady_clock::now();
|
||||
|
||||
std::set<std::string> knownTxids;
|
||||
HushChatMemoOutputMap hushChatReceivedOutputs;
|
||||
@@ -1044,6 +1087,8 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
|
||||
}
|
||||
|
||||
sortTransactionsNewestFirst(result.transactions);
|
||||
result.scanMs = std::chrono::duration<double, std::milli>(
|
||||
std::chrono::steady_clock::now() - scanStart).count();
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -147,6 +147,14 @@ public:
|
||||
std::string errorMessage;
|
||||
};
|
||||
|
||||
// Minimal per-note view of a z_listunspent entry — just what a downstream consumer needs to derive
|
||||
// spendable-note budgets without re-scanning. Kept UI/feature-agnostic (no chat specifics here).
|
||||
struct UnspentNoteLite {
|
||||
double amount = 0.0; // note value, DRGX
|
||||
bool locked = false; // tied up by an in-flight send
|
||||
int confirmations = 0; // TRUE depth (rawconfirmations when present, else confirmations)
|
||||
};
|
||||
|
||||
struct AddressRefreshResult {
|
||||
std::vector<AddressInfo> shieldedAddresses;
|
||||
std::vector<AddressInfo> transparentAddresses;
|
||||
@@ -154,6 +162,12 @@ public:
|
||||
// lists may be falsely short. Consumers that treat an empty list as authoritative (e.g. the
|
||||
// empty-wallet warning) must not trust a 0 count unless this is true.
|
||||
bool addressListOk = true;
|
||||
// Wall-clock spent in the address scan (dominated by z_listunspent — O(mapWallet), holds the
|
||||
// daemon's cs_main). Lets the caller throttle how often it polls (addressRefreshDue()).
|
||||
double scanMs = 0.0;
|
||||
// The wallet's unspent notes from this same z_listunspent scan, so a consumer (e.g. the chat
|
||||
// note-budget) can be fed for free instead of running its own duplicate z_listunspent.
|
||||
std::vector<UnspentNoteLite> unspentNotes;
|
||||
};
|
||||
|
||||
struct AddressRefreshSnapshot {
|
||||
@@ -205,6 +219,9 @@ public:
|
||||
std::size_t shieldedAddressCount = 0;
|
||||
std::unordered_map<std::string, int> shieldedScanHeights;
|
||||
bool shieldedScanComplete = true;
|
||||
// Wall-clock spent in the history scan (z_listreceivedbyaddress — O(mapWallet), holds cs_main).
|
||||
// Lets the caller throttle the routine full rescan by its measured cost (txRefreshDue()).
|
||||
double scanMs = 0.0;
|
||||
};
|
||||
|
||||
struct OperationStatusPollResult {
|
||||
|
||||
Reference in New Issue
Block a user