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:
+6
-2
@@ -104,8 +104,9 @@ static int Info(string[] o)
|
||||
Console.WriteLine($"xpub: {doc.AccountXpub}");
|
||||
if (doc.Cache is { } cache)
|
||||
{
|
||||
Console.WriteLine($"balance: {CoinAmount.Format(cache.ConfirmedSats - cache.ImmatureSats, account.Profile.CoinUnit)} spendable"
|
||||
Console.WriteLine($"balance: {CoinAmount.Format(cache.SpendableSats, account.Profile.CoinUnit)} spendable"
|
||||
+ (cache.ImmatureSats != 0 ? $" + {CoinAmount.Format(cache.ImmatureSats)} maturing (not spendable)" : "")
|
||||
+ (cache.PendingVerificationSats != 0 ? $" + {CoinAmount.Format(cache.PendingVerificationSats)} awaiting SPV verification (not spendable)" : "")
|
||||
+ (cache.UnconfirmedSats != 0 ? $" + {CoinAmount.Format(cache.UnconfirmedSats)} pending confirmation (not spendable)." : ""));
|
||||
Console.WriteLine($"sync: height {cache.TipHeight}, {cache.History.Count} transactions");
|
||||
Console.WriteLine($"receive: {account.GetReceiveAddress(cache.NextReceiveIndex)}");
|
||||
@@ -149,6 +150,8 @@ static async Task<int> Sync(string[] o)
|
||||
ConfirmedSats = result.ConfirmedSats,
|
||||
UnconfirmedSats = result.UnconfirmedSats,
|
||||
ImmatureSats = result.ImmatureSats,
|
||||
PendingVerificationSats = result.PendingVerificationSats,
|
||||
SpendableSats = result.SpendableSats,
|
||||
NextReceiveIndex = result.NextReceiveIndex,
|
||||
NextChangeIndex = result.NextChangeIndex,
|
||||
History = [.. result.History],
|
||||
@@ -160,8 +163,9 @@ static async Task<int> Sync(string[] o)
|
||||
};
|
||||
WalletStore.Save(doc, path, Opt(o, "--password"));
|
||||
|
||||
Console.WriteLine($"Balance: {CoinAmount.Format(result.ConfirmedSats - result.ImmatureSats, account.Profile.CoinUnit)} spendable"
|
||||
Console.WriteLine($"Balance: {CoinAmount.Format(result.SpendableSats, account.Profile.CoinUnit)} spendable"
|
||||
+ (result.ImmatureSats != 0 ? $" + {CoinAmount.Format(result.ImmatureSats)} maturing (not spendable)" : "")
|
||||
+ (result.PendingVerificationSats != 0 ? $" + {CoinAmount.Format(result.PendingVerificationSats)} awaiting SPV verification (not spendable)" : "")
|
||||
+ (result.UnconfirmedSats != 0 ? $" + {CoinAmount.Format(result.UnconfirmedSats)} pending confirmation (not spendable)" : ""));
|
||||
Console.WriteLine($"History ({result.History.Count}):");
|
||||
foreach (var tx in result.History)
|
||||
|
||||
Reference in New Issue
Block a user