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
+12
View File
@@ -131,8 +131,17 @@ static async Task<int> Sync(string[] o)
var sync = new WalletSynchronizer(account, client, doc.GapLimit);
sync.Progress += msg => Console.WriteLine($" {msg}");
var net = PalladiumNetworks.For(account.Profile.Kind);
sync.PreloadCaches(
doc.Cache?.RawTxHex ?? [],
doc.Cache?.VerifiedAt ?? [],
doc.Cache?.BlockHeaders,
doc.Cache?.NextReceiveIndex ?? 0,
doc.Cache?.NextChangeIndex ?? 0,
net);
var result = await sync.SyncOnceAsync();
var (rawHex, verifiedAt, blockHeaders) = sync.ExportCaches(net);
doc.Cache = new SyncCache
{
TipHeight = result.TipHeight,
@@ -143,6 +152,9 @@ static async Task<int> Sync(string[] o)
History = [.. result.History],
Utxos = [.. result.Utxos],
Addresses = [.. result.AddressRows],
RawTxHex = rawHex,
VerifiedAt = verifiedAt,
BlockHeaders = blockHeaders,
};
WalletStore.Save(doc, path, Opt(o, "--password"));