The balance, address scan, history and probe were throttled one by one at 10% each, which added up to 30-40% of dragonxd's cs_main with all of them due. services::WalletScanGovernor runs one wallet scan at a time across both RPC lanes and keeps one shared quiet clock at 15% of wall-clock (cut when the node is behind). Waiters are served first in, first out, with a head start for what the page shows and a starvation floor of max(30 s visible / 900 s hidden, 15x the stream's cost). Explicit work (a dirty set, the user's send, a first load) is never refused but is charged. Routine scans run only when the tip, the mempool size (getmempoolinfo: no cs_main) or a wallet-mutation epoch changed since their last good scan, or a safety sweep is due. Pages that don't show the balance poll it at most every max(10 min, 50x its cost) (2 min with client-side auto-shield); coming back into view with an old value fetches it at once. The staleness badge counts from the last poll that confirmed nothing changed, and hides while the budget holds the balance within its guarantee. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
198 lines
7.2 KiB
C++
198 lines
7.2 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
|
|
#include "history_scan_policy.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
namespace dragonx {
|
|
namespace services {
|
|
|
|
int historyStaleAfterBlocks(std::size_t addressCount, double perAddressScanMs)
|
|
{
|
|
if (addressCount == 0 || !(perAddressScanMs > 0.0)) return kHistoryMinStaleBlocks;
|
|
const double blocks = std::ceil((static_cast<double>(addressCount) * perAddressScanMs / 1000.0) /
|
|
(kHistoryBlockSeconds * kHistoryScanDuty));
|
|
if (!(blocks < kHistoryMaxStaleBlocks)) return kHistoryMaxStaleBlocks; // also NaN / inf
|
|
return std::max(kHistoryMinStaleBlocks, static_cast<int>(blocks));
|
|
}
|
|
|
|
double updateScanCostEstimateMs(double previousMs, double sampleMs)
|
|
{
|
|
if (!(sampleMs > 0.0)) return previousMs;
|
|
if (!(previousMs > 0.0) || sampleMs > previousMs) return sampleMs;
|
|
return 0.75 * previousMs + 0.25 * sampleMs;
|
|
}
|
|
|
|
int transparentPageSize(std::size_t previousRowCount)
|
|
{
|
|
const std::size_t want = previousRowCount + std::max<std::size_t>(200, previousRowCount / 10);
|
|
return static_cast<int>(std::clamp<std::size_t>(want, kTransparentPageMin, kTransparentPageMax));
|
|
}
|
|
|
|
bool sentSweepIsFull(int tip, int fromHeight)
|
|
{
|
|
return tip <= 0 || fromHeight < 0 || fromHeight > tip;
|
|
}
|
|
|
|
nlohmann::json sentSweepParams(const std::string& anyZAddress, int tip, int fromHeight)
|
|
{
|
|
nlohmann::json params = nlohmann::json::array();
|
|
params.push_back(anyZAddress);
|
|
params.push_back(0);
|
|
if (sentSweepIsFull(tip, fromHeight)) return params;
|
|
const int maxDepth = (tip - fromHeight) + kSentSweepReorgMarginBlocks;
|
|
params.push_back(2); // filter type 2: depth
|
|
params.push_back(maxDepth); // at most this deep (mempool txs, depth 0, always pass)
|
|
return params;
|
|
}
|
|
|
|
long fullSentSweepRetryDelaySec(int consecutiveFailures)
|
|
{
|
|
if (consecutiveFailures <= 0) return 0;
|
|
if (consecutiveFailures == 1) return 600;
|
|
if (consecutiveFailures == 2) return 3600;
|
|
return 21600;
|
|
}
|
|
|
|
bool fullSentSweepFailureCounts(rpc::RpcFailureKind kind)
|
|
{
|
|
using K = rpc::RpcFailureKind;
|
|
return kind == K::ReplyTimeout || kind == K::AppError || kind == K::Other;
|
|
}
|
|
|
|
std::size_t oldestScannedAddressIndex(const std::vector<std::string>& addresses,
|
|
const std::unordered_map<std::string, int>& heights)
|
|
{
|
|
std::size_t best = 0;
|
|
int bestHeight = 0;
|
|
bool haveBest = false;
|
|
for (std::size_t i = 0; i < addresses.size(); ++i) {
|
|
const auto it = heights.find(addresses[i]);
|
|
if (it == heights.end()) return i; // never scanned: first
|
|
if (!haveBest || it->second < bestHeight) {
|
|
best = i;
|
|
bestHeight = it->second;
|
|
haveBest = true;
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
std::string ProbeRotation::next(const std::vector<std::string>& addresses,
|
|
const std::unordered_map<std::string, int>& heights)
|
|
{
|
|
std::unordered_set<std::string> listed(addresses.begin(), addresses.end());
|
|
for (auto it = left_.begin(); it != left_.end();) {
|
|
if (listed.count(*it)) ++it;
|
|
else it = left_.erase(it);
|
|
}
|
|
if (left_.empty()) left_ = std::move(listed);
|
|
std::vector<std::string> left;
|
|
for (const auto& address : addresses) {
|
|
if (left_.count(address)) left.push_back(address);
|
|
}
|
|
if (left.empty()) return {};
|
|
return left[oldestScannedAddressIndex(left, heights)];
|
|
}
|
|
|
|
int ShieldedHistoryPass::floorAt(int tip) const
|
|
{
|
|
return tip > 0 ? std::min(startHeight_, tip) : startHeight_;
|
|
}
|
|
|
|
ShieldedHistoryPass::Work ShieldedHistoryPass::evaluate(int tip, const std::vector<std::string>& addresses,
|
|
const std::unordered_map<std::string, int>& heights,
|
|
int staleAfter, bool initialComplete) const
|
|
{
|
|
Work work;
|
|
work.total = addresses.size();
|
|
const int floor = active_ ? floorAt(tip) : -1;
|
|
std::size_t below = 0;
|
|
bool staleDue = false;
|
|
for (const auto& address : addresses) {
|
|
const auto it = heights.find(address);
|
|
if (it == heights.end()) {
|
|
++work.missing;
|
|
continue;
|
|
}
|
|
if (active_) {
|
|
if (it->second < floor) ++below;
|
|
} else if (tip > 0 && it->second < tip - staleAfter) {
|
|
staleDue = true;
|
|
}
|
|
}
|
|
work.pending = work.missing + (active_ ? below : (staleDue ? work.total - work.missing : 0));
|
|
work.explicitWork = !stalled() &&
|
|
(work.missing > 0 || (active_ ? (explicit_ && below > 0) : (staleDue && !initialComplete)));
|
|
work.routineWork = !work.explicitWork && work.pending > 0;
|
|
return work;
|
|
}
|
|
|
|
void ShieldedHistoryPass::begin(int tip, const std::vector<std::string>& addresses,
|
|
const std::unordered_map<std::string, int>& heights, int staleAfter,
|
|
bool initialComplete, bool routineAllowed)
|
|
{
|
|
if (active_) {
|
|
if (tip > 0 && startHeight_ > tip) startHeight_ = tip; // the chain reorganised below the floor
|
|
return;
|
|
}
|
|
if (tip <= 0 || !(routineAllowed || !initialComplete)) return;
|
|
bool staleDue = false;
|
|
for (const auto& address : addresses) {
|
|
const auto it = heights.find(address);
|
|
if (it != heights.end() && it->second < tip - staleAfter) {
|
|
staleDue = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!staleDue) return;
|
|
active_ = true;
|
|
explicit_ = !initialComplete;
|
|
startHeight_ = tip;
|
|
}
|
|
|
|
int ShieldedHistoryPass::scanFloor(int tip, bool routineAllowed) const
|
|
{
|
|
return (active_ && (explicit_ || routineAllowed)) ? floorAt(tip) : -1;
|
|
}
|
|
|
|
void ShieldedHistoryPass::finish(int tip, const std::vector<std::string>& addresses,
|
|
const std::unordered_map<std::string, int>& heights, std::size_t attempted,
|
|
std::size_t succeeded)
|
|
{
|
|
if (attempted > 0) stalledCycles_ = succeeded > 0 ? 0 : stalledCycles_ + 1;
|
|
// An empty list proves nothing (a failed enumeration blanks it); a real wallet change resets the pass.
|
|
if (!active_ || addresses.empty()) return;
|
|
const int floor = floorAt(tip);
|
|
for (const auto& address : addresses) {
|
|
const auto it = heights.find(address);
|
|
if (it == heights.end() || it->second < floor) return; // still work left in this pass
|
|
}
|
|
active_ = false;
|
|
explicit_ = false;
|
|
}
|
|
|
|
void ShieldedHistoryPass::reset()
|
|
{
|
|
*this = ShieldedHistoryPass{};
|
|
}
|
|
|
|
TxCycle decideTxCycle(const TxCycleInputs& in)
|
|
{
|
|
const bool wanted = in.neverFetched || in.newTip || in.dirty || in.explicitScanWork || in.routineScanWork;
|
|
const bool scansAllowed = !in.syncing && in.heightKnown;
|
|
if (wanted) {
|
|
if (scansAllowed && (in.explicitScanWork || in.gateOpen)) return TxCycle::Full;
|
|
// Rows now (a mutation, a tracked send at a new tip); wallet-wide scans wait for the throttle.
|
|
if (in.neverFetched || in.dirty || (in.sendTracking && in.newTip)) return TxCycle::Light;
|
|
}
|
|
if (in.recentDue) return (scansAllowed && in.gateOpen) ? TxCycle::RecentWithProbe : TxCycle::Recent;
|
|
return TxCycle::None;
|
|
}
|
|
|
|
} // namespace services
|
|
} // namespace dragonx
|