feat(app): flag transactions above the newest checkpoint as not anchored

Blocks mined after the newest hardcoded checkpoint cannot be chained to it,
so only the Merkle proof against the server's header backs them. The
history list and the transaction detail now show a 'not anchored' label
for those transactions. Informational only: spending is not blocked.
See ADR 0041.
This commit is contained in:
davide committed 2026-11-03 15:11:06 +01:00
1 parent 24368a07b5
commit 305f22ed0e
13 files changed
+119 -7

No files matched your search

+1 -1
View File
@@ -21,7 +21,7 @@ It does **not** protect against:
This wallet is an SPV client, not a full node. It validates:
- Block headers: `SkipPowValidation` is enabled because LWMA difficulty cannot be recomputed client-side, so proof of work is never checked. Instead, every header used for a Merkle proof is hash-chain-linked (prev-hash) **up to the nearest hardcoded checkpoint at or above its height**, and the header at that checkpoint height must hash to the checkpoint exactly (`Core/Chain/ChainProfiles.cs`, enforced in `Core/Spv/WalletSynchronizer.AnchorToCheckpointAsync`, ADR 0040). The direction matters: with PoW skipped, a chain built *from* a checkpoint *up to* a header proves nothing, because anyone can fabricate such a chain; a chain ending in a known hash cannot be forged without inverting SHA-256. Mainnet checkpoints run every 20,000 blocks from 20,000 (so even the oldest blocks are covered) and are extended at each release. **Known limit:** a transaction in a block *above the highest checkpoint* has no checkpoint to anchor to, so only its Merkle proof against the header the server supplied is checked there; the window is the blocks mined since the last release. Testnet/regtest have no checkpoints yet, so headers there are trusted at face value
- Block headers: `SkipPowValidation` is enabled because LWMA difficulty cannot be recomputed client-side, so proof of work is never checked. Instead, every header used for a Merkle proof is hash-chain-linked (prev-hash) **up to the nearest hardcoded checkpoint at or above its height**, and the header at that checkpoint height must hash to the checkpoint exactly (`Core/Chain/ChainProfiles.cs`, enforced in `Core/Spv/WalletSynchronizer.AnchorToCheckpointAsync`, ADR 0040). The direction matters: with PoW skipped, a chain built *from* a checkpoint *up to* a header proves nothing, because anyone can fabricate such a chain; a chain ending in a known hash cannot be forged without inverting SHA-256. Mainnet checkpoints run every 20,000 blocks from 20,000 (so even the oldest blocks are covered) and are extended at each release. **Known limit:** a transaction in a block *above the highest checkpoint* has no checkpoint to anchor to, so only its Merkle proof against the header the server supplied is checked there; the window is the blocks mined since the last release. The wallet flags such transactions as **not anchored** in the history list and in the transaction detail (ADR 0041), so the reduced guarantee is visible instead of silent. Testnet/regtest have no checkpoints yet, so headers there are trusted at face value
- Transaction inclusion in a confirmed block (Merkle branch proof, mandatory for every confirmed transaction — see `Core/Spv/MerkleProof.cs`)
It does **not** validate: