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:
@@ -33,12 +33,28 @@ public class TransactionFactoryTests
|
||||
{
|
||||
Txid = txid, Vout = 0, ValueSats = sats,
|
||||
Address = account.GetReceiveAddress(0).ToString(),
|
||||
IsChange = false, AddressIndex = 0, Height = 100,
|
||||
IsChange = false, AddressIndex = 0, Height = 100, Verified = true,
|
||||
},
|
||||
};
|
||||
return (utxos, new Dictionary<string, Transaction> { [txid] = funding });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Un_utxo_confermato_ma_non_ancora_verificato_non_e_spendibile()
|
||||
{
|
||||
// Confirmed by the server, but its Merkle proof hasn't been checked yet (progressive
|
||||
// background verification, §7.4): must never be treated as spendable — otherwise a
|
||||
// malicious server could get a fabricated balance spent before the forgery is caught.
|
||||
var account = Account();
|
||||
var (utxos, txs) = Fund(account, 1_000_000);
|
||||
utxos[0].Verified = false;
|
||||
|
||||
var ex = Assert.Throws<WalletSpendException>(() => new TransactionFactory(account).Build(
|
||||
utxos, txs, account.GetReceiveAddress(5), amountSats: 400_000,
|
||||
feeRateSatPerVByte: 2, changeIndex: 0, tipHeight: 100));
|
||||
Assert.Contains("awaiting Merkle-proof verification", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Una_spesa_firmata_verifica_e_paga_la_fee_attesa()
|
||||
{
|
||||
@@ -169,7 +185,7 @@ public class TransactionFactoryTests
|
||||
{
|
||||
new() { Txid = txid, Vout = 0, ValueSats = 1_000_000,
|
||||
Address = mainnetAccount.GetReceiveAddress(0).ToString(),
|
||||
IsChange = false, AddressIndex = 0, Height = 100, IsCoinbase = false },
|
||||
IsChange = false, AddressIndex = 0, Height = 100, IsCoinbase = false, Verified = true },
|
||||
};
|
||||
var txs = new Dictionary<string, Transaction> { [txid] = funding };
|
||||
|
||||
@@ -295,7 +311,7 @@ public class TransactionFactoryTests
|
||||
{
|
||||
Txid = txid, Vout = 0, ValueSats = 300_000,
|
||||
Address = account.GetReceiveAddress(i).ToString(),
|
||||
IsChange = false, AddressIndex = i, Height = 100,
|
||||
IsChange = false, AddressIndex = i, Height = 100, Verified = true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user