Skip to main content

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

OperationTimeGasSize
Proof Generation~1sOff-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

View on Stellar Expert →

Next Steps