Tutorial progress
- What is ZKML
- EZKL vs Giza
- Prerequisites
- Train a model
- Generate ZK proof (EZKL)
- On-chain verifier
- Giza alternative
- Next steps
What is ZKML
Zero-Knowledge Machine Learning (ZKML) lets you prove that a specific ML model produced a specific output for a specific input -- without revealing the model weights. The proof is a compact cryptographic object that anyone can verify, including a smart contract on Ethereum.
Use cases for ZKML:
- Verifiable AI predictions -- a DeFi protocol uses an ML price oracle. ZKML proves the oracle ran the correct model, not a manipulated one. The smart contract verifies the proof before acting on the prediction.
- Private model inference -- you want to sell inference from a proprietary model without revealing the weights. ZKML proves the output is genuine while keeping the model secret.
- On-chain AI governance -- a DAO votes on AI-generated proposals. ZKML proves the proposal was generated by the DAO's approved model, not a rogue actor.
- Tokenized model verification -- combined with erc721-ai, ZKML proves that inference came from the exact model represented by a specific NFT.
EZKL vs Giza: two approaches
The two leading ZKML frameworks take different approaches:
EZKL
- Proof system: Halo2 (KZG or IPA)
- Input: ONNX model file
- Strengths: Broad ONNX operator coverage, fast proving for small models, Solidity verifier generation
- Proving time: ~10s for a small classifier, minutes for medium models
- Verifier gas: ~300K-500K gas on Ethereum
- Best for: Smaller models (< 10M params), quick prototyping
Giza
- Proof system: STARK (Cairo/Stone)
- Input: ONNX model via Orion framework
- Strengths: No trusted setup, transparent proofs, Starknet-native verification
- Proving time: Slower but scales better to larger models
- Verifier gas: Lower on Starknet, higher on L1
- Best for: Production ZKML, larger models, Starknet ecosystem
This tutorial covers both. We start with EZKL (easier to get running) and show the Giza alternative in step 7.
Prerequisites
Before you start
- Python 3.10+ with
torchandonnx - EZKL CLI:
pip install ezkl(orcargo install ezklfor the Rust binary) - Foundry for deploying the Solidity verifier
- A Sepolia testnet wallet with test ETH
- Optional:
pip install giza-agentsfor the Giza path
Train and export a model
We will use a small neural network that predicts whether a DeFi lending position is at risk of liquidation. The model takes 4 features (collateral ratio, borrow APY, price volatility, utilization rate) and outputs a risk score between 0 and 1.
import torch import torch.nn as nn import numpy as np # Simple 2-layer network for liquidation risk prediction class LiquidationRiskModel(nn.Module): def __init__(self): super().__init__() self.net = nn.Sequential( nn.Linear(4, 16), nn.ReLU(), nn.Linear(16, 8), nn.ReLU(), nn.Linear(8, 1), nn.Sigmoid(), ) def forward(self, x): return self.net(x) # Generate synthetic training data np.random.seed(42) N = 2000 collateral_ratio = np.random.uniform(1.0, 3.0, N) borrow_apy = np.random.uniform(0.01, 0.30, N) volatility = np.random.uniform(0.05, 0.80, N) utilization = np.random.uniform(0.3, 0.99, N) # Label: high risk if collateral ratio is low AND volatility is high risk = ((collateral_ratio < 1.5) & (volatility > 0.4)).astype(np.float32) X = np.column_stack([collateral_ratio, borrow_apy, volatility, utilization]).astype(np.float32) y = risk.reshape(-1, 1) X_t, y_t = torch.tensor(X), torch.tensor(y) # Train model = LiquidationRiskModel() optimizer = torch.optim.Adam(model.parameters(), lr=0.01) loss_fn = nn.BCELoss() for epoch in range(100): pred = model(X_t) loss = loss_fn(pred, y_t) optimizer.zero_grad() loss.backward() optimizer.step() if epoch % 25 == 0: print(f"Epoch {epoch}, loss: {loss.item():.4f}") # Export to ONNX (required by EZKL) dummy_input = torch.randn(1, 4) torch.onnx.export( model, dummy_input, "liquidation_risk.onnx", input_names=["features"], output_names=["risk_score"], dynamic_axes={"features": {0: "batch"}}, ) print("Exported to liquidation_risk.onnx") # Save a test input for proving test_input = torch.tensor([[1.2, 0.15, 0.65, 0.85]]) # high risk case with torch.no_grad(): test_output = model(test_input) print(f"Test prediction: {test_output.item():.4f} (should be close to 1.0 = high risk)") # Save test input as JSON for EZKL import json json.dump({"input_data": [test_input.tolist()]}, open("input.json", "w"))
Generate a ZK proof with EZKL
EZKL converts the ONNX model into an arithmetic circuit, then generates a zero-knowledge proof that the model produced the claimed output. The pipeline has four steps: calibrate, compile, setup, prove.
# Step 1: Generate settings (calibrate the circuit) ezkl gen-settings \ --model liquidation_risk.onnx \ --settings settings.json # Step 2: Calibrate with sample data for optimal quantization ezkl calibrate-settings \ --model liquidation_risk.onnx \ --settings settings.json \ --data input.json # Step 3: Compile the model to a circuit ezkl compile-circuit \ --model liquidation_risk.onnx \ --compiled-circuit model.compiled \ --settings settings.json # Step 4: Run the trusted setup (generates proving + verification keys) ezkl setup \ --compiled-circuit model.compiled \ --pk-path pk.key \ --vk-path vk.key # Step 5: Generate the proof ezkl prove \ --compiled-circuit model.compiled \ --pk-path pk.key \ --witness input.json \ --proof-path proof.json # Step 6: Verify locally (sanity check before on-chain) ezkl verify \ --proof-path proof.json \ --vk-path vk.key \ --settings settings.json echo "Proof verified locally!"
What just happened:
- gen-settings analyzes the ONNX graph and determines the circuit configuration (number of columns, rows, lookup bits).
- calibrate-settings runs sample data through the model to find the optimal fixed-point quantization -- EZKL maps floating-point operations to finite field arithmetic.
- compile-circuit converts every layer (Linear, ReLU, Sigmoid) into Halo2 circuit constraints.
- setup generates the structured reference string (SRS), proving key, and verification key. The verification key is what goes on-chain.
- prove runs the model on your input inside the circuit and produces a ~1KB proof.
Proof size: For this small model, the proof is approximately 1-2 KB. Verification on Ethereum costs ~300K-500K gas (~$0.15-0.25 at typical L1 gas prices). On L2s like Base or Arbitrum, verification costs under $0.01.
Deploy the on-chain verifier
EZKL can generate a Solidity verifier contract from the verification key. This contract has a single function: verify(bytes proof, uint256[] instances) that returns true if the proof is valid.
# Generate the Solidity verifier contract ezkl create-evm-verifier \ --vk-path vk.key \ --settings settings.json \ --sol-code-path Verifier.sol # The generated Verifier.sol contains the verification key # baked into the contract -- no external dependencies. # Deploy with Foundry forge create Verifier.sol:Halo2Verifier \ --rpc-url https://sepolia.base.org \ --private-key $PRIVATE_KEY # => Deployer: 0x...your_address... # => Deployed to: 0x...verifier_address...
Now write a consumer contract that uses the verifier to gate on-chain actions based on ML predictions:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; interface IVerifier { function verify( bytes calldata proof, uint256[] calldata instances ) external view returns (bool); } contract LiquidationOracle { IVerifier public immutable verifier; // Stores the latest verified risk prediction struct RiskPrediction { uint256 riskScore; // fixed-point, scaled by 1e18 uint256 timestamp; address submitter; } mapping(address => RiskPrediction) public predictions; event RiskUpdated(address indexed position, uint256 riskScore, address submitter); constructor(address _verifier) { verifier = IVerifier(_verifier); } /// @notice Submit a verified ML risk prediction /// @param position The lending position address /// @param proof The EZKL proof bytes /// @param instances Public inputs/outputs of the circuit function submitPrediction( address position, bytes calldata proof, uint256[] calldata instances ) external { // Verify the ZK proof on-chain require(verifier.verify(proof, instances), "Invalid ZKML proof"); // instances[0..3] = input features (collateral ratio, etc.) // instances[4] = model output (risk score) uint256 riskScore = instances[4]; predictions[position] = RiskPrediction({ riskScore: riskScore, timestamp: block.timestamp, submitter: msg.sender }); emit RiskUpdated(position, riskScore, msg.sender); } /// @notice Check if a position is high risk (score > 0.7) function isHighRisk(address position) external view returns (bool) { RiskPrediction memory pred = predictions[position]; require(pred.timestamp > 0, "No prediction"); require(block.timestamp - pred.timestamp < 1 hours, "Prediction stale"); return pred.riskScore > 7e17; // 0.7 scaled by 1e18 } }
A DeFi lending protocol can now call isHighRisk(position) to check whether a ZKML-verified model predicts a position is at risk. The proof guarantees the prediction came from the correct model -- no one can submit a fake prediction.
Alternative: Giza + STARK proofs
If you prefer STARK proofs (no trusted setup, transparent), Giza takes a different path. Giza uses the Orion framework to represent ML models as Cairo programs, then generates STARK proofs using the Stone prover.
from giza_agents import GizaAgent, AgentConfig from giza_agents.model import GizaModel # Step 1: Transpile ONNX model to Cairo via Orion config = AgentConfig( model_path="liquidation_risk.onnx", framework="CAIRO", ) model = GizaModel(config) model.transpile() # converts ONNX -> Cairo/Orion representation # Step 2: Create a verifiable agent agent = GizaAgent( model=model, chain="starknet-sepolia", # STARK verification is native on Starknet ) # Step 3: Run inference with proof generation input_data = [1.2, 0.15, 0.65, 0.85] result = agent.predict(input_data, verify=True) print(f"Prediction: {result.output}") print(f"Proof ID: {result.proof_id}") print(f"Verifiable on-chain: {result.verification_url}") # Step 4: Verify on Starknet verification = agent.verify_on_chain(result.proof_id) print(f"On-chain verification tx: {verification.tx_hash}")
Key differences from the EZKL path:
- No trusted setup -- STARK proofs are transparent. No ceremony, no toxic waste.
- Starknet-native -- STARK verification is a native operation on Starknet. On Ethereum L1, STARK verification is more expensive than Halo2.
- Orion operators -- Giza's Orion framework re-implements ML operators (Linear, Conv2d, ReLU, etc.) in Cairo. Not all ONNX operators are supported yet.
- Agent framework -- Giza provides a higher-level agent SDK. You deploy a "verifiable agent" that generates proofs on every inference.
Next steps
You can now generate zero-knowledge proofs of ML inference and verify them on-chain. Here is where to go next:
- Combine with erc721-ai -- tokenize your model as an NFT and use ZKML to prove inference came from the tokenized model. Buyers can verify they are getting genuine inference.
- ZKML oracle network -- build a network of ZKML oracles where multiple provers submit verified predictions. Aggregate using median or weighted average for robust on-chain AI.
- Larger models -- for models with millions of parameters, investigate proof aggregation (splitting the model into chunks, proving each, then recursively verifying). Both EZKL and Giza support this.
- Privacy-preserving inference -- use ZKML to prove you ran a model on private data (e.g., a credit score model) without revealing the data itself.