Snapshot Fraud Proofs¶
A posted snapshot root is an optimistic claim. It is final only if unchallenged for SNAPSHOT_CHALLENGE_WINDOW_SECS (1 day) OR survives every challenge. Fraud is proven on-chain by one of five predicates in the FraudProof enum.
Bond economics¶
| Actor | Bond | Outcome on valid fraud | Outcome on false challenge |
|---|---|---|---|
Poster (on post_snapshot) |
1 × max-appeal-fee |
forfeited (+ challenger's bond → challenger) | returned (on finalize_snapshot) |
Challenger (on challenge_snapshot) |
equal to poster's | returned + receives poster's bond | forfeited → poster |
Both sweeps are PDA-signed out of the Subaccord vault. Wrong root ⇒ Snapshot.status = Voided (never drawable). False challenge ⇒ snapshot stays Posted (window still open).
Predicates¶
| # | Predicate | What it catches | Witness | Resolution |
|---|---|---|---|---|
| 1 | Duplicate |
Two leaves at different indices with the same juror pubkey, both verifying against the root. | leaf_a, proof_a, index_a, leaf_b, proof_b, index_b |
void |
| 2 | Omission |
The snapshot dropped a staked juror. | Two adjacent sorted leaves bracketing challenger.key() (leaf_lo.juror < challenger < leaf_hi.juror, consecutive indices) + challenger's JurorStake (remaining_accounts[0]) showing last_change_slot < anchor_slot && amount > 0. |
void |
| 3 | WrongStake |
A leaf's stake ≠ the juror's actual anchor-time stake. |
leaf, proof, index + the juror's JurorStake (remaining_accounts[0]) with last_change_slot < anchor_slot ⇒ live amount is the anchor-time amount; require amount != leaf.stake. |
void |
| 4 | Inflation (at draw) |
Leaf overstates a juror's stake. | Enforced in draw, not challenge_snapshot: JurorStake.amount ≥ leaf.stake. Reads live state — race-immune. |
draw reverts InflatedStake |
| 5 | NotSorted |
Tree not sorted by juror pubkey ascending (breaks omission proofs). | Two leaves index_lo < index_hi with leaf_lo.juror > leaf_hi.juror, both verifying against the root. |
void |
Predicates 1, 2, 3, 5 fire in challenge_snapshot (window-gated). Predicate 4 fires in draw (every draw, every leaf).
Anchor-slot witness¶
JurorStake.last_change_slot is the slot of the most recent stake/unstake. If last_change_slot < Snapshot.anchor_slot, the live JurorStake.amount equals the anchor-time amount — the live account is its own historical witness. No ring buffer, no epoch snapshot. Closes the TOCTOU gap for predicates 2 and 3.
sequenceDiagram
participant P as Poster
participant V as Vault
participant C as Challenger
participant S as Snapshot
P->>V: post_snapshot (bond 1×max-appeal-fee)
S-->>S: status=Posted, anchor_slot=now
C->>V: challenge_snapshot (equal bond) + FraudProof
alt valid fraud
V->>C: 2× bond (poster's + challenger's)
S-->>S: status=Voided
else false challenge
V->>P: challenger's bond
S-->>S: status stays Posted
end
Why: ADR-0003, ADR-0008. Sortition consumption: sortition & VRF.