Verification Flow
This document provides a detailed walkthrough of the zkIdentity verification process, covering each of the 10 steps from initial user action through final on-chain attestation. It also documents what data is recorded at each stage, verification expiry policies, and troubleshooting guidance.
Overview
The verification flow transforms a user's raw KYC result into a privacy-preserving, on-chain attestation. The entire process typically completes in 1-3 minutes, depending on the KYC provider's processing time.
User ─── 1. Select Provider ──── Frontend
│
2. Create Session ──── Attestor ──── KYC Provider
│
3. Complete Verification ──── User ──── KYC Provider
│
4. Provider Webhook ──── KYC Provider ──── Attestor
│
5. zkFetch TLS Tunnel ──── Attestor ──── KYC Provider
│
6. Generate 14 ZK Proofs ──── Attestor (TEE)
│
7. Extract Non-PII Metadata ──── Attestor (TEE)
│
8. Submit to Cartesi ──── Attestor ──── Cartesi Rollup
│
9. Store Attestation ──── Cartesi Rollup ──── Arbitrum
│
10. User Sees "Verified" ──── Frontend ──── User
The 10 Steps
Step 1: User Selects Provider
Actor: User (via Frontend)
The user connects their wallet through Privy and is presented with available KYC providers. The frontend determines which providers to display based on the user's selected country:
- African countries (NG, KE, ZA, GH, UG, TZ, etc.): Smile ID
- US, CA, UK, EU: Plaid
The user selects their country and provider. The frontend derives the user's DID (did:key) from their wallet's secp256k1 public key and prepares a session request.
Data at this stage:
- DID (derived locally, never sent to a central registry)
- Selected provider
- Selected country
Step 2: Attestor Creates Session
Actor: Attestor (TEE)
The frontend sends a session creation request to the attestor:
{
"did": "did:key:z6MkhRXz...",
"provider": "smile_id",
"country": "NG",
"signature": "0x..." // Wallet signature proving DID ownership
}
The attestor:
- Verifies the wallet signature to confirm DID ownership.
- Generates a unique session ID.
- Calls the KYC provider's API to create a verification session.
- Returns the session ID and provider-specific configuration (e.g., Smile ID widget URL or Plaid Link token) to the frontend.
Duration: < 1 second
Step 3: User Completes Verification
Actor: User (via KYC Provider UI)
The user interacts directly with the KYC provider's verification interface:
- Smile ID: The user uploads a photo of their identity document (national ID, passport, or driver's license) and takes a selfie for biometric matching. SmartSelfie performs liveness detection to prevent spoofing.
- Plaid: The user logs into their bank account through Plaid Link. Plaid retrieves identity information from the bank's records.
During this step, PII flows directly between the user and the KYC provider. The attestor does not see the user's documents, selfies, or bank credentials.
Duration: 30 seconds - 5 minutes (depends on user interaction speed and provider processing time)
Step 4: Provider Webhook
Actor: KYC Provider -> Attestor
After the provider completes processing, it sends a webhook notification to the attestor:
POST https://attestor.yourdomain.com/webhooks/smile-id/{session_id}
The webhook payload contains the verification result. The attestor:
- Verifies the webhook signature to confirm it came from the legitimate provider.
- Checks that the session ID is valid and has not expired.
- Records the webhook receipt timestamp.
- Initiates the proof generation pipeline.
Duration: Instant (provider delivery time varies)
Step 5: zkFetch TLS Tunnel
Actor: Attestor (TEE) -> KYC Provider API
Rather than relying solely on the webhook payload, the attestor independently fetches the verification result from the provider's API using Reclaim Protocol's zkFetch. This creates a cryptographic record:
- zkFetch establishes a TLS 1.3 connection to the provider's API.
- The attestor sends an authenticated API request for the verification result.
- The TLS session is recorded, capturing cryptographic artifacts needed for proof generation.
- The provider's response is received and held in TEE memory.
This step is critical because it allows ZK proofs to be generated over a fresh, verifiable TLS session rather than relying on a webhook payload that could be tampered with.
Duration: 1-3 seconds
Step 6: Generate 14 ZK Proofs
Actor: Attestor (TEE)
Using the cryptographic artifacts from the zkFetch TLS session, the attestor generates 14 ZK proofs. These proofs are organized into three categories:
| Category | Proofs | Purpose |
|---|---|---|
| TLS Authenticity | #1-4 | Prove the response came from the real provider over genuine TLS |
| Content Claims | #5-11 | Prove specific facts about the verification result without revealing the full response |
| Integrity | #12-14 | Bind the proofs to the user's DID and ensure bundle consistency |
Each proof is a cryptographic object that can be independently verified. Together, they form a complete attestation that the user passed KYC with a specific provider in a specific country.
See ZK Proof System for a detailed breakdown of each proof.
Duration: 10-30 seconds
Step 7: Extract Non-PII Metadata
Actor: Attestor (TEE)
The attestor extracts a minimal set of non-PII metadata from the provider's response. This metadata is the only information that will be stored on-chain:
{
"status": "verified",
"provider": "smile_id",
"country": "NG",
"documentType": "national_id",
"livenessCheck": "passed",
"timestamp": 1700000000
}
Simultaneously, the attestor encrypts any document references (CIDs pointing to provider-stored documents) using AES-256-GCM with a key derived from the user's wallet public key. The encrypted blob is uploaded to IPFS via Pinata.
Duration: 1-5 seconds (including IPFS upload)
Step 8: Submit to Cartesi Rollup
Actor: Attestor -> Cartesi Rollup
The attestor packages the attestation and submits it as an input to the Cartesi rollup:
{
"type": "attestation",
"did": "did:key:z6MkhRXz...",
"provider": "smile_id",
"country": "NG",
"status": "verified",
"documentType": "national_id",
"livenessCheck": "passed",
"timestamp": 1700000000,
"proofHashes": ["0xabc...", "0xdef...", "..."],
"ipfsCid": "QmXyz...",
"attestorSignature": "0x..."
}
The Cartesi rollup's RISC-V virtual machine:
- Verifies the attestor's signature (confirming it came from a genuine TEE).
- Validates all 14 ZK proofs.
- Checks the metadata is consistent with the proofs.
- Stores the attestation in the rollup state.
- Emits a notice (event) confirming the attestation.
Duration: < 1 second (rollup processing)
Step 9: Attestation Stored On-Chain
Actor: Cartesi Rollup -> Arbitrum
The attestation is now part of the Cartesi rollup state. Periodically, the rollup's state is committed to Arbitrum through the standard Cartesi rollup mechanism:
- The rollup node computes a state hash.
- The state hash is submitted to the Arbitrum rollup contract.
- After the dispute period, the state is finalized.
Once finalized, the attestation is anchored to Arbitrum with Ethereum-equivalent security.
Duration: State commitment happens in batches. Finality depends on the rollup's epoch configuration (typically minutes to hours).
Step 10: User Sees "Verified"
Actor: Frontend -> User
The frontend polls the attestor's status endpoint throughout the process. Once the Cartesi rollup confirms the attestation, the attestor updates the session status to "verified":
{
"sessionId": "abc-123",
"status": "verified",
"did": "did:key:z6MkhRXz...",
"provider": "smile_id",
"country": "NG",
"attestationTx": "0x...",
"explorerUrl": "https://explorer.cartesi.io/..."
}
The frontend displays the verified status along with links to view the attestation on-chain.
Duration: Instant (once rollup confirms)
What Gets Recorded
The following table summarizes what data is stored at each layer:
| Data | Frontend | Attestor (TEE) | KYC Provider | IPFS | Cartesi Rollup | Arbitrum |
|---|---|---|---|---|---|---|
| Wallet address | Session only | Session only | No | No | No | No |
| DID | Session only | Session only | No | No | Yes | State root |
| Name, DOB, etc. | Never | Never persisted | Yes (their policy) | No | No | No |
| Document images | Never | Never persisted | Yes (their policy) | No | No | No |
| Verification status | Displayed | Session only | Yes | No | Yes | State root |
| Provider name | Displayed | Session only | N/A | No | Yes | State root |
| Country code | Displayed | Session only | Yes | No | Yes | State root |
| ZK proof hashes | Optional display | Transient | No | No | Yes | State root |
| Encrypted doc refs | Never | Transient | No | Yes (encrypted) | CID only | State root |
Verification Expiry
Attestations do not have a built-in cryptographic expiry. However, the system supports application-level expiry policies:
Default Policy
- Verification validity: 365 days from the verification timestamp.
- Grace period: 30 days after expiry during which the user can re-verify without losing their attestation history.
- Re-verification: After expiry, the user must complete the full verification flow again to obtain a new attestation.
Custom Policies
Applications consuming zkIdentity attestations can enforce their own expiry policies by checking the timestamp field in the attestation:
const attestation = await rollup.getAttestation(did);
const ageInDays = (Date.now() / 1000 - attestation.timestamp) / 86400;
if (ageInDays > 365) {
// Attestation expired -- require re-verification
}
Expiry Notifications
The frontend displays the attestation's age and upcoming expiry. Users receive a notification when their attestation is within 30 days of the default expiry.
Status States
A verification session can be in one of the following states:
| Status | Description |
|---|---|
created | Session created, waiting for user to begin provider verification |
provider_pending | User has initiated provider verification, waiting for completion |
provider_completed | Provider has returned a result (success or failure) |
proof_generating | zkFetch and ZK proof generation in progress |
proof_generated | All 14 ZK proofs successfully generated |
submitting | Attestation being submitted to Cartesi rollup |
verified | Attestation confirmed on rollup; verification successful |
rejected | Provider returned a negative result; verification failed |
expired | Session TTL exceeded before completion |
error | An error occurred during processing |
Troubleshooting the Verification Flow
Session Stuck at "provider_pending"
Likely cause: The user has not completed the provider's verification flow, or the provider's webhook has not been delivered.
Resolution:
- Check if the user completed the provider flow (visible in the provider's dashboard).
- Verify the webhook URL is reachable from the provider's servers.
- Check the attestor logs for webhook receipt.
- If the provider shows "completed" but no webhook arrived, manually trigger a result fetch.
Session Stuck at "proof_generating"
Likely cause: ZK proof generation is taking longer than expected, or has failed.
Resolution:
- Check attestor logs for proof generation errors.
- Verify the TEE has sufficient resources (CPU and memory).
- Check connectivity to the provider's API (zkFetch needs to reach the provider).
- If proofs fail repeatedly, restart the attestor and retry.
Session Shows "error"
Likely cause: An unrecoverable error occurred during processing.
Resolution:
- Check the session's error details via the attestor API:
GET /api/sessions/{session_id} - Common error causes:
- Provider API returned an unexpected format
- IPFS upload failed
- Cartesi rollup submission failed
- Attestor wallet has insufficient funds
- Address the root cause and ask the user to re-verify.
Session Shows "expired"
Likely cause: The session TTL (default: 1 hour) expired before the verification flow completed.
Resolution:
- The user must start a new verification.
- If sessions frequently expire, consider increasing
SESSION_TTLin the configuration. - Check if the provider's processing time is unusually long.
Related Documentation
- Architecture Overview -- System architecture context.
- ZK Proof System -- Details on Step 6 (proof generation).
- TEE Attestor -- Details on the attestor's role.
- Monitoring -- Monitoring verification success rates.
- Troubleshooting -- Comprehensive troubleshooting guide.