Architecture
How OpenZKTool implements zero-knowledge proofs on Stellar Soroban
System Overview
┌─────────────────────────────────────────────────────────────────┐
│ OpenZKTool Architecture │
└─────────────────────────────────────────────────────────────────┘
┌──────────────┐ ┌──────────────┐ ┌────────────┐
│ Off-Chain │ │ Stellar │ │ On-Chain │
│ (Client) │ ───> │ Network │ ───> │ (Soroban) │
└──────────────┘ └──────────────┘ └────────────┘
│ │ │
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ Circuit │ │ Proof │ │Verifier │
│Compiler │ │ (800B) │ │Contract │
└─────────┘ └─────────┘ └─────────┘
Three-Layer Design
1. Circuit Layer (Off-Chain)
Purpose: Define what you want to prove
// circuits/kyc_transfer.circom
template KYCTransfer() {
// Private inputs (never on-chain)
signal input age;
signal input balance;
signal input country;
// Public inputs (visible on-chain)
signal input minAge;
signal input minBalance;
signal input allowedCountries[10];
// Public output
signal output kycValid;
// Constraints
component ageCheck = GreaterEqThan(8);
component balanceCheck = GreaterEqThan(32);
component countryCheck = IsInArray(10);
ageCheck.in[0] <== age;
ageCheck.in[1] <== minAge;
balanceCheck.in[0] <== balance;
balanceCheck.in[1] <== minBalance;
countryCheck.value <== country;
countryCheck.array <== allowedCountries;
kycValid <== ageCheck.out * balanceCheck.out * countryCheck.out;
}
Compilation:
circom kyc_transfer.circom --r1cs --wasm --sym
# Generates:
# - kyc_transfer.r1cs (constraints)
# - kyc_transfer.wasm (witness generator)
# - kyc_transfer.sym (symbols for debugging)
Trusted Setup:
# Phase 1: Powers of Tau (can be reused)
snarkjs powersoftau new bn128 12 pot12_0000.ptau
snarkjs powersoftau contribute pot12_0000.ptau pot12_final.ptau
# Phase 2: Circuit-specific
snarkjs groth16 setup kyc_transfer.r1cs pot12_final.ptau circuit_0000.zkey
snarkjs zkey contribute circuit_0000.zkey circuit_final.zkey
# Export verification key
snarkjs zkey export verificationkey circuit_final.zkey verification_key.json
2. Proof Generation Layer (Client-Side)
Purpose: Generate proofs from private data
import { groth16 } from "snarkjs";
async function generateProof(privateData: any) {
// 1. Prepare inputs
const inputs = {
// Private
age: privateData.age,
balance: privateData.balance,
country: privateData.countryId,
// Public
minAge: 18,
minBalance: 500,
allowedCountries: [1, 2, 3, 0, 0, 0, 0, 0, 0, 0]
};
// 2. Generate witness
const { proof, publicSignals } = await groth16.fullProve(
inputs,
"kyc_transfer.wasm",
"circuit_final.zkey"
);
// 3. Serialize for Soroban
const proofBytes = serializeProof(proof);
const pubInputsBytes = serializePublicInputs(publicSignals);
return { proofBytes, pubInputsBytes };
}
Proof Structure:
Groth16 Proof (256 bytes)
├── A: G1 point (64 bytes)
├── B: G2 point (128 bytes)
└── C: G1 point (64 bytes)
Public Inputs (variable)
├── kycValid: 1 field element (32 bytes)
├── minAge: 1 field element (32 bytes)
├── minBalance: 1 field element (32 bytes)
└── allowedCountries: 10 field elements (320 bytes)
Total: ~800 bytes
3. Verification Layer (Stellar Soroban)
Purpose: Verify proofs on-chain
// contracts/src/lib.rs
#![no_std]
use soroban_sdk::{contract, contractimpl, contracttype, Env, Vec, Bytes};
mod groth16;
use groth16::{verify_groth16, Proof, PublicInputs};
#[contract]
pub struct Groth16Verifier;
#[contractimpl]
impl Groth16Verifier {
/// Verify a Groth16 proof
pub fn verify(
env: Env,
proof_bytes: Bytes,
public_inputs_bytes: Bytes
) -> bool {
// 1. Deserialize proof
let proof = Proof::from_bytes(&proof_bytes);
// 2. Deserialize public inputs
let public_inputs = PublicInputs::from_bytes(&public_inputs_bytes);
// 3. Verify using Groth16 algorithm
verify_groth16(&proof, &public_inputs)
}
}
Groth16 Verification Algorithm
The verifier implements the pairing equation:
e(A, B) = e(α, β) · e(C, γ) · e(∑ᵢ (pubInputᵢ · ICᵢ), δ)
Implementation Steps:
Step 1: Parse Verification Key
pub struct VerificationKey {
alpha_g1: G1Affine, // α in G1
beta_g2: G2Affine, // β in G2
gamma_g2: G2Affine, // γ in G2
delta_g2: G2Affine, // δ in G2
ic: Vec<G1Affine>, // IC₀, IC₁, ..., ICₙ
}
Step 2: Compute IC Sum
fn compute_ic_sum(vk: &VerificationKey, public_inputs: &[Fr]) -> G1Affine {
let mut acc = vk.ic[0];
for (i, input) in public_inputs.iter().enumerate() {
let scaled = vk.ic[i + 1].mul(*input);
acc = acc.add(&scaled);
}
acc
}
Step 3: Pairing Check
fn verify_groth16(
proof: &Proof,
public_inputs: &PublicInputs
) -> bool {
let ic_sum = compute_ic_sum(&VK, &public_inputs.values);
// Compute pairings
let p1 = pairing(&proof.a, &proof.b);
let p2 = pairing(&VK.alpha_g1, &VK.beta_g2);
let p3 = pairing(&proof.c, &VK.gamma_g2);
let p4 = pairing(&ic_sum, &VK.delta_g2);
// Check equation
p1 == p2 * p3 * p4
}
BN254 Curve Implementation
Field Arithmetic
// Base field Fq (modulus q)
pub struct Fq {
value: U256,
}
impl Fq {
pub fn add(&self, other: &Fq) -> Fq { ... }
pub fn sub(&self, other: &Fq) -> Fq { ... }
pub fn mul(&self, other: &Fq) -> Fq { ... }
pub fn inv(&self) -> Fq { ... }
}
// Extension fields
pub struct Fq2 { c0: Fq, c1: Fq }
pub struct Fq6 { c0: Fq2, c1: Fq2, c2: Fq2 }
pub struct Fq12 { c0: Fq6, c1: Fq6 }
Elliptic Curve Groups
// G1: Points over Fq
pub struct G1Affine {
x: Fq,
y: Fq,
}
// G2: Points over Fq2
pub struct G2Affine {
x: Fq2,
y: Fq2,
}
Optimal Ate Pairing
pub fn pairing(p: &G1Affine, q: &G2Affine) -> Fq12 {
// Miller loop
let f = miller_loop(p, q);
// Final exponentiation
final_exponentiation(f)
}
Performance Characteristics
| Operation | Time | Gas | Size |
|---|---|---|---|
| Proof Generation | ~1s | Off-chain | - |
| Proof Size | - | - | 256 bytes |
| Public Inputs | - | - | ~500 bytes |
| Verification | ~200ms | ~200k gas | - |
| Contract Size | - | - | 20KB WASM |
Deployment Flow
1. Circuit Development
├── Write circuit (Circom)
├── Compile to R1CS
└── Run trusted setup
│
▼
2. Contract Deployment
├── Export verification key
├── Generate Rust verifier
├── Compile to WASM
└── Deploy to Soroban
│
▼
3. Client Integration
├── Load circuit artifacts
├── Generate proofs (browser/Node.js)
└── Submit to Stellar
Security Considerations
Trusted Setup
- Circuit-specific ceremony required
- Safe if ≥1 participant is honest
- Powers of Tau can be reused
- Recommend 100+ participants for production
Contract Security
- Verification key is immutable
- Proof validation is deterministic
- No external calls or dependencies
- Extensive test coverage (49+ tests)
Circuit Constraints
- All constraints must be satisfied
- Under-constrained circuits are vulnerable
- Formal verification recommended
- Regular security audits
Live Deployment
Testnet Contract:
CBPBVJJW5NMV4UVEDKSR6UO4DRBNWRQEMYKRYZI3CW6YK3O7HAZA43OI
Next Steps
- Quick Start - Generate your first proof
- Stellar Integration - Deploy to Soroban
- Custom Circuits - Build your own circuits