Program ID: zQWSrznKgcK8aHA4ry7xbSCdP36FqgUHj766YM3pwre
x0-zk-verifier provides on-chain verification of zero-knowledge proofs generated by the x0-zk-proofs WASM library. It validates Groth16 proofs over the Ristretto255 curve for confidential token operations.
Problem Statement
Token-2022’s ConfidentialTransfer extension encrypts balances using Twisted ElGamal, but the protocol still needs to enforce correctness constraints:
- Public key validity — The ElGamal public key is correctly derived from a secret scalar
- Zero balance — An encrypted balance is actually zero (required for account closure)
- Withdraw correctness — A withdrawal amount is valid against the encrypted balance
- Transfer correctness — Encrypted sender/receiver ciphertexts are consistent
Each of these requires a zero-knowledge proof that reveals nothing about the underlying amounts.
Proof Types
PubkeyValidityProof
Proves that an ElGamal public key P is correctly derived: P=s⋅H where s is the secret scalar and H is the Ristretto255 generator.
Size: 64 bytes
ZeroBalanceProof
Proves that an ElGamal ciphertext encrypts the value 0 without revealing the randomness.
Size: 96 bytes
WithdrawProof
Proves that withdrawing amount a from encrypted balance E is valid — i.e., the remaining balance is non-negative and bounded by 248−1.
Size: 160 bytes
TransferProof
Proves that a confidential transfer correctly re-encrypts the amount under the recipient’s public key, and the sender’s remaining balance is non-negative.
Size: 288 bytes
Instructions
verify_pubkey_validity
Verifies an ElGamal public key validity proof and stores the result in a ProofContext PDA.
verify_pubkey_validity(proof_data: Vec<u8>, elgamal_pubkey: [u8; 32])
verify_withdraw
Verifies a withdrawal proof and records the verified amount.
verify_withdraw(
proof_data: Vec<u8>,
amount: u64,
new_decryptable_balance: [u8; 36]
)
verify_zero_balance
Verifies that an encrypted balance is zero.
verify_zero_balance(proof_data: Vec<u8>)
verify_transfer
Verifies a confidential transfer proof and records the amount and recipient.
verify_transfer(
proof_data: Vec<u8>,
amount: u64,
recipient: Pubkey
)
State Account
ProofContext
Each verified proof creates a ProofContext PDA that serves as an on-chain receipt:
pub struct ProofContext {
pub version: u8,
pub proof_type: ProofType,
pub verified: bool,
pub owner: Pubkey,
pub verified_at: i64,
pub amount: Option<u64>,
pub recipient: Option<Pubkey>,
pub elgamal_pubkey: Option<[u8; 32]>,
pub mint: Pubkey,
pub token_account: Pubkey,
pub bump: u8,
}
PDA Seeds: ["proof-context", owner, mint]
Proof Freshness
Proof contexts expire after 300 seconds (PROOF_CONTEXT_FRESHNESS_SECONDS). The is_fresh() method checks:
current_time−verified_at≤Δproof=300s
This prevents replay attacks — a proof verified 5 minutes ago cannot be used to authorize a new transfer.
Amount Bounds
All proof amounts are bounded by:
0≤a≤248−1=281,474,976,710,655
This constraint comes from the Token-2022 ConfidentialTransfer extension’s use of 48-bit range proofs.
Verification Flow
Client x0-zk-proofs (WASM) Solana
│ │ │
│── generate_withdraw_proof ──▶│ │
│◀── { proofData, newBal } ───│ │
│ │ │
│── verify_withdraw(proof) ────────────────────────────▶│
│ │ x0-zk-verifier │
│ │ ┌── verify proof ───┤
│ │ │ store context │
│◀──────────── tx signature ───────────────────────────│
│ │ │
│── withdraw(amount) ─────────────────────────────────▶│
│ │ x0-token │
│ │ ┌── check context ──┤
│ │ │ is_fresh()? │
│ │ │ execute │
│◀──────────── confirmed ──────────────────────────────│
Error Codes
| Code | Name | Description |
|---|
0x1700 | ProofVerificationFailed | ZK proof did not verify |
0x1701 | InvalidProofData | Proof data format invalid |
0x1703 | InvalidProofType | Unknown proof type |
0x1704 | ProofExpired | Proof context older than 300s |
0x1710 | AmountTooLarge | Amount exceeds 248−1 |
0x1711 | InvalidElGamalPubkey | ElGamal public key invalid |
0x1713 | ProofSizeMismatch | Proof data wrong length |
0x1720 | ArithmeticOverflow | Integer overflow in verification |
Last modified on February 8, 2026