z_listsentbyaddress re-walks the whole wallet when there's no usable watermark, which the daemon does under cs_main for every send -- hundreds of seconds, 400+ s and ~19 GB RSS on a large wallet. The watermark lived only in memory, so every relaunch, reconnect or daemon restart paid for a full sweep. This persists it so those cases run a bounded windowed sweep ([z, 0, 2, tip - W + margin]) instead (SENT-HISTORY-SWEEP Part 1). - The watermark (W = last swept tip), its anchor (A = W - margin) and anchor hash (H_A = getblockhash(A)), plus the enrichment backlog and the full- sweep backoff, ride in the encrypted history snapshot as an optional `sent_sweep` object (schema stays v1; a missing/inconsistent one reads as "no watermark" = today's behaviour, so older builds and payloads are safe). - Before the first windowed sweep after a load, the anchor is verified (getblockhash(A) == H_A). A match means any fork is inside the window; a mismatch (reorg deeper than the margin, different chain/datadir) or an inability to check drops the watermark and sweeps the whole wallet. - The first sweep after a load uses a bigger margin (absorbs notifier lag), and a watermark older than 2400 blocks uses the long timeout (callLong). - The backlog is persisted WITH W: a pure z->z send's txid lives only in a sweep result and would be lost if the window moved past an un-enriched one. - Watermark changes reach disk (folded into the store signature), and every hard case still forces a full sweep: reorg (anchor verify), GUI rescan / zap / import / address change, restoreOriginalWallet, and disconnect. Deferred (follow-ups, each only turns a correct-but-slow case faster, never a correctness risk): the banded full sweep for seeding on a pre-note-cache daemon, the external-import tripwire, and the weekly safety-net re-sweep. Part 2 (the daemon's decrypt cost) is already on the daemon's perf/sapling-note-cache branch. Pure helpers + cache round-trip + anchor-verify/large-window covered by tests; the getblockhash-after-sweep updates the pinned RPC sequences. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
73 lines
3.7 KiB
C++
73 lines
3.7 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
//
|
|
// Pure decision logic for persisting the tx-history snapshot: what counts as "changed", and when a write is
|
|
// due. No I/O, no clock, no sodium: unit-tested directly.
|
|
|
|
#pragma once
|
|
|
|
#include "wallet_state.h"
|
|
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
#include <vector>
|
|
|
|
namespace dragonx {
|
|
namespace data {
|
|
|
|
// 64-bit fingerprints of what a snapshot would contain.
|
|
struct HistorySnapshotSig {
|
|
std::uint64_t content = 0; // rows (confirmations by UI bucket) + wallet hash + key id
|
|
std::uint64_t slow = 0; // shielded scan heights + raw confirmations + tip: drift every block
|
|
std::size_t rows = 0; // rows that would be persisted (pending sends excluded)
|
|
};
|
|
|
|
// Order-independent over rows. `content` covers the persisted row fields (txid, type, amount bit pattern,
|
|
// timestamp, address, from_address, memo) with confirmations bucketed at the thresholds the UI and the refresh
|
|
// read off a stored row (< 0 conflicted, 0 pending, 1-9 counted, >= 10 confirmed, >= 100 mature), plus the
|
|
// wallet hash and `keyId` (a passphrase change must rewrite: the old ciphertext is unreadable with the new key).
|
|
// `slow` covers what changes every block: the per-address scan heights (entries replace() would persist), each
|
|
// row's raw confirmations, and the tip.
|
|
HistorySnapshotSig historySnapshotSignature(const std::vector<TransactionInfo>& transactions,
|
|
const std::unordered_set<std::string>& skipTxids,
|
|
const std::string& walletHash,
|
|
std::uint64_t keyId,
|
|
const std::unordered_map<std::string, int>& shieldedScanHeights,
|
|
int tipHeight,
|
|
const std::string& tipHash,
|
|
// Sent-sweep watermark: W/A/H_A move per sweep (folded into `slow`,
|
|
// written on the slow cadence and always on disconnect/lock/exit),
|
|
// the enrichment backlog into `content` (written within ~20 s), so an
|
|
// invalidation that only changes the watermark still reaches disk.
|
|
int sentHeight = -1,
|
|
int sentAnchorHeight = -1,
|
|
const std::string& sentAnchorHash = std::string(),
|
|
const std::unordered_set<std::string>& sentBacklog = {});
|
|
|
|
enum class HistoryStoreMode {
|
|
Coalesce, // routine refresh: content changes at most every kHistoryMinStoreIntervalSec, slow drift every
|
|
// kHistorySlowStoreIntervalSec
|
|
Now // user action / disconnect / lock / exit: store now if anything differs from the last submit
|
|
};
|
|
enum class HistoryStoreAction { Skip, Defer, Store };
|
|
|
|
constexpr double kHistoryMinStoreIntervalSec = 20.0;
|
|
constexpr double kHistorySlowStoreIntervalSec = 600.0;
|
|
|
|
struct HistoryStoreInput {
|
|
HistoryStoreMode mode = HistoryStoreMode::Coalesce;
|
|
bool haveStored = false; // a snapshot of this session (submitted, or the loaded one) is on disk
|
|
HistorySnapshotSig stored; // its signature
|
|
HistorySnapshotSig current;
|
|
double sinceLastSubmitSec = 1e9; // 1e9 = never submitted this session
|
|
};
|
|
|
|
HistoryStoreAction decideHistoryStore(const HistoryStoreInput& input);
|
|
|
|
} // namespace data
|
|
} // namespace dragonx
|