age_check circuit generates a zero-knowledge proof that a user meets a minimum age threshold.
How It Works
The age verification system uses a circom zero-knowledge circuit (~1500 constraints) that proves:“I am at least N years old as of today, and this proof is bound to transaction T, and my identity commitment is C.”Without revealing:
- Your exact birth date
- Your passport or identity documents
- Any other personal information
Private
Birth date stays on your device, never shared
Binding
Proof is bound to a specific transaction intent
Non-Replayable
Cannot be reused for other transactions
The age_check Circuit
The circuit is implemented in circom and compiled to a Groth16 proof system. Let’s break down how it works.Circuit Inputs
The circuit has both private inputs (known only to the prover) and public inputs (verified on-chain):The
dobHash and identityCommitment were created during identity registration when the user scanned their passport. See the identity setup documentation for details.Step 1: Verify Date of Birth Hash
The circuit first verifies that the private birth date matches the committeddobHash:
Step 2: Parse Reference Date
The reference date (current date) is encoded as an integer like20260309 (March 9, 2026). The circuit parses it into year, month, and day:
Step 3: Range Checks
The circuit ensures all date components are within valid ranges to prevent malicious witnesses:Step 4: Compute Effective Age
The circuit computes the user’s age with month and day precision:rawAge = 2026 - 1998 = 28refMonthDay = 309,birthMonthDay = 415hasBirthdayPassed = 0(309 < 415)effectiveAge = 28 - 1 + 0 = 27✓ (birthday hasn’t happened yet this year)
Step 5: Check Age Threshold
The circuit verifies that the effective age meets the minimum requirement:effectiveAge < ageThreshold, the constraint fails and proof generation aborts.
Step 6: Bind Intent and Identity
Finally, the circuit binds the proof to the specific transaction intent and identity commitment:identityCommitment and intentHash are included as public inputs and cannot be omitted by the prover.
Creating an Age Proof (Wallet Side)
When a user attempts to purchase an age-restricted item, the wallet generates the proof automatically:Load identity secrets
Birth date, user salt, and identity commitment are loaded from secure storage (set during passport NFC scan).
Compute dobHash
Hash the birth date using Poseidon to match the committed value from identity registration.
Proof Generation (snarkjs)
The proof is generated client-side using snarkjs running in an invisible WebView:proofBytes: 256 bytes (G1 point A, G2 point B, G1 point C)publicInputsBytes: 128 bytes (4 public inputs × 32 bytes each)
Why WebView instead of native ZK libraries?
Why WebView instead of native ZK libraries?
snarkjs is the most mature and well-tested Groth16 implementation for circom circuits. While native Rust/Kotlin libraries exist, they lack the ecosystem maturity and would require significant porting effort. The WebView approach:
- Reuses battle-tested snarkjs code
- Supports all circom circuits without changes
- Maintains compatibility with web wallet implementations
- Generates proofs in ~2 seconds on modern mobile devices
Verifying the Proof (On-Chain)
The on-chain settlement contract verifies the age proof before transferring funds:Parse public inputs
Extract the 4 public inputs from the 128-byte blob: age threshold, reference date, identity commitment, and intent hash.
Verify ZK proof
Call Sui’s
groth16::verify with the age_check circuit’s verifying key. This cryptographically proves the user meets the age threshold.Verify intent hash
Recompute the intent hash from transaction parameters and ensure it matches the public input. This prevents proof replay.
Verify identity registration
Check that the identity commitment was registered on-chain during passport verification. This prevents fake identities.
Security Properties
The age verification system provides strong security guarantees:Zero-Knowledge
The merchant and blockchain see only that the user is ≥ N years old, not their exact birth date.
Non-Replayable
Each proof is cryptographically bound to the transaction intent hash and cannot be reused.
Identity-Bound
The proof is tied to the user’s on-chain identity commitment, preventing proof sharing.
Date Integrity
The birth date hash is committed during passport NFC verification, preventing lying.
Preventing Proof Replay
TheintentHash binding prevents several attack vectors:
Preventing Fake Identities
The on-chain settlement contract verifies thatidentityCommitment was registered via the identity registration flow:
- Scanning a passport via NFC
- Verifying the passport’s passive authentication (RSA signature)
- Submitting a ZK proof of the identity_registration circuit
User Experience Flow
User scans age-gated QR code
The wallet detects that the merchant requires age verification (e.g.,
ageGate: 21).Wallet shows age requirement
The review screen displays: “This merchant requires proof that you are 21 or older. Your exact birth date will not be shared.”
User confirms payment
The wallet automatically generates the age proof in the background (~2 seconds).
Privacy-first design: The wallet never asks for permission to access the birth date. It was already stored securely during identity registration, so age proofs are generated silently without additional user interaction.
Compliance and Regulations
The age verification system is designed to comply with regulations like:- 21+ alcohol purchases (US federal law)
- 18+ tobacco purchases (varies by jurisdiction)
- 18+ gambling (varies by jurisdiction)
- 13+ COPPA compliance (Children’s Online Privacy Protection Act)
Identity Verification
Identity Verification
All identity commitments are created by scanning government-issued passports via NFC. The passport’s passive authentication (RSA signature) is verified client-side, and a ZK proof of the identity_registration circuit is submitted on-chain.This meets the “reliable evidence” standard required by regulations like the UK Age Verification Act.
Audit Trail
Audit Trail
Every age-gated transaction emits an on-chain event with:
- Merchant DID
- Buyer’s identity commitment (pseudonymous)
- Age threshold
- Transaction timestamp
- ZK proof hash
No Centralized Storage
No Centralized Storage
Birth dates are stored only on the user’s device, never on backend servers or smart contracts. This eliminates data breach risks and complies with GDPR’s data minimization principle.
Performance
| Metric | Value |
|---|---|
| Circuit constraints | ~1,500 |
| Proof generation time (mobile) | ~2 seconds |
| Proof size | 256 bytes |
| Public inputs size | 128 bytes (4 × 32 bytes) |
| On-chain verification gas |
Next Steps
Identity Setup
Learn how to scan passports and register identities
Merchant Payments
See how age proofs fit into the full checkout flow
Circuits Reference
Explore the age_check circuit in detail
