Tenzro Testnet is live —request testnet TNZO
← Back to Tutorials

Zero-Knowledge Proofs on Tenzro

SecurityAdvanced40 min

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 arkworks and 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: IdentityProof

Step 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 bytes

Step 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 inputs

Step 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-proof

On-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 successfully

REST 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:    true

MPC 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

Next Steps