Technical comparison between Groth16 zero-knowledge proof verification on EVM and Stellar Soroban. Both verify proofs using the BN254 (alt_bn128) elliptic curve, but use different architectural approaches.
Key finding: Soroban implementation now includes complete pairing cryptography (Version 4), matching EVM's cryptographic capabilities without relying on precompiled contracts.
- Introduction
- Architectural Overview
- Cryptographic Primitives Comparison
- Implementation Details
- Performance Analysis
- Code Metrics
- Security Considerations
- Conclusions
Zero-knowledge proofs enable privacy-preserving verification in blockchain systems. Groth16 is one of the most efficient SNARK schemes, requiring only a single pairing check for verification.
Both implementations verify the same mathematical equation:
e(A, B) = e(α, β) · e(L, γ) · e(C, δ)
Where:
e(·, ·)is the optimal ate pairing functionA, B, Care proof elements (pi_a, pi_b, pi_c)α, β, γ, δare verification key parametersLis the linear combination of public inputs
This is typically checked as:
e(A, B) · e(-α, β) · e(-L, γ) · e(-C, δ) = 1
EVM provides native precompiled contracts for BN254 operations:
| Address | Function | Gas Cost |
|---|---|---|
| 0x06 | EC Addition (G1) | 150 gas |
| 0x07 | EC Scalar Multiplication (G1) | 6,000 gas |
| 0x08 | Pairing Check | 45,000 + 34,000 per pair |
Advantages:
- Native performance (implemented in Go/Rust at client level)
- Battle-tested (used since Byzantium fork, EIP-196/197)
- Minimal contract code (~250 lines of Solidity)
- Low gas costs
Disadvantages:
- Black-box operation (no visibility into pairing computation)
- Limited to BN254 curve
- Cannot customize or optimize for specific use cases
Soroban implements all cryptographic primitives from scratch in Rust (no_std):
Advantages:
- Full transparency and auditability
- Portable across any WASM environment
- Extensible to other curves/schemes
- Educational value and documentation
- Complete control over optimization
Disadvantages:
- Larger WASM binary size (~10KB vs EVM's native code)
- Potentially higher execution costs
- Requires careful security review
- Implementation: Native (part of precompiles)
- Field: Fq (prime field, 254 bits)
- Extension: Fq2, Fq6, Fq12 (implicit in precompile 0x08)
- Implementation: Custom Montgomery arithmetic
- Field: Explicit Fq struct with 4x u64 limbs
- Extension: Fully implemented tower extension
Fq → Fq2 (u² + 1 = 0) → Fq6 (v³ - ξ = 0, ξ = u + 9) → Fq12 (w² - v = 0) - Location:
src/field.rs(350 lines),src/fq12.rs(372 lines)
Key Operations Implemented:
- Montgomery multiplication (constant-time)
- Modular inversion (Fermat's little theorem)
- Frobenius endomorphism (for pairing)
- Karatsuba multiplication (for Fq6)
// Point addition (G1)
staticcall(sub(gas(), 2000), 6, input, 128, output, 64)
// Scalar multiplication (G1)
staticcall(sub(gas(), 2000), 7, input, 96, output, 64)// src/curve.rs (292 lines)
// G1 affine point addition
pub fn add(&self, other: &G1Affine) -> G1Affine {
// λ = (y2 - y1) / (x2 - x1)
let dy = other.y.sub(&self.y);
let dx = other.x.sub(&self.x);
let lambda = dy.mul(&dx.inverse().unwrap());
// x3 = λ² - x1 - x2
// y3 = λ(x1 - x3) - y1
...
}
// Scalar multiplication (double-and-add)
pub fn mul(&self, scalar: &[u64; 4]) -> G1Affine {
let mut result = G1Affine::infinity();
for each bit in scalar {
if bit == 1 { result = result.add(temp); }
temp = temp.double();
}
result
}Comparison:
- EVM: Black-box, highly optimized
- Soroban: Transparent, standard double-and-add algorithm
- Both support G1 (Fq) and G2 (Fq2) points
This is the most significant difference between the two implementations.
// Pairing check: e(P1,Q1)·e(P2,Q2)·...·e(Pn,Qn) = 1
// Input: 192 bytes per pair (G1 point + G2 point)
// Output: 32 bytes (0x01 if true, 0x00 if false)
function pairingCheck(
uint256[2] memory a1, uint256[2][2] memory b1,
uint256[2] memory a2, uint256[2][2] memory b2,
...
) internal view returns (bool) {
uint256[1] memory out;
bool success;
assembly {
success := staticcall(
sub(gas(), 2000),
8, // Precompile address
input, // 192n bytes
768, // For 4 pairs
out,
0x20
)
}
return out[0] != 0;
}What happens inside (hidden from Solidity):
- Parse input pairs
- Compute Miller loop for each pair
- Multiply results
- Perform final exponentiation
- Check if result equals 1
// src/pairing.rs (453 lines)
/// Optimal ate pairing for BN254
pub fn pairing(p: &G1Affine, q: &G2Affine) -> Fq12 {
if p.is_infinity() || q.is_infinity() {
return Fq12::one();
}
// Miller loop: accumulate line functions
let f = miller_loop(p, q);
// Final exponentiation: raise to (p^12 - 1) / r
final_exponentiation(&f)
}
/// Miller loop implementation
fn miller_loop(p: &G1Affine, q: &G2Affine) -> Fq12 {
let mut f = Fq12::one();
let mut r = *q;
// Iterate over ate loop count bits (6u + 2)
for bit in ate_loop_bits {
// Double step: f = f² · line_{R,R}(P)
let (line, doubled) = double_step(&r, p);
f = f.square().mul(&line);
r = doubled;
// Add step if bit = 1
if bit {
let (line, added) = add_step(&r, q, p);
f = f.mul(&line);
r = added;
}
}
f
}
/// Final exponentiation (optimized for BN curves)
fn final_exponentiation(f: &Fq12) -> Fq12 {
// Easy part: (p^6 - 1)(p^2 + 1)
let f1 = easy_part(f);
// Hard part: (p^4 - p^2 + 1) / r
hard_part(&f1)
}
/// Multi-pairing for batch verification
pub fn multi_pairing(pairs: &[(G1Affine, G2Affine)]) -> Fq12 {
let mut f = Fq12::one();
for (p, q) in pairs {
let fi = miller_loop(p, q);
f = f.mul(&fi);
}
final_exponentiation(&f)
}
/// Pairing check (Groth16 verification)
pub fn pairing_check(pairs: &[(G1Affine, G2Affine)]) -> bool {
let result = multi_pairing(pairs);
result.is_one()
}Key Components:
-
Miller Loop (70 lines)
- Implements double-and-add algorithm
- Evaluates line functions at each step
- Accumulates results in Fq12
-
Line Function Evaluation (30 lines)
- Computes tangent line for doubling
- Computes secant line for addition
- Embeds result in Fq12 (sparse representation)
-
Final Exponentiation (60 lines)
- Easy part: Frobenius maps and multiplications
- Hard part: Optimal addition chain for BN254
- Uses curve-specific optimizations
-
Batch Verification (20 lines)
- Combines multiple pairings efficiently
- Single final exponentiation for all pairs
File: circuits/evm/Verifier.sol
Total: 168 lines
- Auto-generated by snarkJS
- Template code: ~30 lines
- Verification key hardcoded: ~60 lines
- Verification logic: ~40 lines
- Assembly for precompiles: ~30 lines
Key Functions:
function verify(
uint[2] memory a,
uint[2][2] memory b,
uint[2] memory c,
uint[1] memory input
) public view returns (bool)Complete Module Structure:
soroban/src/
├── lib.rs (520 lines) - Main contract + integration
├── field.rs (350 lines) - Fq and Fq2 arithmetic
├── curve.rs (292 lines) - G1 and G2 operations
├── fq12.rs (372 lines) - Fq6 and Fq12 tower extension
└── pairing.rs (453 lines) - Complete pairing implementation
Total: 1,987 lines of Rust (excluding tests and comments)
Key Integration (lib.rs):
fn verify_pairing_equation(
env: &Env,
proof: &ProofData,
vk: &VerifyingKey,
vk_x: &G1Point,
) -> bool {
// 1. Validate points are on curve
// 2. Convert contract types to affine points
// 3. Negate G1 points for the equation
let neg_alpha = alpha.neg();
let neg_vk_x = vk_x_affine.neg();
let neg_pi_c = pi_c.neg();
// 4. Prepare pairs: e(A,B)·e(-α,β)·e(-L,γ)·e(-C,δ) = 1
let pairs = [
(pi_a, pi_b),
(neg_alpha, beta),
(neg_vk_x, gamma),
(neg_pi_c, delta),
];
// 5. Perform the pairing check
pairing_check(&pairs) // ← Now uses complete implementation!
}// Points are arrays of uint256
uint[2] memory g1_point; // [x, y] in Fq
uint[2][2] memory g2_point; // [[x0, x1], [y0, y1]] in Fq2
// Proof structure
struct Proof {
uint[2] a; // G1
uint[2][2] b; // G2
uint[2] c; // G1
}// Custom types with Soroban SDK
#[contracttype]
pub struct G1Point {
pub x: Bytes, // 32 bytes (big-endian)
pub y: Bytes, // 32 bytes
}
#[contracttype]
pub struct G2Point {
pub x: Vec<Bytes>, // [x0, x1] each 32 bytes
pub y: Vec<Bytes>, // [y0, y1] each 32 bytes
}
// Internal representation
pub struct G1Affine {
pub x: Fq, // 4x u64 limbs (Montgomery form)
pub y: Fq,
pub infinity: bool,
}
pub struct G2Affine {
pub x: Fq2,
pub y: Fq2,
pub infinity: bool,
}Groth16 Verification (4 pairing checks):
Base pairing check: 45,000 gas
Additional pairs: 34,000 × 3 = 102,000 gas
G1 operations: ~1,000 gas
Total: ~148,000 gas
At 50 gwei: ~$0.37 (at $2500/ETH)
Breakdown by Operation:
- EC addition (0x06): 150 gas
- EC mul (0x07): 6,000 gas
- Pairing (0x08): 45,000 + 34,000n gas
Contract Size:
WASM binary: ~10 KB (release build)
- Without optimization: ~50 KB
- With wasm-opt: ~10 KB
Expected CPU/Memory (estimates):
- Field operations: ~10-50 instructions each
- Pairing computation: ~10,000-50,000 instructions
- Memory: ~100 KB working set
Note: Exact Soroban costs depend on network calibration and are still being optimized.
| Operation | EVM | Soroban | Complexity |
|---|---|---|---|
| Field mul | Native | ~40 ops | O(n²) for n limbs |
| Field inv | Native | ~1000 ops | O(log p) via Fermat |
| EC add | Native | ~20 field ops | O(1) affine |
| EC mul | Native | ~256 doubles | O(log n) |
| Pairing | Native | ~5000 field ops | O(log r) Miller + exp |
Miller Loop Operations:
- Iterations: ~60 (based on ate parameter)
- Per iteration: 1 Fq12 square + 1 Fq12 mul + line function
- Line function: ~10 Fq2 operations
Final Exponentiation:
- Easy part: ~10 Fq12 operations
- Hard part: ~50 Fq12 operations
| Component | EVM | Soroban |
|---|---|---|
| Verification logic | 40 | 520 |
| Field arithmetic | 0 (precompile) | 350 |
| Curve operations | 0 (precompile) | 292 |
| Tower extension | 0 (precompile) | 372 |
| Pairing | 0 (precompile) | 453 |
| Total | 168 | 1,987 |
EVM Verifier:
- Main function: Complexity 5 (simple branches)
- Assembly blocks: Complexity 1 each
- Overall: Low complexity (template-generated)
Soroban Verifier:
- Field ops: Complexity 3-8 per function
- Curve ops: Complexity 5-12 per function
- Miller loop: Complexity 15
- Final exp: Complexity 20
- Overall: Moderate complexity (well-structured)
EVM:
- Integration tests with snarkJS
- Test vectors from Circom compiler
- Limited unit tests (precompiles tested at client level)
Soroban:
- 25+ unit tests across all modules
- Property-based tests (identity, associativity)
- Integration tests for full verification
- Test vectors from known proofs
Strengths:
- Precompiles audited by Ethereum Foundation
- Used in production for years (Constantinople fork)
- Implementation bugs would affect entire network (incentivized fixes)
- Formal verification of EIP-196/197 spec
Risks:
- No visibility into actual computation
- Cannot detect subtle implementation bugs
- Dependent on specific client implementations
Strengths:
- Fully auditable code
- Transparent implementation
- Can be formally verified
- Testable at all levels
- Rust memory safety
Risks:
- New implementation (less battle-tested)
- Potential for bugs in complex pairing code
- Requires independent security audit
- Timing attacks (constant-time operations needed)
For Soroban Implementation:
-
Formal Verification
- Verify Montgomery arithmetic correctness
- Prove pairing bilinearity property
- Check constant-time execution
-
Security Audit
- Independent third-party review
- Focus on pairing implementation
- Side-channel analysis
-
Testing
- Cross-verify with EVM results
- Test with malicious inputs
- Fuzz testing for edge cases
-
Constant-Time Operations
- Ensure no secret-dependent branches
- Use constant-time field operations
- Avoid timing side channels
The Soroban Groth16 verifier (Version 4) now includes:
- Complete BN254 field arithmetic (Fq, Fq2, Fq6, Fq12)
- Full elliptic curve operations (G1 and G2)
- Optimal ate pairing implementation (Miller loop + final exp)
- Batch pairing verification (efficient multi-pairing)
- Production-ready structure (modular, testable, documented)
| Aspect | EVM | Soroban |
|---|---|---|
| Cryptographic Completeness | Full (precompiles) | Full (native implementation) |
| Implementation | Native (black-box) | Rust WASM (transparent) |
| Code Size | 168 lines Solidity | 1,987 lines Rust |
| Binary Size | N/A (native) | ~10 KB WASM |
| Auditability | Limited | Complete |
| Performance | Optimal (native) | Good (WASM) |
| Extensibility | Limited to BN254 | Can support multiple curves |
| Battle-Testing | Production (5+ years) | New (requires audit) |
| Documentation | EIP-196/197 | Extensive inline docs |
Use EVM Verifier:
- Deploying on Ethereum mainnet or EVM L2s
- Minimizing gas costs is critical
- Using standard BN254 Groth16 proofs
- Want battle-tested implementation
Use Soroban Verifier:
- Building on Stellar network
- Need full transparency and auditability
- Want educational implementation
- Planning to extend to other curves
- Require customized optimizations
Optimization opportunities:
- Sparse multiplication in Miller loop
- Precomputed Frobenius constants
- Assembly-level field operations
- Batch inversion techniques
- Lazy reduction strategies
Testing & validation:
- Cross-chain test vectors (compare EVM ↔ Soroban)
- Formal verification of critical paths
- Performance benchmarking
- Fuzz testing campaign
- Independent security audit
Documentation:
- Developer integration guide
- Performance tuning guide
- Security best practices
- Migration guide (EVM → Soroban)
# Field modulus (254 bits)
p = 21888242871839275222246405745257275088696311157297823662689037894645226208583
# Curve equation: y² = x³ + 3
b = 3
# Order of G1 and G2
r = 21888242871839275222246405745257275088548364400416034343698204186575808495617
# BN parameter
u = 4965661367192848881
# Ate loop count
ate_loop = 6*u + 2 = 29793968203157093288
# Embedding degree
k = 12
# G1 generator (over Fq)
G1_x = 1
G1_y = 2
# G2 generator (over Fq2)
G2_x = [10857046999023057135944570762232829481370756359578518086990519993285655852781,
11559732032986387107991004021392285783925812861821192530917403151452391805634]
G2_y = [8495653923123431417604973247489272438418190587263600148770280649306958101930,
4082367875863433681332203403145435568316851327593401208105741076214120093531]
-
EIP-196: Precompiled contracts for addition and scalar multiplication on the elliptic curve alt_bn128 https://eips.ethereum.org/EIPS/eip-196
-
EIP-197: Precompiled contracts for optimal ate pairing check on the elliptic curve alt_bn128 https://eips.ethereum.org/EIPS/eip-197
-
Groth16 Paper: "On the Size of Pairing-based Non-interactive Arguments" by Jens Groth (2016) https://eprint.iacr.org/2016/260
-
BN254 Implementation: "High-Speed Software Implementation of the Optimal Ate Pairing over Barreto-Naehrig Curves" https://eprint.iacr.org/2010/354
-
snarkJS: JavaScript implementation of zkSNARK schemes https://github.com/iden3/snarkjs
-
Soroban Documentation: Stellar smart contracts platform https://developers.stellar.org/docs/smart-contracts
Version: 1.0 Date: 2025 Author: OpenZKTool Development Team Repository: https://github.com/xcapit/stellar-privacy-poc License: AGPL-3.0-or-later
Related Files:
circuits/evm/Verifier.sol- EVM Groth16 verifiersoroban/src/lib.rs- Soroban main contractsoroban/src/pairing.rs- Complete pairing implementationsoroban/src/fq12.rs- Tower extension fieldssoroban/src/curve.rs- Elliptic curve operationssoroban/src/field.rs- Base field arithmetic
This document demonstrates that the Soroban implementation has achieved feature parity with EVM's precompiled contracts, providing a fully transparent and auditable alternative for Groth16 verification on the Stellar network.