feat(spv): verify Merkle proofs progressively, gate spendability on it

Balance/history now render as soon as tx downloads finish instead of
blocking on every historical Merkle proof, critical for mobile where
proof-checking can take much longer than the download itself. Proofs
continue to be checked in the background and each tx's Verified flag
catches up progressively; header ranges are now fetched in batches
(blockchain.block.headers) instead of one call per header to keep this
fast over high-latency links.

Coin selection (UtxoSpendability.IsSpendable) refuses to spend a UTXO
until its Merkle proof is actually checked, regardless of confirmation
count, so a server that fabricates a confirmed balance can get it
displayed early but never spent before the forgery is caught. The disk
cache only ever persists the fully-verified end state of a sync.

UI surfaces the new PendingVerificationSats/SpendableSats split with a
"verifying..." badge, and the sync save now runs off the UI thread to
avoid freezing on slower hardware.
This commit is contained in:
2026-07-17 19:41:31 +02:00
parent feb765663c
commit 3460e53b4f
16 changed files with 487 additions and 82 deletions
@@ -102,7 +102,8 @@ public sealed class TransactionDetailsViewModel
{
if (d.Confirmations <= 0)
return loc["tx.status.mempool"];
return $"{d.Confirmations} {loc["tx.status.confirmations"]} ({loc["tx.status.block"]} {d.Height})";
var status = $"{d.Confirmations} {loc["tx.status.confirmations"]} ({loc["tx.status.block"]} {d.Height})";
return d.Verified ? status : $"{status} — {loc["history.unverified"]}";
}
private string Signed(long sats)