Zero-Knowledge Proofs on Tenzro
Generate and verify Groth16 zero-knowledge proofs on the Tenzro Network. Prove that an AI model produced a specific output, that a settlement was correctly calculated, or that an identity meets a KYC requirement — all without revealing the underlying data. This tutorial covers the full ZK lifecycle including the MPC trusted setup ceremony and ZK-in-TEE hybrid execution.
What You'll Build
- Groth16 proofs on BN254 for inference verification, settlement, and identity
- Local proof generation with
arkworksand MiMC hash - On-chain verification via the ZK_VERIFY EVM precompile
- REST API verification via
/api/verify/zk-proof - ZK-in-TEE hybrid execution combining math and hardware guarantees
- MPC trusted setup ceremony participation
Prerequisites
[dependencies]
tenzro-sdk = "0.1"
tenzro-zk = "0.1"
tenzro-tee = { version = "0.1", optional = true } # for ZK-in-TEE
tokio = { version = "1", features = ["full"] }Step 1: Available ZK Circuits
Tenzro ships three pre-built circuits. Each circuit is a Groth16 SNARK on the BN254 curve (~100-bit security post-exTNFS):
use tenzro_zk::{
ZkProver, ZkVerifier, CircuitType,
InferenceVerificationCircuit,
SettlementProofCircuit,
IdentityProofCircuit,
};
// List available circuit types
let circuits = vec![
CircuitType::InferenceVerification,
CircuitType::SettlementProof,
CircuitType::IdentityProof,
];
for circuit in &circuits {
println!("Circuit: {:?}", circuit);
}Circuit: InferenceVerification
Circuit: SettlementProof
Circuit: IdentityProofStep 2: Generate Proving Key
use tenzro_zk::{setup, CircuitType};
// Generate proving and verification keys for a circuit
// This runs the Groth16 trusted setup (or uses cached keys)
let (proving_key, verifying_key) = setup(CircuitType::InferenceVerification)?;
println!("Proving key: {} bytes", proving_key.serialized_size());
println!("Verifying key: {} bytes", verifying_key.serialized_size());Proving key: 45,632 bytes
Verifying key: 1,024 bytesStep 3: Create an Inference Verification Proof
Prove that a specific model produced a specific output. The proof reveals the model hash, input hash, and output hash as public inputs, but the actual weights, prompt, and response remain private:
use tenzro_zk::{ZkProver, InferenceVerificationCircuit};
// Create an inference verification proof
// This proves that a specific model produced a specific output
// without revealing the model weights or intermediate computations
let circuit = InferenceVerificationCircuit {
model_hash: "a1b2c3d4e5f6...".to_string(), // SHA-256 of model weights
input_hash: "f6e5d4c3b2a1...".to_string(), // SHA-256 of input prompt
output_hash: "1234567890ab...".to_string(), // SHA-256 of output tokens
provider_id: "prov-7f3a9c1e".to_string(),
timestamp: 1712934567,
};
let prover = ZkProver::new(&proving_key)?;
let proof = prover.prove(&circuit)?;
println!("Proof generated:");
println!(" Type: Groth16 on BN254");
println!(" Size: {} bytes", proof.proof_bytes.len());
println!(" Inputs: {} public inputs", proof.public_inputs.len());Proof generated:
Type: Groth16 on BN254
Size: 192 bytes
Inputs: 5 public inputsStep 4: Create a Settlement Proof
Prove that a payment was correctly calculated without revealing the exact amounts:
use tenzro_zk::SettlementProofCircuit;
// Create a settlement proof
// Proves a payment was correctly calculated without revealing amounts
let settlement_circuit = SettlementProofCircuit {
provider_address: "0x7f3a...9c1e".to_string(),
customer_address: "0x4b2d...8f3a".to_string(),
amount_hash: "abcdef123456...".to_string(), // hash of amount
fee_hash: "654321fedcba...".to_string(), // hash of fee
settlement_id: "settle-a1b2c3".to_string(),
timestamp: 1712934567,
};
let settlement_proof = prover.prove(&settlement_circuit)?;
println!("Settlement proof: {} bytes", settlement_proof.proof_bytes.len());Step 5: Create an Identity Proof
Prove you meet a KYC tier requirement without revealing personal data:
use tenzro_zk::IdentityProofCircuit;
// Create an identity proof
// Proves you meet a KYC tier requirement without revealing personal data
let identity_circuit = IdentityProofCircuit {
did_hash: "did-hash-7890...".to_string(),
kyc_tier: 2, // Enhanced
credential_hash: "cred-hash-abcd...".to_string(),
issuer_hash: "issuer-hash-ef01...".to_string(),
timestamp: 1712934567,
};
let identity_proof = prover.prove(&identity_circuit)?;
println!("Identity proof: {} bytes", identity_proof.proof_bytes.len());Step 6: Verify Proofs
Three verification methods: local, on-chain, and REST API.
use tenzro_zk::ZkVerifier;
// Verify any proof locally
let verifier = ZkVerifier::new(&verifying_key)?;
let is_valid = verifier.verify(&proof)?;
println!("Proof valid: {}", is_valid);
// Verify via the Web API
// POST /api/verify/zk-proofOn-Chain Verification
The ZK_VERIFY EVM precompile verifies Groth16 proofs directly in the VM. This is used by smart contracts that need to gate actions on proof validity:
use tenzro_sdk::{TenzroClient, config::SdkConfig};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = TenzroClient::connect(SdkConfig::testnet()).await?;
// Verify a proof on-chain via the ZK_VERIFY precompile
// The EVM precompile at the ZK_VERIFY address accepts:
// - proof bytes (Groth16 A, B, C points)
// - public inputs (field elements)
// - verification key hash
let result = client.zk().verify_proof(
&proof.proof_bytes,
"groth16", // proof system
proof.public_inputs.clone(),
).await?;
println!("On-chain verification: {}", result.valid);
println!("Message: {}", result.message);
Ok(())
}On-chain verification: true
Message: Proof verified successfullyREST API Verification
# Verify a ZK proof via the REST API
curl -X POST https://api.tenzro.network/verify/zk-proof \
-H "Content-Type: application/json" \
-d '{
"proof": "<base64-encoded-proof>",
"public_inputs": ["0x1234...", "0x5678..."],
"proof_system": "groth16",
"circuit_type": "inference_verification"
}'
# Response:
# {
# "valid": true,
# "proof_system": "groth16",
# "circuit_type": "inference_verification",
# "verification_time_ms": 12
# }Step 7: ZK-in-TEE Hybrid Execution
Combine ZK proofs with TEE attestation for the strongest guarantee: the computation is mathematically correct (ZK) AND it ran on genuine hardware (TEE). This is the gold standard for verifiable AI inference:
use tenzro_zk::{ZkProver, InferenceVerificationCircuit};
use tenzro_tee::{detect_tee, TeeType};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let tee = detect_tee().await?;
// Generate a ZK proof INSIDE the TEE enclave
// This combines two guarantees:
// 1. ZK proof: the computation is correct (math guarantee)
// 2. TEE attestation: the proof was generated in genuine hardware
// (hardware guarantee)
// Step 1: Generate TEE attestation
let attestation = tee.generate_attestation(b"zk-proof-session").await?;
// Step 2: Generate ZK proof inside the TEE
let circuit = InferenceVerificationCircuit {
model_hash: "a1b2c3d4e5f6...".to_string(),
input_hash: "f6e5d4c3b2a1...".to_string(),
output_hash: "1234567890ab...".to_string(),
provider_id: "prov-7f3a9c1e".to_string(),
timestamp: 1712934567,
};
let (proving_key, verifying_key) = tenzro_zk::setup(
tenzro_zk::CircuitType::InferenceVerification,
)?;
let prover = ZkProver::new(&proving_key)?;
let proof = prover.prove(&circuit)?;
// Step 3: Bundle proof + attestation
println!("Hybrid ZK-in-TEE proof:");
println!(" ZK proof: {} bytes (Groth16/BN254)", proof.proof_bytes.len());
println!(" TEE platform: {}", attestation.platform);
println!(" Combined guarantee: correct computation in genuine hardware");
// Verifier checks both: ZK proof validity AND TEE attestation
let zk_valid = ZkVerifier::new(&verifying_key)?.verify(&proof)?;
let tee_valid = tenzro_tee::AttestationVerifier::new().verify(&attestation).await?.valid;
println!(" ZK valid: {}", zk_valid);
println!(" TEE valid: {}", tee_valid);
println!(" Hybrid: {}", zk_valid && tee_valid);
Ok(())
}Hybrid ZK-in-TEE proof:
ZK proof: 192 bytes (Groth16/BN254)
TEE platform: intel-tdx
Combined guarantee: correct computation in genuine hardware
ZK valid: true
TEE valid: true
Hybrid: trueMPC Trusted Setup Ceremony
Groth16 requires a trusted setup. Tenzro uses a BGM17 MPC ceremony with Phase 1 (Powers of Tau universal accumulator) and Phase 2 (circuit-specific CRS). Anyone can contribute randomness:
# Participate in the ZK trusted setup ceremony (MPC)
# Phase 1: Powers of Tau (universal accumulator)
tenzro ceremony contribute --phase 1
# Phase 2: Circuit-specific CRS
tenzro ceremony contribute --phase 2 --circuit inference-verification
# Check ceremony status
tenzro ceremony status
# Output:
# Ceremony Status:
# Phase 1: complete (47 contributions)
# Phase 2:
# inference-verification: 23 contributions (active)
# settlement-proof: 18 contributions (active)
# identity-proof: 12 contributions (active)
# Random beacon: pending (finalizes after contribution deadline)Security property. The MPC ceremony is secure as long as at least one participant is honest and destroys their randomness. Phase 2 initialization is deterministically seeded from the Phase 1 accumulator hash, ensuring MPC-contributed randomness flows through to the Groth16 key generation.
What You Learned
- Groth16 proofs — generating SNARKs on BN254 for inference, settlement, and identity
- Circuit types — InferenceVerification, SettlementProof, and IdentityProof
- On-chain verification — using the ZK_VERIFY EVM precompile
- REST verification — the
/api/verify/zk-proofendpoint - ZK-in-TEE hybrid — combining math correctness with hardware attestation
- MPC ceremony — contributing to the trusted setup for Groth16 key generation
Next Steps
- See the TEE Confidential Computing tutorial for the full TEE walkthrough
- See the Network Plugin Agent tutorial for building agents that use ZK proofs
- Read the Custody Application tutorial for ZK-based identity proofs in key management