perf(history): scan shielded history in bounded passes, not every block

Every block re-ran z_listreceivedbyaddress for every z-address (O(wallet)
each, under cs_main) and a whole-wallet z_listsentbyaddress. On a 40-address
wallet that was ~80% of wall-clock. The history now advances in passes:

- an address is rescanned when it is stale by a number of blocks scaled to
  its measured cost (8 to 500), never-scanned and hinted addresses first
  (a hint: z_listunspent shows a note the history doesn't);
- z_listsentbyaddress covers only the blocks since the last good sweep
  (+100 reorg margin); the whole-wallet sweep runs once per connect, with
  a 10 min / 1 h / 6 h backoff after real failures;
- listtransactions pages are sized from last time, and an unchanged tip
  reuses the transparent rows;
- found-but-unenriched txids carry over as a backlog; only -5 is a
  definitive "not ours" for z_viewtransaction;
- between blocks, a cheap recent poll plus a one-address probe.

Duty model for a 40-address wallet at 10 s per scan: ~9% instead of ~80%.

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 8b9a498eee
commit 879de37d18
10 changed files with 1645 additions and 233 deletions
+230 -70
View File
@@ -1,4 +1,5 @@
#include "network_refresh_service.h"
#include "history_scan_policy.h"
#include "rpc/rpc_busy_gate.h"
#include "../util/logger.h"
@@ -134,20 +135,43 @@ void appendTrackedSendPlaceholder(std::vector<TransactionInfo>& transactions,
transactions.push_back(std::move(info));
}
// A row the shielded scans (or z_viewtransaction) produce rather than listtransactions.
bool isShieldedRelatedRow(const TransactionInfo& transaction)
{
return (!transaction.address.empty() && transaction.address[0] == 'z') ||
(!transaction.from_address.empty() && transaction.from_address[0] == 'z') ||
!transaction.memo.empty();
}
void appendMissingPreviousTransactions(std::vector<TransactionInfo>& transactions,
const std::vector<TransactionInfo>& previousTransactions,
bool includeAll,
const std::unordered_set<std::string>& pinnedTxids)
{
// Presence is decided against the rows built this cycle, before any is carried: deduping against rows
// just appended kept only the first leg of a multi-recipient send.
std::set<std::pair<std::string, std::string>> present;
for (const auto& transaction : transactions) present.emplace(transaction.txid, transaction.type);
for (const auto& transaction : previousTransactions) {
bool pinned = pinnedTxids.find(transaction.txid) != pinnedTxids.end();
if (!includeAll && !pinned) {
bool shieldedRelated = (!transaction.address.empty() && transaction.address[0] == 'z') ||
(!transaction.from_address.empty() && transaction.from_address[0] == 'z') ||
!transaction.memo.empty();
if (!shieldedRelated) continue;
}
if (hasTransactionType(transactions, transaction.txid, transaction.type)) continue;
if (!includeAll && !pinned && !isShieldedRelatedRow(transaction)) continue;
if (present.count({transaction.txid, transaction.type})) continue;
transactions.push_back(transaction);
}
}
// On a full rebuild, keep the stored rows of sends still waiting to be re-enriched. Presence is decided
// against the rows built this cycle, before any is carried, so every row of a multi-recipient send is kept
// (deduping against rows just appended would keep only the first).
void appendPinnedPreviousTransactions(std::vector<TransactionInfo>& transactions,
const std::vector<TransactionInfo>& previousTransactions,
const std::unordered_set<std::string>& pinnedTxids)
{
std::set<std::pair<std::string, std::string>> present;
for (const auto& transaction : transactions) present.emplace(transaction.txid, transaction.type);
for (const auto& transaction : previousTransactions) {
if (!pinnedTxids.count(transaction.txid)) continue;
if (present.count({transaction.txid, transaction.type})) continue;
transactions.push_back(transaction);
}
}
@@ -767,6 +791,9 @@ NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddres
lite.confirmations = (nz.contains("rawconfirmations") && nz["rawconfirmations"].is_number_integer())
? nz["rawconfirmations"].get<int>()
: nz.value("confirmations", 0);
if (nz.contains("address") && nz["address"].is_string()) lite.address = nz["address"].get<std::string>();
if (nz.contains("txid") && nz["txid"].is_string()) lite.txid = nz["txid"].get<std::string>();
if (nz.contains("change") && nz["change"].is_boolean()) lite.change = nz["change"].get<bool>();
result.unspentNotes.push_back(lite);
}
}
@@ -1044,10 +1071,13 @@ 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.
// Time the whole cycle: listtransactions, the per-address z_listreceivedbyaddress scans and
// z_listsentbyaddress are each O(mapWallet) and hold the daemon's cs_main. The measured cost feeds the
// caller's throttle (txRefreshDue()) so routine scans can't starve block connection on a large wallet.
const auto scanStart = std::chrono::steady_clock::now();
auto msSince = [](std::chrono::steady_clock::time_point from) {
return std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - from).count();
};
std::set<std::string> knownTxids;
HushChatMemoOutputMap hushChatReceivedOutputs;
@@ -1056,47 +1086,62 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
: nullptr;
bool transactionRpcError = false;
try {
constexpr int pageSize = 1000;
int skip = 0;
while (true) {
json transactions = rpc.call("listtransactions", json::array({"", pageSize, skip}));
appendTransparentTransactions(result.transactions, knownTxids, transactions, snapshot.miningAddresses);
if (!transactions.is_array() || transactions.size() < static_cast<std::size_t>(pageSize)) break;
skip += pageSize;
// ── Transparent rows (listtransactions) ─────────────────────────────────────────────────────────
if (snapshot.reuseTransparentRows && !snapshot.previousTransactions.empty()) {
// Same tip and nothing changed since the rows on screen were fetched: keep their transparent rows
// instead of paging the whole history again. Shielded rows come from the scans below, and rows of
// tracked sends and pending ops are rebuilt by enrichment.
for (const auto& transaction : snapshot.previousTransactions) {
if (isShieldedRelatedRow(transaction)) continue;
if (snapshot.sendTxids.count(transaction.txid) || snapshot.pendingOpids.count(transaction.txid)) continue;
result.transactions.push_back(transaction);
if (!transaction.txid.empty()) knownTxids.insert(transaction.txid);
}
result.transparentRowsReused = true;
} else {
try {
// The first page holds last time's count plus headroom, so most wallets get every row in one
// call (re-paging 1000 rows at a time formats ~rows^2/2000 rows); skip-paging stays the fallback.
const int pageSize = transparentPageSize(snapshot.transparentRowHint);
for (int skip = 0;; skip += pageSize) {
json page = rpc.call("listtransactions", json::array({"", pageSize, skip}));
appendTransparentTransactions(result.transactions, knownTxids, page, snapshot.miningAddresses);
const std::size_t got = page.is_array() ? page.size() : 0;
result.transparentRowCount += got;
if (got < static_cast<std::size_t>(pageSize)) break;
}
result.transparentChainOk = true;
} catch (const std::exception& e) {
transactionRpcError = true;
DEBUG_LOGF("listtransactions error: %s\n", e.what());
}
} catch (const std::exception& e) {
transactionRpcError = true;
DEBUG_LOGF("listtransactions error: %s\n", e.what());
}
// An address counts as "scanned at tip" if its last scan is within shieldedScanTipTolerance
// blocks of the current tip. Tolerance 0 means strict (exact tip); a small tolerance lets a
// multi-cycle pass over many addresses complete even as the tip advances during it.
const int scanStaleThreshold = currentBlockHeight - std::max(0, snapshot.shieldedScanTipTolerance);
auto scannedAtTip = [&](const std::string& address) {
if (currentBlockHeight < 0) return false;
auto it = result.shieldedScanHeights.find(address);
return it != result.shieldedScanHeights.end() && it->second >= scanStaleThreshold;
// ── Shielded receives (z_listreceivedbyaddress, one per z-address) ──────────────────────────────
// A pass re-scans each address once: one scanned below the pass floor (or never) needs a scan, so a
// pass over many addresses completes however the tip moves meanwhile. Priority addresses go first.
const int scanFloor = snapshot.shieldedScanFloorHeight;
auto needsPassScan = [&](const std::string& address) {
const auto it = result.shieldedScanHeights.find(address);
if (it == result.shieldedScanHeights.end()) return true;
return scanFloor >= 0 && it->second < scanFloor;
};
std::size_t shieldedStart = snapshot.shieldedScanStartIndex;
if (shieldedStart >= snapshot.shieldedAddresses.size()) shieldedStart = 0;
std::size_t nextSearchIndex = shieldedStart;
for (std::size_t index = shieldedStart; index < snapshot.shieldedAddresses.size(); ++index) {
const auto& address = snapshot.shieldedAddresses[index];
if (scannedAtTip(address)) {
nextSearchIndex = index + 1;
continue;
}
if (snapshot.maxShieldedReceiveScans > 0 &&
result.shieldedAddressesScanned >= snapshot.maxShieldedReceiveScans) {
nextSearchIndex = index;
break;
}
auto needsPriorityScan = [&](const std::string& address) {
const auto priority = snapshot.priorityScanFloors.find(address);
if (priority == snapshot.priorityScanFloors.end()) return false;
const auto it = result.shieldedScanHeights.find(address);
return it == result.shieldedScanHeights.end() || it->second < priority->second;
};
auto budgetLeft = [&]() {
return snapshot.maxShieldedReceiveScans == 0 ||
result.shieldedAddressesScanned < snapshot.maxShieldedReceiveScans;
};
std::unordered_set<std::string> scannedNow;
auto scanAddress = [&](const std::string& address) {
++result.shieldedAddressesScanned;
nextSearchIndex = index + 1;
scannedNow.insert(address);
const auto callStart = std::chrono::steady_clock::now();
try {
json received = rpc.call("z_listreceivedbyaddress", json::array({address, 0}));
appendShieldedReceivedTransactions(result.transactions,
@@ -1105,11 +1150,36 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
received,
snapshot.miningAddresses,
hushChatReceivedOutputsPtr);
++result.shieldedScansSucceeded;
result.receiveScanOkMs += msSince(callStart);
if (currentBlockHeight >= 0) result.shieldedScanHeights[address] = currentBlockHeight;
} catch (const std::exception& e) {
transactionRpcError = true;
DEBUG_LOGF("z_listreceivedbyaddress error: %s\n", e.what());
}
};
std::size_t shieldedStart = snapshot.shieldedScanStartIndex;
if (shieldedStart >= snapshot.shieldedAddresses.size()) shieldedStart = 0;
std::size_t nextSearchIndex = shieldedStart;
if (!snapshot.skipShieldedScans) {
for (const auto& address : snapshot.shieldedAddresses) {
if (!budgetLeft()) break;
if (needsPriorityScan(address)) scanAddress(address);
}
for (std::size_t index = shieldedStart; index < snapshot.shieldedAddresses.size(); ++index) {
const auto& address = snapshot.shieldedAddresses[index];
if (!needsPassScan(address) || scannedNow.count(address)) {
nextSearchIndex = index + 1;
continue;
}
if (!budgetLeft()) {
nextSearchIndex = index;
break;
}
nextSearchIndex = index + 1;
scanAddress(address);
}
}
auto findPendingShieldedIndex = [&]() -> std::optional<std::size_t> {
@@ -1117,7 +1187,7 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
std::size_t start = nextSearchIndex < snapshot.shieldedAddresses.size() ? nextSearchIndex : 0;
for (std::size_t offset = 0; offset < snapshot.shieldedAddresses.size(); ++offset) {
std::size_t index = (start + offset) % snapshot.shieldedAddresses.size();
if (!scannedAtTip(snapshot.shieldedAddresses[index])) return index;
if (needsPassScan(snapshot.shieldedAddresses[index])) return index;
}
return std::nullopt;
};
@@ -1126,11 +1196,11 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
result.shieldedScanComplete = !pendingShieldedIndex.has_value();
result.nextShieldedScanStartIndex = pendingShieldedIndex.value_or(0);
// Discover outgoing shielded (z->z) sends. A pure z->z send moves no value in or out of the
// transparent pool (valueBalance == 0), so the daemon's `listtransactions` never reports it,
// and — being a send rather than a receive — it never shows up in `z_listreceivedbyaddress`
// either. Its txid therefore never reaches `knownTxids`, so the z_viewtransaction enrichment
// below is never run for it and the send is invisible in History (the reported bug).
// ── Outgoing shielded sends (z_listsentbyaddress) ───────────────────────────────────────────────
// A pure z->z send moves no value in or out of the transparent pool (valueBalance == 0), so the
// daemon's `listtransactions` never reports it, and — being a send rather than a receive — it never
// shows up in `z_listreceivedbyaddress` either. Its txid only reaches `knownTxids`, and so the
// z_viewtransaction enrichment below, through this call.
//
// `z_listsentbyaddress` scans the whole wallet for sends — the address argument is NOT used to
// filter (the daemon iterates every wallet tx against "*"), so a single call yields all send
@@ -1144,13 +1214,22 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
// deduped (by txid+address+amount) against listtransactions-derived t->t sends above, so a
// purely-transparent (t->t) send — which the payload ALSO reports — is not double-counted.
//
// Gated on shieldedScanComplete so it adds no cost during the multi-cycle initial received
// scan: it is one more O(mapWallet) pass holding cs_main (comparable to the listtransactions
// pass), and the caller's adaptive throttle (txRefreshDue) already accounts for it via scanMs.
if (result.shieldedScanComplete && !snapshot.shieldedAddresses.empty()) {
// It trial-decrypts every send it returns under cs_main, the heaviest call here on a wallet with many
// sends. So it runs once a pass is complete and, past a successful sweep (the caller's watermark), only
// over the txs at most a little deeper than the blocks since then (filter type 2). The whole-wallet
// sweep, needed when there's no watermark, gets a long timeout so it can finish on a large wallet.
const bool sweepIsFull = sentSweepIsFull(currentBlockHeight, snapshot.sentScanFromHeight);
if (!snapshot.shieldedAddresses.empty() && !snapshot.skipSentSweep &&
(result.shieldedScanComplete || (snapshot.allowMidPassSentSweep && !sweepIsFull))) {
result.sentSweepRan = true;
result.sentSweepFull = sweepIsFull;
const json params = sentSweepParams(snapshot.shieldedAddresses.front(), currentBlockHeight,
snapshot.sentScanFromHeight);
const auto sweepStart = std::chrono::steady_clock::now();
try {
json sent = rpc.call("z_listsentbyaddress",
json::array({snapshot.shieldedAddresses.front(), 0}));
json sent = result.sentSweepFull
? rpc.callLong("z_listsentbyaddress", params, kFullSentSweepTimeoutSec)
: rpc.call("z_listsentbyaddress", params);
if (sent.is_array()) {
for (const auto& sentTx : sent) {
auto txid = readOptional<std::string>(sentTx, "txid");
@@ -1158,11 +1237,15 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
appendSentTransparentOutputs(result.transactions, *txid, sentTx);
knownTxids.insert(*txid); // shielded (z->z) legs enriched via z_viewtransaction
}
result.sentSweepOk = true;
result.sentSweepHeight = currentBlockHeight;
}
} catch (const std::exception& e) {
transactionRpcError = true;
result.sentSweepErrKind = rpc::rpcFailureKindOf(e);
DEBUG_LOGF("z_listsentbyaddress error: %s\n", e.what());
}
result.sentSweepMs = msSince(sweepStart);
}
for (const auto& txid : snapshot.sendTxids) {
@@ -1173,17 +1256,41 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
if (!cachedView.outgoing_outputs.empty()) knownTxids.insert(txid);
}
// Shielded send rows from an earlier session (the history cache) whose view isn't cached: they count as
// enriched by their age, so nothing would rebuild them, and a windowed sweep never rediscovers an old
// send. Re-enrich them, and keep their stored rows ("pinned") until the daemon has answered.
std::unordered_set<std::string> rowBacklog;
// A settled stored row's block time and confirmations (>= 10, the History's "confirmed"): reused for
// its re-enrichment instead of a gettransaction per row. An unsettled one still asks, or its stale
// count would stick for the session (a cached view keeps the metadata it was built with).
std::unordered_map<std::string, std::pair<std::int64_t, int>> settledStoredMeta;
for (const auto& transaction : snapshot.previousTransactions) {
if (transaction.type != "send" || transaction.txid.empty() || !isShieldedRelatedRow(transaction)) continue;
if (snapshot.viewTxCache.count(transaction.txid) || snapshot.sendTxids.count(transaction.txid) ||
snapshot.pendingOpids.count(transaction.txid)) continue;
rowBacklog.insert(transaction.txid);
if (transaction.timestamp != 0 && transaction.confirmations >= 10)
settledStoredMeta.emplace(transaction.txid, std::make_pair(transaction.timestamp, transaction.confirmations));
}
std::set<std::string> backlog(snapshot.enrichBacklogTxids.begin(), snapshot.enrichBacklogTxids.end());
backlog.insert(rowBacklog.begin(), rowBacklog.end());
std::unordered_set<std::string> pinnedRows = rowBacklog;
// Tracked sends go first so they get the per-cycle z_viewtransaction budget ahead of routine
// enrichment, and a spent budget only skips further RPCs: cached rows later in the order are still
// appended (stopping the loop at the cap dropped them from History).
// enrichment, then this cycle's discoveries, then the backlog. A spent budget only skips further
// RPCs: cached rows later in the order are still appended (stopping the loop at the cap dropped them
// from History), and what was skipped is handed back as unenrichedTxids.
std::vector<const std::string*> enrichOrder;
enrichOrder.reserve(knownTxids.size());
enrichOrder.reserve(knownTxids.size() + backlog.size());
for (const auto& txid : knownTxids) {
if (snapshot.sendTxids.count(txid)) enrichOrder.push_back(&txid);
}
for (const auto& txid : knownTxids) {
if (!snapshot.sendTxids.count(txid)) enrichOrder.push_back(&txid);
}
for (const auto& txid : backlog) {
if (!knownTxids.count(txid) && !snapshot.sendTxids.count(txid)) enrichOrder.push_back(&txid);
}
int viewTxCount = 0;
for (const std::string* txidPtr : enrichOrder) {
@@ -1202,9 +1309,12 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
}
}
if (!trackedSend && snapshot.fullyEnrichedTxids.count(txid)) continue;
if (!trackedSend && snapshot.fullyEnrichedTxids.count(txid) && !rowBacklog.count(txid)) continue;
if (viewTxCount >= maxViewTransactionsPerCycle) continue;
if (viewTxCount >= maxViewTransactionsPerCycle) {
if (!trackedSend) result.unenrichedTxids.insert(txid);
continue;
}
++viewTxCount;
try {
@@ -1216,11 +1326,22 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
appendTrackedSendPlaceholder(result.transactions,
txid,
hasRawTransaction ? &rawTransaction : nullptr);
} else {
result.unenrichedTxids.insert(txid);
}
continue;
}
auto entry = parseViewTransactionCacheEntry(viewTransaction);
const bool storedRow = rowBacklog.count(txid) > 0;
if (storedRow && entry.timestamp == 0) {
const auto settled = settledStoredMeta.find(txid);
if (settled != settledStoredMeta.end()) {
entry.timestamp = settled->second.first;
entry.confirmations = settled->second.second;
}
}
const std::size_t rowsBeforeView = result.transactions.size();
appendViewTransactionOutputs(result.transactions, txid, entry);
// (Chat metadata is harvested only from RECEIVED notes, not our own outgoing memos — see
// the cached-view branch above.)
@@ -1246,6 +1367,21 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
}
}
if (storedRow) {
// A stored send rebuilt without a date (gettransaction failed): keep its stored rows (still
// pinned) and retry next cycle, rather than cache an undated view for the session.
bool undated = false;
for (std::size_t i = rowsBeforeView; i < result.transactions.size(); ++i) {
if (result.transactions[i].txid == txid && result.transactions[i].timestamp == 0) undated = true;
}
if (undated) {
result.transactions.resize(rowsBeforeView);
result.unenrichedTxids.insert(txid);
continue;
}
pinnedRows.erase(txid);
}
if (trackedSend && !hasTransactionType(result.transactions, txid, "send")) {
if (!hasRawTransaction) hasRawTransaction = tryFetchRawTransaction(rpc, txid, rawTransaction);
appendTrackedSendPlaceholder(result.transactions,
@@ -1275,13 +1411,20 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
}
}
} catch (const std::exception& e) {
(void)e;
if (trackedSend) {
json rawTransaction;
bool hasRawTransaction = tryFetchRawTransaction(rpc, txid, rawTransaction);
appendTrackedSendPlaceholder(result.transactions,
txid,
hasRawTransaction ? &rawTransaction : nullptr);
} else if (const auto* rpcError = dynamic_cast<const rpc::RpcError*>(&e);
rpcError && rpcError->code == -5) {
// "Invalid or non-wallet transaction id": definitive. Cache an empty view so it isn't
// asked again, and let a stored row for it go.
result.newViewTxEntries[txid] = TransactionViewCacheEntry{};
pinnedRows.erase(txid);
} else {
result.unenrichedTxids.insert(txid); // anything else may be transient: retry
}
}
}
@@ -1289,17 +1432,21 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectTr
appendExtractedHushChatMetadata(result.hushChatMetadata, hushChatReceivedOutputs);
if (!snapshot.previousTransactions.empty()) {
std::unordered_set<std::string> pinned = snapshot.pendingOpids;
pinned.insert(pinnedRows.begin(), pinnedRows.end());
if (transactionRpcError) {
appendMissingPreviousTransactions(result.transactions, snapshot.previousTransactions, true, snapshot.pendingOpids);
appendMissingPreviousTransactions(result.transactions, snapshot.previousTransactions, true, pinned);
} else if (snapshot.shieldedAddresses.empty() || !result.shieldedScanComplete ||
result.shieldedAddressesScanned < result.shieldedAddressCount) {
appendMissingPreviousTransactions(result.transactions, snapshot.previousTransactions, false, snapshot.pendingOpids);
appendMissingPreviousTransactions(result.transactions, snapshot.previousTransactions, false, pinned);
} else if (!pinnedRows.empty()) {
// A full rebuild, except the stored rows of sends still waiting to be re-enriched.
appendPinnedPreviousTransactions(result.transactions, snapshot.previousTransactions, pinnedRows);
}
}
sortTransactionsNewestFirst(result.transactions);
result.scanMs = std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now() - scanStart).count();
result.scanMs = msSince(scanStart);
return result;
}
@@ -1310,6 +1457,7 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectRe
int pageSize)
{
TransactionRefreshResult result;
const auto scanStart = std::chrono::steady_clock::now();
result.blockHeight = currentBlockHeight;
result.transactions = snapshot.previousTransactions;
result.shieldedAddressCount = snapshot.shieldedAddresses.size();
@@ -1360,9 +1508,12 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectRe
if (snapshot.maxShieldedReceiveScans > 0) {
shieldedLimit = std::min(shieldedLimit, snapshot.maxShieldedReceiveScans);
}
// Without the probe (the scan throttle is closed) the poll is listtransactions only.
if (snapshot.skipShieldedScans) shieldedLimit = 0;
for (std::size_t index = shieldedStart; index < shieldedStart + shieldedLimit; ++index) {
const auto& address = snapshot.shieldedAddresses[index];
const auto callStart = std::chrono::steady_clock::now();
try {
std::vector<TransactionInfo> scannedTransactions;
std::set<std::string> scannedTxids;
@@ -1384,18 +1535,27 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectRe
}
if (currentBlockHeight >= 0) result.shieldedScanHeights[address] = currentBlockHeight;
++result.shieldedAddressesScanned;
++result.shieldedScansSucceeded;
result.receiveScanOkMs += std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now() - callStart).count();
} catch (const std::exception& e) {
++result.shieldedAddressesScanned;
DEBUG_LOGF("recent z_listreceivedbyaddress error: %s\n", e.what());
}
}
result.nextShieldedScanStartIndex =
(shieldedStart + shieldedLimit >= snapshot.shieldedAddresses.size()) ? 0 : shieldedStart + shieldedLimit;
if (snapshot.skipShieldedScans) {
result.nextShieldedScanStartIndex = shieldedStart;
} else {
result.nextShieldedScanStartIndex =
(shieldedStart + shieldedLimit >= snapshot.shieldedAddresses.size()) ? 0 : shieldedStart + shieldedLimit;
}
result.shieldedScanComplete = true;
appendExtractedHushChatMetadata(result.hushChatMetadata, hushChatReceivedOutputs);
sortTransactionsNewestFirst(result.transactions);
result.scanMs = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - scanStart).count();
return result;
}