Skip to content
On this page

zkML Rollup Tutorial with Sindri

Introduction - Code Once, Verify Anywhere

This guide will show you how to build verifiable machine learning inference into your Rollkit rollup using zero-knowledge proofs generated on Sindri. The rollup will rely on a local Celestia devnet for consensus and ensuring data availability.

We will focus on using a ZK circuit that incorporates a compact deep neural network model to enable verifiable ML inference. In this guide, we will deploy the circuit on Sindri, obtain a smart contract verifier (automatically generated by Sindri) for that circuit, deploy it on Rollkit, create a proof of ML inference on Sindri, and verify the proof on-chain.

This approach not only secures the verification process of machine learning models, but also leverages the decentralized security and scalability of Celestia's architecture.


Setting Up the Polaris EVM using Rollkit

This walkthrough assumes you started the Polaris EVM using Rollkit and should be interpreted as a direct continuation of this Polaris EVM and Rollkit guide.

Installing the Sindri Python SDK

Because we're working with ML, we're going to build with Sindri's Python SDK because Python is widely used in ML development. The Sindri Python SDK Quickstart Guide contains installation instructions and a high-level walkthrough of the functionality of this package, but the following will suffice if you have pip installed:

pip install sindri

Deploying and Proving a Cool zkML Circuit to Sindri

For this tutorial, we'll be working with a pre-built zkML circuit built by Sindri. For a more in-depth description of this circuit and its corresponding ML model's behavior and design, please see here.


Clone the Sindri Resources GitHub repo.

cd $HOME
git clone

Navigate to the food_ml circuit tutorial directory.

cd sindri-resources/circuit_tutorials/circom/food_ml/

Here, you will find a handful of files. The circuit/ directory contains the circuit code that we will upload to Sindri. The circuit/sindri.json file is the Sindri manifest for your upload. Within it, you can modify the circuit's "name" value to whatever you like.


Open the script and append the following lines to the very bottom.

# Obtain smart contract verifier for our circuit and save it to a file
smart_contract_code: str = sindri.get_circuit_smart_contract_verifier(circuit_id)
verifier_code_file: str = "Verifier.sol"
with open(verifier_code_file, "w") as f:
print(f"Smart contract verifier code written to {verifier_code_file}\n")

# Obtain our proof's proof+public formatted as calldata for our circuit's
# smart contract verifier
proof = sindri.get_proof(proof_id, include_smart_contract_calldata=True)
calldata_file: str = "calldata.txt"
calldata: str  = proof["smart_contract_calldata"]

# Fix formatting so it works with Rollkit
import json
a = json.loads("["+calldata_str+"]")
calldata_objects = []
for i in a:
    calldata_objects.append(json.dumps(i).replace("\"", "").replace(" ",""))
rollkit_calldata_str = " ".join(calldata_objects)

# Save calldata to file
with open(calldata_file, "w") as f:
print(f"Proof calldata written to {calldata_file}\n")

These additions will allow us to fetch and save the following to files:

  • the circuit's smart contract verifier code that is generated by Sindri when we deployed our circuit
  • the proof's proof+public formatted as calldata to run with the smart contract verifier on Rollkit

Deploy and Prove your zkML Circuit on Sindri

Export your Sindri API Key to an environment variable (or prepend it to the run command in the next step).


Run the script. This will upload the circuit/ directory to Sindri, where Sindri will compile and host your circuit. Then, it will run a single proof for the circuit. Finally, it will save the circuit's smart contract verifier code that is generated by Sindri to the Verifier.sol file and it will save the proof's proof+public formatted as calldata for that smart contract to calldata.txt.


Congratulations! You just deployed a zkML circuit to Sindri and proved it on Sindri's production-quality infrastructure with built-in, custom GPU-accelerated proving techniques. Furthermore, you now have smart contract verifier code (and some calldata) for verifying proofs of that circuit on-chain.

Deploy Smart Contract Verifier to Rollkit

Next, we will deploy this smart contract to Rollkit and verify our zkML proof on-chain. This section assumes you have an operational Rollkit instance of Polaris EVM running and your gm-portal/ directory is located in the $HOME directory on your machine.


