The exact order of checks
The contract runs these in order, and stops at the first failure — emitting anActionRejected(reason) and a Privacy Receipt:
Check the approved root & revocation state
The proof’s
approved_root must match the policy’s current root. Stale-version proofs are rejected — this is how revocation works.Verify the ZK proof on-chain
Run the UltraHonk verifier against the submitted proof and public inputs. This is real on-chain zero-knowledge verification.
Check the action context
Recompute
context_hash and action_id from the submitted action and confirm they match the proof’s public inputs — recipient, amount, asset, consuming contract, and network must all line up.Check the nullifier and action_id are unused
If either was seen before, this is a replay →
ReplayBlocked.Sequence diagram
When any check fails
The contract does not silently drop the transaction. It emits a typed rejection and a Privacy Receipt describing exactly what happened:The nullifier binds
intent_nonce into action_id, and action_id is consumed on-chain. A genuinely new payment requires a new intent nonce and a new proof — you cannot reuse a proof for a second payment.Next: claim-safety
Which of these checks the circuit proves, and which the contract enforces — the #1 rule.