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
+10 -1
View File
@@ -317,6 +317,9 @@
<TextBlock Text="{Binding ImmatureText}" Foreground="#FCD34D"
TextWrapping="Wrap"
IsVisible="{Binding ImmatureText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<TextBlock Text="{Binding VerifyingText}" Foreground="#FCD34D"
TextWrapping="Wrap"
IsVisible="{Binding VerifyingText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<TextBlock Text="{Binding NetworkInfo}" Classes="on-hero" FontSize="12"
Margin="0,2,0,0"/>
</StackPanel>
@@ -373,13 +376,16 @@
<DataTemplate x:DataType="vm:HistoryRow">
<Panel Cursor="Hand">
<!-- Desktop: 3 fixed columns -->
<Grid ColumnDefinitions="90,160,*"
<Grid ColumnDefinitions="90,160,*,Auto"
IsVisible="{Binding $parent[UserControl].((vm:MainWindowViewModel)DataContext).IsDesktop}">
<TextBlock Grid.Column="0" Text="{Binding Conferma}" Foreground="{DynamicResource TextSecondaryBrush}"/>
<TextBlock Grid.Column="1" Text="{Binding Importo}" FontFamily="monospace"/>
<TextBlock Grid.Column="2" Text="{Binding Txid}"
FontFamily="monospace" FontSize="12"
TextTrimming="CharacterEllipsis"/>
<TextBlock Grid.Column="3" Text="{Binding $parent[UserControl].((vm:MainWindowViewModel)DataContext).Loc[history.unverified]}"
Foreground="#FCD34D" FontSize="11" Margin="6,0,0,0"
IsVisible="{Binding !Verified}"/>
</Grid>
<!-- Mobile: vertical card -->
<StackPanel Spacing="2"
@@ -389,6 +395,9 @@
<TextBlock Text="{Binding Conferma}" Foreground="{DynamicResource TextSecondaryBrush}" FontSize="11"/>
<TextBlock Text="{Binding Txid}" FontFamily="monospace" FontSize="11"
TextTrimming="CharacterEllipsis"/>
<TextBlock Text="{Binding $parent[UserControl].((vm:MainWindowViewModel)DataContext).Loc[history.unverified]}"
Foreground="#FCD34D" FontSize="11"
IsVisible="{Binding !Verified}"/>
</StackPanel>
</Panel>
</DataTemplate>