Copy your new Verifier.sol smart contract to the ~/gm-portal/contracts/src/. directory in your Polaris EVM.

cd $HOME
cp sindri-resources/circuit_tutorials/circom/food_ml/Verifier.sol gm-portal/contracts/src/Verifier.sol

Next, let's make a copy of the example ~/gm-portal/contracts/script/GmPortal.s.sol and modify it to reference your new Verifier.sol contract.

cd gm-portal/contracts/script/
cp GmPortal.s.sol Verifier.s.sol

Open up the new Verifier.s.sol and modify it to interact with your new Verifier.sol instead of the example GmPortal.sol.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import "forge-std/Script.sol";

import {Verifier} from "src/Verifier.sol";

contract VerifierScript is Script {
    function setUp() public {}

    function run() public {
        new Verifier();

Then, deploy the contract to your Rollkit environment. Your PRIVATE_KEY and RPC_URL are for your Rollkit Polaris EVM.

export PRIVATE_KEY=0xfffdbb37105441e14b0ee6330d855d8504ff39e705c3afa8f859ac9865f99306
export RPC_URL=http://localhost:8545
cd ..
forge script script/Verifier.s.sol:VerifierScript --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast

A successful deployment's output will look similar to the following.

forge script script/Verifier.s.sol:VerifierScript --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast
[⠒] Compiling...
[⠆] Compiling 20 files with 0.8.24
[⠰] Solc 0.8.24 finished in 101.99ms
Compiler run successful!
Script ran successfully.

== Logs ==
  i am a smart contract on Polaris EVM x Rollkit. gm!


Waiting for receipts.
⠉ [00:00:00] [######################] 1/1 receipts (0.0s)
##### 80085
✅  [Success]Hash: 0xa06a4585af436e2271fc9f697488ce49771c6480e72caac76739e286564c0fc3
Contract Address: 0x5C59C83c099F72FcE832208f96a23a1E43737a14
Block: 5699
Paid: 0.002924172006823068 ETH (974724 gas * 3.000000007 gwei)


From your contract deployment output, export your contract address: Contract Address: 0x5C59C83c099F72FcE832208f96a23a1E43737a14. Note that the address will be different.

export CONTRACT_ADDRESS=0x5C59C83c099F72FcE832208f96a23a1E43737a14

Interact with the Contract - Verify your zkML Proof On-Chain

Now, we will send your zkML circuit's proof to the contract and verify it on-chain. Keep in mind that your verifier contract can be used across any EVM-compatible environment. Thus, we can compare the cost of execution across multiple environments.

First, grab the contents of your proof calldata and save it in a variable.

CALLDATA=$(cat $HOME/sindri-resources/circuit_tutorials/circom/food_ml/calldata.txt)

Then, interact with the smart contract using the calldata.

"verifyProof(uint256[2],uint256[2][2],uint256[2],uint256[1])" \
--private-key $PRIVATE_KEY \
--rpc-url $RPC_URL

The output will look like the following.

cast send $CONTRACT_ADDRESS "verifyProof(uint256[2],uint256[2][2],uint256[2],uint256[1])" $CALLDATA --private-key $PRIVATE_KEY --rpc-url $RPC_URL

blockHash               0xbbd872d0c37fe889c2456daf80505c20f262b001842d919d06e48c163319af3d
blockNumber             11544
cumulativeGasUsed       231649
effectiveGasPrice       3000000007
from                    0x20f33CE90A13a4b5E7697E3544c3083B8F8A51D4
gasUsed                 231649
logs                    []
logsBloom               0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
status                  1
transactionHash         0x58096aabd3cb58bdef28501bda01b6cf4a37ed0ba482f81462bc1043bb91f996
transactionIndex        0
type                    2
to                      0x5C59C83c099F72FcE832208f96a23a1E43737a14


Note: To see the decoded output of the contract call (to check if the proof was verified), you will need to view the call in a block explorer.


Congratulations, you've just verified a zkML circuit on Rollkit.

For further reading, check out Sindri's blog post explaining how using Sindri + Rollkit x Celestia means verifiable ML doesn’t have to be prohibitively expensive for operators or end users.

Released under the APACHE-2.0 License