perf(spv): incremental discovery, parallel chains, persistent header cache

Speed up wallet synchronization, especially re-syncs of large wallets:

- Incremental address discovery: known indices from the previous sync
  (NextReceiveIndex/NextChangeIndex) are fetched in a single parallel
  burst instead of sequential gap-limit rounds, cutting round-trips from
  O(used/gapLimit) to ~O(1). Receive and change chains scan in parallel.
- Merge tx download and Merkle verification into one parallel phase
  (no barrier between them); the per-call semaphore is dropped in favor
  of the transport-level in-flight gate.
- Persist verified block headers (SyncCache.BlockHeaders, height -> hex):
  immutable once stored, so blockchain.block.header round-trips for
  already-verified proofs are eliminated on later syncs.
- RetryOnBusyAsync with exponential backoff around server calls, treating
  -101/-102 and "server busy"/"excessive resource usage" as transient.

Wire the new PreloadCaches/ExportCaches signatures through the desktop
view model and the CLI sync command.
This commit is contained in:
2026-06-16 09:27:40 +02:00
parent 38e0f0a52e
commit 5ff2075a45
4 changed files with 227 additions and 148 deletions
@@ -221,6 +221,9 @@ public partial class MainWindowViewModel
_synchronizer.PreloadCaches(
_doc.Cache?.RawTxHex ?? [],
_doc.Cache?.VerifiedAt ?? [],
_doc.Cache?.BlockHeaders,
_doc.Cache?.NextReceiveIndex ?? 0,
_doc.Cache?.NextChangeIndex ?? 0,
net);
_synchronizer.Progress += msg => Dispatcher.UIThread.Post(() => StatusMessage = msg);
}
@@ -231,7 +234,7 @@ public partial class MainWindowViewModel
var result = await _synchronizer.SyncOnceAsync();
_lastTransactions = result.Transactions;
var (rawHex, verifiedAt) = _synchronizer.ExportCaches(
var (rawHex, verifiedAt, blockHeaders) = _synchronizer.ExportCaches(
PalladiumNetworks.For(_account.Profile.Kind));
_doc.Cache = new SyncCache
{
@@ -245,6 +248,7 @@ public partial class MainWindowViewModel
Addresses = [.. result.AddressRows],
RawTxHex = rawHex,
VerifiedAt = verifiedAt,
BlockHeaders = blockHeaders,
};
WalletStore.Save(_doc, _walletPath!, _password);
ApplyCache(_doc.Cache);
@@ -334,11 +338,12 @@ public partial class MainWindowViewModel
try
{
var net = PalladiumNetworks.For(_account.Profile.Kind);
var (rawHex, verifiedAt) = _synchronizer.ExportCaches(net);
var (rawHex, verifiedAt, blockHeaders) = _synchronizer.ExportCaches(net);
if (rawHex.Count == 0 && verifiedAt.Count == 0)
return;
(_doc.Cache ??= new SyncCache()).RawTxHex = rawHex;
_doc.Cache.VerifiedAt = verifiedAt;
_doc.Cache.BlockHeaders = blockHeaders;
WalletStore.Save(_doc, _walletPath, _password);
}
catch { /* non fatale: il prossimo salvataggio completo recupererà */ }