# What is NovaNet

### NovaNet: A Peer-to-Peer Decentralized Prover Network

NovaNet is a peer-to-peer decentralized prover network that builds upon the principles of non-uniform incremental verifiable computation (NIVC) introduced by the [SuperNova](https://eprint.iacr.org/2022/1758.pdf) proving scheme. This foundation enables a highly portable and customizable zero-knowledge (zk) virtual machine and opcode-based prover framework.

### Why NIVC?

NIVC offers exceptional memory efficiency, allowing NovaNet provers to operate across a wide range of devices. This inclusivity means that virtually anyone can participate in the network as a prover, whether on high-end servers, consumer laptops, or lightweight embedded systems.

By supporting diverse device types, NovaNet democratizes access to zero-knowledge proving. Participants can potentially earn tokens while playing zkGames, interacting with browser-based apps, or contributing as specialized provers. This ethos of "ZK's *e pluribus unum*" encourages both casual users and expert contributors to join the ecosystem. Researchers, in particular, can design novel proving schemes and be rewarded for their adoption in real-world applications.

### First Implementation: WASM, with Extensibility in Mind

NovaNet's initial implementation targets WebAssembly (WASM) executables, chosen for its unparalleled portability. WASM runs seamlessly across browsers, servers, blockchains, and many other environments, making it an ideal starting point.

However, NovaNet's vision extends far beyond WASM. The network's modular design could support other virtual machines (e.g., RISC-V) or even bespoke architectures in the future. Additionally, NovaNet integrates specialized opcodes for advanced use cases such as zero-knowledge machine learning (zkML), integration with other zk virtual machines (e.g., JOLT), and any domain where tailored circuits can outperform general-purpose zkVMs.

{% hint style="info" %}
\*Open-sourced non-audited version of zkEngine is here (expect breaking changes), recent changes implemented[ Nebula](https://eprint.iacr.org/2024/1605.pdf) NIVC : <https://github.com/ICME-Lab/zkEngine_dev/tree/main>
{% endhint %}

### Decentralized Proving with NovaNet: Privacy, Flexibility, and Scalability

Many existing and emerging prover services rely on centralized prover stacks. The idea behind this approach is that centralized provers enable "proof racing," where competition drives optimal speed. However, this model often comes with significant trade-offs, particularly for privacy-preserving and locally executed proofs.

When you send data to a non-local prover, you inherently lose privacy—it’s as simple as that. NovaNet takes a different path. By leveraging proof-carrying data (PCD), a generalization of incremental verifiable computation (IVC), NovaNet enables parallelization and introduces game-theoretic optimizations. These optimizations incentivize participants while achieving faster proving speeds in various settings. Moreover with NIVC we achieve an efficient aggregation mechanism for client-side privacy preserving ZKP.

### Flexibility for Developers

NovaNet empowers developers to host verifiers wherever they prefer—on Ethereum, other blockchains, or even cloud servers. You have the freedom to choose the infrastructure that best fits your use case.

Rather than building a NovaNet-specific zk-L1, we opted for a cross-chain token layer. Core components of NovaNet are designed to work across multiple chains, with specific integrations to be announced. Orchestration smart contracts manage key functions such as tokenomics, escrow, and validators. These contracts, while largely abstracted away from application developers, are fully on-chain and transparent for anyone to inspect.

### Who Should Use NovaNet?

NovaNet's testnet is open as of late 2024. We are actively collaborating with partners to ensure a robust balance of demand- and supply-side participants during the testnet phase. This will allow for comprehensive testing and optimization of the network. Reach out if interested!

NovaNet opens up diverse use cases, including:

* **zk-Games**: Gamers could act as provers, earning tokens or in-game rewards while enabling features like fog-of-war or cheat prevention.
* **Consumer apps:** Many web3 consumer apps would benefit from privacy. Some simple examples are, login services (Basenames), escrow, proof of reserves, proof of NFT ownership (without doxing account), etc.
* **DePin:** Memory efficient ZK is vital for verifiaiblity in large scale DePin networks. Most ZKP are created to scale blockchains. This makes them poor choices for constrainted devices. At NovaNet out north star is portability.&#x20;
* **WASM-Compatible Applications**: Teams running workloads in WASM but lacking the expertise or resources to develop a dedicated zkFramework or learn a DSL (e.g., Circom) are ideal candidates for the testnet.

If you're a Rust developer, NovaNet supports your workflows out-of-the-box. Simply compile your code into WASM, and the network will generate cryptographic proofs that attest to correct execution. These proofs can be verified anywhere. Developers using any of the 30+ languages supporting WASM can also use NovaNet with minimal to no modifications.

### Open Source for Transparency and Collaboration

Most of NovaNet’s code will be open-sourced shortly after the testnet launch. Open-sourcing serves multiple purposes: enabling peer review, facilitating auditing, and inviting community contributions to optimize the framework for diverse use cases.

Let’s work together to build the most open and user-friendly zero-knowledge proof framework. Join us in shaping the future of decentralized proving with NovaNet!


# How It Works

Modern, Adaptable, and Decentralized

## A New Paradigm for zkVMs

Traditionally, zkVMs have relied on STARK-like systems (e.g., Risc0, zkEVM). While these systems are robust, newer paradigms, such as JOLT, explore more adaptable approaches, leveraging multivariate polynomial commitment schemes.

NovaNet embraces this spirit of innovation to achieve two critical goals:

1. **Permissionless Decentralization:** Supporting a wide variety of devices.
2. **Privacy-Preserving Local Execution:** Ensuring sensitive computations remain local.

To meet these requirements, NovaNet builds on ICME’s pioneering work in non-uniform incremental verifiable computation (NIVC). The journey started with the implementation of the [SuperNova](https://eprint.iacr.org/2022/1758.pdf) proving scheme, further refined with opcodes and improvements like optimal memory consistency checks (MCC) and extensions for parallelization under the same MCC paradigm. The science has led to ultra fast and portable schemes such as [Nebula](https://eprint.iacr.org/2024/1605.pdf), which we were the first team to implement.

### Why Nova and NIVC?

NovaNet leverages Nova and similar IVC-based folding schemes because of their unmatched memory efficiency. These schemes require minimal space to execute, making them ideal for a wide range of devices.

In NovaNet, the network dynamically determines chunk sizes based on each prover’s hardware capabilities and the complexity of the execution trace. This flexibility enables the network to divide proofs into smaller, parallelized problems. Specialized machines handle intensive computations, while smaller, geographically closer participants process other portions.

**Cooperation Over Competition**\
Unlike the “proof racing” model favored by centralized systems, NovaNet prioritizes cooperative proof generation. This approach delivers:

* Broader use case support at exceptional speeds.
* Stronger incentives for network participants to adapt and grow.

NovaNet is built on a simple observation: **winner-takes-all works for some use cases, but cooperation works better for many others.** There is no one-prover fits all - shapely values, and the alignment of incentives brings incredible prover speeds for a wide range of use-case.

## Why Peer-to-Peer?

#### Privacy and Local Execution

Some use cases demand local proof generation for full privacy preservation. Local execution ensures sensitive data remains on users’ devices. In contrast, centralized proving services—even when designed for privacy—can leak data. This limitation reduces the viability of many privacy-sensitive applications.

NovaNet's peer-to-peer design overcomes this challenge by enabling proofs to be generated locally or collaboratively within a local network. This eliminates the need for data to traverse multiple network hops, reducing both lag and privacy risks.

#### Proximity and Performance

Locality is key to reducing latency in distributed networks. Peer-to-peer architecture enables machines within the same local network to collaborate directly on solving problems. By contrast, centralized proving services—whether on L1 or L2—must move data across distant nodes, often ignoring the benefits of the "network edge."

#### True Permissionless Participation

For a network to be truly permissionless, anyone should be able to join and contribute. NovaNet achieves this by allowing participants to interact directly, fostering deeper decentralization. This architecture enables game-theoretic optimizations that incentivize cooperation, encourage network growth, and ultimately result in faster, cheaper, and more flexible proving.

***

### NovaNet: Fast, Cost-Effective, and Adaptable

NovaNet is a modular, peer-to-peer, decentralized prover network that developers can leverage with ease. Whether the goal is privacy-preserving computation, high-speed proof generation, or collaborative problem solving, NovaNet delivers a modern, adaptable, and scalable solution for the next generation of zero-knowledge applications.


# For Developers

### Hey Dev Team 👋

Have you ever wanted to use zero-knowledge proofs (ZKPs) for **verifiability, privacy, or scalability** but didn’t want to be tied down to a monolithic proving service or zkL1? Or maybe you’ve wanted to explore ZKPs without needing to learn complex math or domain-specific programming languages?

If so, you’re in the right place!

**NovaNet** makes it easy to leverage ZKPs by allowing you to:

* Take any valid **WASM file**.
* Execute it (even with **private inputs**).
* Generate a proof attesting to the correct execution of that code.

The generated proof can be verified **anywhere**—on Ethereum, another L1, or even a cloud server.

NovaNet operates as a **peer-to-peer network**, designed to minimize lock-in while delivering high performance and low memory overhead.

Here, you’ll find all the documentation you need to get started with NovaNet in no time. Let’s build the future of verifiable, private, and scalable computation together!

<br>


# Basics

The zkFramework and WASM

At its core, NovaNet is powered by the memory-efficient **non-uniform incremental verifiable computation (NIVC)** scheme, SuperNova/Nebula The foundational codebase is written in Rust, with many components open-sourced and developed in collaboration with other leading teams in the IVC space.

NovaNet is designed for flexibility. Developers can add custom opcodes tailored to specific use cases or utilize the pre-existing opcodes for more conventional applications. Its zkFramework is initially implemented for the WebAssembly (WASM) virtual machine, ensuring high portability across various environments. With NovaNet, zkApps can run seamlessly in browsers, on blockchains, or anywhere else WASM is supported.

### **Why is NovaNet Fast?**

**TL;DR:** Most of the heavy cryptographic work (the "SNARK machinery") happens only at the final step, thanks to folding schemes.

**Incremental and Parallelized Computation**

NovaNet leverages IVC, as described in the Nova paper, to perform zero-knowledge computations incrementally, minimizing costs at each iteration. Here's how it works:

1. Problems are divided into smaller chunks, with each chunk executed as its own SuperNova instance.
2. These chunks can be processed in parallel.
3. When recombining the chunks, NovaNet proves that they followed the correct sequence and produced the claimed outputs for each segment of the execution trace.

**What Sets NovaNet Apart?**

While other systems, such as [Risc0](https://www.youtube.com/playlist?list=PLcPzhUaCxlCirUkJY0ltpjdtzWcz5U_6y), use approaches like Merkle trees and sorting (referred to as "continuations"), NovaNet takes a different path. It employs **offline memory checking**, inspired by techniques described in [JOLT](https://people.cs.georgetown.edu/jthaler/Jolt-paper.pdf) and earlier by [Blum et al](https://ieeexplore.ieee.org/document/185352)., and adapts these methods specifically for folding schemes. This design choice eliminates the need for Merkle tree overhead, resulting in faster and more efficient proofs.

### Getting Started with WASM

To use NovaNet, you'll first need to compile your code into a WASM file. Here’s how:

**For Rust Developers:**

Run the following command on your existing Rust code (with minimal adjustments):

```
sudo cargo build --target=wasm32-unknown-unknown --release
```

#### For Go Developers:

```
GOOS=js GOARCH=wasm go build -o main.wasm
```

#### Other Languages:

Over 30 programming languages can compile into WASM, including C, C++, Python, and JavaScript. Additionally, WASM runs natively in most modern browsers, making it highly versatile.

### Next Steps

Now that you’ve generated your WASM file, you’re ready to integrate it with NovaNet. Let’s dive into the next section to learn how!


# Interacting With The Network

Get started with NovaNet's p2p proving network.

NovaNet is a **peer-to-peer network** designed for decentralized coordination and incentivization of zero-knowledge provers. Unlike zkEVM or zkL2, its primary focus isn’t on scaling Ethereum but on enabling **privacy**, **scalability**, and **verifiable compute** across diverse applications. With NovaNet, there’s no verifier lock-in—you can post proofs to your choice of an L1 blockchain or even a Web2 server, making it uniquely suited for privacy-focused use cases.

The network’s tokenomics, escrow, and payouts are managed via blockchain smart contracts (ERC20). Developers initiate proving jobs by depositing funds into an escrow contract. NovaNet estimates the computational effort based on the **WASM execution trace** and provides a cost estimate for review. If the developer commits, the network coordinates the proving process through its peer-to-peer layer. Once the proof is verified, funds are distributed among participants, either evenly or with premium options for specialized provers.

NovaNet balances **performance, flexibility, and cost-efficiency**, allowing developers to create scalable, privacy-preserving applications. By enabling decentralized proving and avoiding centralized lock-in, it fosters an open, permissionless ecosystem for zero-knowledge computation.

### **Top-Down and Bottom-Up**

You can use NovaNet to parallelize proofs across many nodes. In this case privacy is not possible as parts of the proofs will run across many nodes in the network. We call this the 'top-down' use-case. Examples are zkBLS or zkECDSA where verifiable computation is the main goal.\
\
You can also use NovaNet for aggregating privacy proofs, this is called 'bottom-up' proving.  Your nodes (gamers, local DeFi, DePin), will be joining as provers in the network. This could be 5,000 gamers who all want to run a proof locally and benefit from the aggregation and amortization done in NovaNet. In this case, the opcode amount should be contained to a reasonable amount for local proving.

### Fixing Bad Assumptions

Traditional systems often assume that adding more provers automatically lowers costs for users. However, the actual cost is primarily determined by how inexpensive it is for a single prover to post a proof to the L1, whether aggregated or not. Fast proving schemes like STARKs, Plonky, and JOLT often come with higher on-chain verification costs. NovaNet addresses this by wrapping various proving schemes within a NovaNet aggregation circuit, which posts a single, **"cheap" proof** on-chain, significantly reducing verification expenses.

In NovaNet, proving costs are not directly tied to the number of competing provers. Developers have the flexibility to control proof sharding and opt to pay a premium for specialized provers when needed. Want to involve 10 provers? You can split the workload, reducing effort for each prover while sharing the total price. Costs are determined by the total opcode count, current network conditions, and the expense of posting the final proof to your chosen L1.

For applications with **bottom-up use cases**, such as user-driven participation in zk-enabled apps, costs depend on the **total aggregated opcodes** and the **final proof posting cost**, not the total number of provers in the network. NovaNet’s incentive structure strikes a balance, rewarding specialized provers while keeping on-chain verification costs low—offering developers unparalleled **freedom and flexibility**.<br>

***

### Commands

#### Estimate

You first send a minimal balance to the smart contract to generate a job ID from parameters. Parameters include the WASM byte code, start and end opcodes, function to invoke and arguments that are passed into that function. Parameters are concatenated into a byte array and a random key is also generated and concatenated into the byte array and the byte array is hashed, producing a Keccak-256 checksum which is the job ID. All following commands, whether estimation or proving, require you to pass the same job parameters and key that were used to generate the job ID.\
\
Getting an estimate does not require you to commit to running the job, but rather returns basic details from the network such as estimated costs & amount of nodes required.&#x20;

```
novanet file_wasm estimate --key <key>
```

This command returns an estimation based on the execution trace size (in # of opcodes) and current network conditions.&#x20;

```
novanet file_wasm \
    estimate-bottom-up \
    --num-provers N \
    --num-aggregations N \
    --key <key>
```

This command also returns an estimation based on the execution trace size (in # of opcodes x amount of provers) and current network conditions. The `--num-provers` option is used later to determine when the job should be finished. The `--num-aggregations` option specifies the interval at which the ON should aggregate proofs and it must be a multiple of `--num-provers`. If, e.g., `--num-provers` is 6 and `--num-aggregations` is 3, aggregation triggers for every other proof and the client is sent three SNARKs.

{% hint style="info" %}
&#x20;In the following: NODE 1 is the orchestrator node. Any node can take up this role.&#x20;
{% endhint %}

<figure><img src="/files/V2wBbojbkANEnkNkY0H6" alt=""><figcaption><p>Estimate command initiates this network flow.</p></figcaption></figure>

***

#### Lets prove our WASM (Top-Down)

You must transfer the estimated amount from the last step into the smart contract after which you can proceed to the next step — proving your WASM.&#x20;

To run a proof with the default parameters.

```
novanet file_wasm prove --key <key>
```

The user commits to the job and estimated costs. This returns *\`success\`* when the network starts on the proof.\
\
Optionally  `prove --finalize`  can be run which will block until the job is finalized and the proof is received. The proof is serialized to JSON and stored on disk.&#x20;

By default the job will be sharded among a predetermined amount of provers for the amount of opcodes in the WASM byte code. Alternatively, you can choose the amount of provers to be used using the `--shards` option. If there aren't enough provers in the network to accept the requested shard count, ON rejects the job and you must try with a lower value.

```
novanet file_wasm prove --finalize --key <key> --shards N
```

<figure><img src="/files/IOLXGK2j0PLEEiSYOQOh" alt=""><figcaption><p>The network starts proving and aggregating.</p></figcaption></figure>

#### Lets prove our WASM (Bottom-Up)

Nodes will need to join the network via the NovaNet executable. This is available as an addition to your project via the RustSDK or directly (browser, etc) with our WASM compile.&#x20;

Run this command to start aggregating proofs from the local nodes. The `--orchestrator` option specifies the on-chain address of the ON to whom the proof should be sent when it's ready. `--key` must be provided so the ON can verify that this proof belongs to an active and funded job.&#x20;

```
novanet file_wasm prove-local --orchestrator <on-chain address> --key <key>
```

You can keep this running to see when N out of N proofs has been completed. With N being the `amount_of_provers` that was set in the estimation process.

***

#### Lets get our data out of the network (Top-Down)

To download proof data and complete verification.&#x20;

```
novanet file_wasm finalize --key <key>
```

with the `--endpoint` options a developer can select an endpoint to send the proof data for verification. This could be a custom smart contract or verifier endpoint. This step is not needed If `--finalize` was passed into the `prove` command (`prove --finalize`) as it will automatically download the proof.\
\
\*The verifier code will be open-sourced. It can be made to run anywhere Rust can run. We expect verifiers to come online as ETH smart contract and various other platforms in the near term. If you need a solution now for L1 interop reach out as we have solutions. <br>

<figure><img src="/files/n95PJeirtkzJ9IvJQVOe" alt=""><figcaption><p>The transaction completes here. You can now host and verify your proof anywhere.</p></figcaption></figure>

***

#### Lets get our data out of the network (Bottom-Up)

After the bottom-up has been funded on L1, bottom-up provers can start proving and sending proofs to the selected ON. The job creator can download the aggregated proofs as follows:

```
novanet download-proofs \
   --orchestrator <on-chain address> \
   --job-id <job id> \
   --token <token>
```

Where `--orchestrator` specifies the on-chain address of the ON that was selected for the bottom-up job, `--job-id` specifies the calculated ID of the job and `--token` specifies an authentication token which allows the connecting client to authenticate their proof downloading request.

The ON selected for the job accepts as many proofs and aggregates them as many times as was requested in the L1 job funding event. For example, if the client specified 6 proofs and 3 aggregations, the ON aggregates two proofs, sends the SNARK to client and waits for another two proofs to be received. This process continue until all proofs have been received and aggregated. The proofs produced by `novanet download-proofs` are stored on the client's disk.

***

**Prove parameters**

Following is a list of optional parameters a developer can choose from for the \`prove\` step.

*specialized -type*

Sometimes problems are best solved by specialized machines. These large and powerful machines are available for use in NovaNet. This option reduces decentralization and most of the speed from parallelization. -type determines the type of special prover. A full list of these provers will be provided after testnet (JOLT prover, zkML prover, etc); A smart contract manages this list.


# Rust SDK

Introduction of the Rust SDK for NovaNet.

This documentation is WIP until the full SDK is complete.\
It has the core functionality from the last section.

```
estimate
```

```
prove
```

```
finalize 
```

```
prove --finalize
```


# Custom opcodes

Specialization sometimes beats general purpose zkVM. Use custom opcodes and provers to get the best of both worlds.

The NovaNet zkFramework initially supports standard WASM opcodes. But it may be the case that different opcodes (circuits) are more efficient for specific use-case, or even different prover paradigms. NovaNet is written in Rust using R1CS via the [Bellpepper ](https://github.com/lurk-lab/bellpepper)crate. If you can code with Bellpepper you can create an custom opcode with relative ease. Moreover, one could use the [Circom-scotia](https://github.com/lurk-lab/circom-scotia) crate to convert Circom circuits into something that can be used with NovaNet.

### **Common pitfalls and a balancing act**

Commitment key parameters are determined by the largest circuit in the execution trace. Larger circuits need larger parameters which can result in longer proof generation times.&#x20;

The step circuit size proportionately increases the prover memory requirements which may limit the types of machines your custom opcode supports.&#x20;

### **Custom provers**

NovaNet will have open-source libraries that allow for proof composition for various prover schemes. For example these could be used to make the JOLT verifier into an R1CS circuit which can be proved recursively in NovaNet. This allows for NovaNet to capitalize on new or specialized proving schemes.

{% hint style="info" %}
\*Proof composition is required for almost all proving systems as this is the most direct way to get proofs into constrained environments and blockchains.&#x20;
{% endhint %}

### **Advanced**

If you have the Rust verifier code of another proving system, you could take that and its resulting proof data, compile it into WASM and run that in NovaNet. In this way you can have proof composition without custom circuit work.  Benefits of zkVM proof composition are from what the underlying Nova proof lib supports: KZG (ETH compatible proofs), aggregation, and better memory profiles where needed.

<figure><img src="/files/DinkX7FwR1yrH3WL1t2Z" alt=""><figcaption></figcaption></figure>

***

**Prizes**\
\
Do you think you have what it takes to make a custom but highly optimized circuit? Be our guest and win prizes for your work in NovaNet's decentralized prize pool. All code must be open-sourced and on Github with proper licensing to be eligible.&#x20;


# Prover selection

Scripting to power ON.

Novanet supports scriptable prover selection with Rhai. Each orchestrator keeps track of the performance of the provers that it knows about and each prover profile contains information such as how many shards they have proven and how many times they have disconnected while proving. When an orchestrator receives a request from client to prove a job, it fetches these prover profiles and calls a prover selection script which is fed these profiles and the number of provers the selector must choose from the set of available provers. The current prover selection strategy for NovaNet orchestrators is to ignore disconnected provers and sort the rest of provers in increasing order by their job queue size:

```rust
fn generate_list_a(provers, num_provers_needed, _num_opcodes) {
    provers.filter_disconnected();

    if provers.len() < num_provers_needed {
        return ();
    }

    provers.sort_by_job_queue_size_asc();
    return provers.select(num_provers_needed);
}
```

The selection can be fallible, meaning if there isn't enough suitable provers to complete the job, the script can return `()` to indicate that it failed. This will cause the job to be rejected and either some other orchestrator will pick up the job or the client has to try with a lower shard count.

Currently, the prover file exports the following fields:

* `prover` - on-chain address of the prover
* `num_shards_proven` - how many shards has the prover proven
* `num_disconnections` - how many times has the prover disconnected while proving
* `job_queue_size` - how many jobs does the prover have in queue
* `connected` - is the prover currently connected to the orchestrator


# JOLT Atlas (zkML)

JOLT Atlas is a high-performance zkML framework built upon [JOLT](https://eprint.iacr.org/2023/1217.pdf) for efficient neural network inference from ONNX models with cryptographic proofs.

Traditional circuit-based approaches are prohibitively expensive when representing non-linear functions like ReLU and SoftMax. Lookups eliminate the need for circuit representation entirely.

In JOLT Atlas, we eliminate the complexity that plagues other approaches: no quotient polynomials, no byte decomposition, no grand products, no permutation checks, and most importantly — no complicated circuits.

For an extended introduction to JOLT Atlas and the novelty of its approach, we recommend reading [this article](https://blog.icme.io/sumcheck-good-lookups-good-jolt-good-particularly-for-zero-knowledge-machine-learning/).


# ONNX

Introduction

ONNX is an open format built to represent machine learning models. ONNX defines a common set of operators - the building blocks of machine learning and deep learning models - and a common file format.

While most zkVMs run their proofs over a RISC-V trace, JOLT Atlas computes proofs over an ONNX trace. That is, a proof in JOLT Atlas convinces a verifier that the ONNX trace is the result of running a neural network model for some inputs parameters. Neural networks have a different computational model (they are a DAG) and a different set of opcodes.

We'll dive into the ONNX runtime next.


# ONNX Runtime

Informally, the ONNX runtime consists of a computation graph and a heap-like memory where tensors reside. The runtime evaluates the graph in a dataflow fashion by executing each node once its inputs are ready, updating memory with outputs. The memory and the graph together form the runtime machine.

**Definition 1.1 (ONNX Runtime State).** *The machine state of an ONNX runtime consists of a pair: $(G, M)$. $G$ is a directed acyclic computation graph whose nodes represent operator invocations (e.g., Add, MatMul, Relu) and whose edges represent tensor data dependencies. $M$ is a linear, read-write, byte-addressable memory array of size N bytes $(M: \[0..N) → u8)$, used to store all tensor data (inputs, intermediate results, and outputs). Each tensor occupies a contiguous region within $M$, and tensor metadata (such as shape and type) is tracked separately in a tensor table.*

**Definition 1.2 (ONNX node format).** Any ONNX node (operator) can be written in the following format: $\[op\\\_type, inputs, outputs, attributes]$, where:&#x20;

* $op\\\_type$: a string identifying the operator (e.g., "$Add$", "$Relu$")&#x20;
* $inputs$: a list of input tensor names (each mapped to memory)&#x20;
* $outputs$: a list of output tensor names&#x20;
* $attributes$: constant parameters specific to the operator (e.g., axis in $Softmax$)

**Definition 1.3 (ONNX Step Transition).** Given:&#x20;

* A machine state $(G, M)$, where $G$ is the current computation graph and $M$ is linear memory&#x20;
* &#x20;A topologically sorted node list $\[n₀, n₁, ..., n\_k]$&#x20;
* A tensor table $T$ mapping tensor names to $(offset, dtype, shape)$ in memory

We define the step transition for ONNX as:

1. Select node $nᵢ = \[op\_type, inputs, outputs, attributes, subgraph]$ from the graph (in topological order).
2. Read input tensors $x₀, ..., x\_k$ from memory $M$ using tensor metadata in $T$. Each tensor is loaded via its memory offset and interpreted by $dtype$.
3. Apply operator function $f\_{op}$ defined by op\_type, using $inputs$ and $attributes$:  $\[y₀, ..., y\_m] ← f\_{op}(x₀, ..., x\_k, attributes)$
4. Write outputs $y₀, ..., y\_m$ to memory $M$, assigning memory locations using $T$. Update memory values at corresponding offsets.
5. Advance to next node in G. Repeat until all nodes are executed.

###


# Quantization

In ML, quantization means converting real-valued parameters (weights, activations, etc.)—which are usually represented as 32-bit or 16-bit floating-point numbers—into lower-precision integer or fixed-point representations (like 8-bit integers). This is done to reduce memory and bandwidth costs and speed up inference, which also means speeding up proving inference. Plus, a floating-point model can’t be realistically proven inside a ZKP; encoding floats directly would explode circuit size and complexity

The downside of quantization is that it may degrade the accuracy of the model. However, the loss of accuracy may be considered insignificant, as described in <https://eprint.iacr.org/2024/1018.pdf>,

> "Most existing models are trained with 32-bit floating points (FP32), which provides greater precision than needed. Model pruning and quantization techniques have been developed to address these issues by transforming dense, high-precision parameters (e.g., FP32) into sparse, lower-bit representations (e.g., 8-bit integers, INT8)."

Currently, our models are quantized to 8-bits integers.&#x20;


# Tensor Instructions

Compared to JOLT instructions, JOLT Atlas operates on tensors. Most operations on tensors act entry wise, like `ADD`, while others such as `MAX` don't.

In particular, each instruction specifies:

* The **program counter (PC)** address of this instruction in the bytecode.
* The **operation code (opcode)** that defines the instruction’s function.
* Three **input tensor operands**, specified as the index of a node in the computation graph. These tensor operands are analogous to registers in RISC-V, as both indicate the source location of an operand.
  * The third input tensor operand, used by special opcodes such as `Select`.
* The **destination tensor index**, i.e. the node index in the computation graph where the result will be stored. It is analogous to **`rd`** in RISC-V, indicating the write destination of the operation result.
* An **immediate value**, if applicable to the instruction.
* The number of **virtual instructions remaining** in a *virtual sequence* (see Section 6.2 of the *Jolt* paper).
* The **dimensions** of the output tensor.

> **Note:** Currently limited to rank-2 tensors; scaling for higher ranks is a planned improvement.

* The **number of active elements** in the output tensor.

### List of instructions

| **Op**            | **Expression / Description**                        |
| ----------------- | --------------------------------------------------- |
| **Input**         | $X(input)$                                          |
| **MatMul**        | A \* B                                              |
| **Relu**          | $max(0, x)$                                         |
| **Sigmoid**       | $1 / (1 + e^{(-x)})$                                |
| **Add**           | $A + B$                                             |
| **EinSum**        | $Cᵢⱼ = ∑ₖ Aᵢₖ Bₖⱼ$                                  |
| **Const**         | $c (const)$                                         |
| **RmAxis**        | $squeeze(X)$                                        |
| **Reshape**       | $reshape(X, new\\\_shape)$                          |
| **Conv**          | $(X \* K)(i, j) = ∑\_{ₘ,ₙ} X(i + m, j + n) K(m, n)$ |
| **MaxPool**       | $Y(i, j) = max\_{ₘ,ₙ ∈ window} X(i + m, j + n)$     |
| **Gather**        | $Y(i) = X(g(i))$                                    |
| **Softmax**       | $yᵢ = e^{(xᵢ)} / ∑ⱼ e^{(xⱼ)}$                       |
| **Reduce**        | $y = ∑ᵢ x$ or $y = (1/n) ∑ᵢ xᵢ$                     |
| **AddAxis**       | $Y = expand\\\_dims(X)$                             |
| **Cast**          | $Y = cast(X, type)$                                 |
| **TypedBinOp**    | $C = A ∘ B$                                         |
| **ElementWiseOp** | $Y = f(X)$                                          |


# Virtual instructions

Quoting [the Jolt docs](https://jolt.a16zcrypto.com/how/architecture/emulation.html#virtual-instructions-and-sequences):

> Some instructions, like division, don't neatly adhere to the lookup table structure required by [prefix-suffix Shout](https://jolt.a16zcrypto.com/how/architecture/instruction_execution.html). To handle these cases, the problematic instruction is expanded into a virtual sequence, a series of instructions (some potentially virtual).

Most opcodes in ONNX are non-linear, which makes it difficult to provide the structure required by Shout. Thus, we convert an instruction into multiple virtual instructions that behave as one.


# Softmax

The softmax operator, defined by

$$ \sigma: \mathbb{R}^K \rightarrow (0,1)^K, $$

where $K > 1$, takes a tuple

$$ \mathbf{z} = (z\_1, \ldots, z\_K) \in \mathbb{R}^K $$

and computes each component of vector $\sigma(\mathbf{z}) \in (0,1)^K$ with

$$ \sigma(\mathbf{z})*i = \frac{e^{z\_i}}{\sum*{j=1}^{K} e^{z\_j}}. $$

We refer to $N = \sum e^{z\_j}$ as the *normalising* factor. After applying softmax, each component will be in the interval (0,1), and the components will add up to 1, so that they can be interpreted as probabilities.

To avoid numerical instability, a variant called "safe softmax" replaces the original softmax. It subtracts the maximum value of the input vector from each element before applying the exponential. This simple trick doesn’t change the output, but it significantly improves numerical stability.

$$ \sigma(\mathbf{z})*i = \frac{e^{z\_i - z*{max}}}{\sum\_{j=1}^{K} e^{z\_j - z\_{max}}}. $$

The base used in softmax is irrelevant. $e$ is used for convenience, but other bases such as 2 and 3 can also be used, since softmax is translation-invariant (adding the same constant to every logit does nothing). That is, for any base $b \geq 2$, $\sigma\_b(\mathbf{z})*i = \frac{e^{(\ln b)\\,z\_i}}{\sum*{j} e^{(\ln b)\\,z\_j}} = \sigma\left( (\ln b)\\,\mathbf{z} \right)*i$\*. To maximise the domain, we pick $b = 2$. This is what we want to prove: $\sigma(\mathbf{z})i = \frac{2^{z\_i - z\_{max}}}{\sum (2^{z\_j - z\_*{max}})}.$\*

The normalising factor $N = \sum e^{z\_j}$ needs to be known before any individual $\sigma(\mathbf{z})\_i$ can be computed. This means that at least a two-step instruction is required.

### Implementation

The sequence performs 14 virtual steps:&#x20;

| Step | Operation       | Description                           |
| ---- | --------------- | ------------------------------------- |
| 1    | VirtualConst(0) | Initialize zero tensor                |
| 2    | Gte             | Compute `ge0 = (z >= 0)`              |
| 3    | Sub             | Compute `neg_z = -z`                  |
| 4    | Select          | Compute `abs_z = select(ge0, z, -z)`  |
| 5    | VirtualPow2     | Compute \`c = 2^{                     |
| 6    | VirtualConst(Q) | Constant quantization scalar          |
| 7    | Div             | Compute `d_q_over_c = Q / c`          |
| 8    | Mul             | Compute `d_q_times_c = Q * c`         |
| 9    | Select          | Select `d = (z >= 0 ? Q * c : Q / c)` |
| 10   | Sum             | `ReduceSum(d)` to get total           |
| 11   | Broadcast       | Broadcast the sum                     |
| 12   | Mul             | Compute `f = Q * d`                   |
| 13   | Div             | Normalize `g = f / e_sum`             |
| 14   | VirtualMove     | Write final result to output tensor   |


# Div

Division involves a sequence described in detail in section 6.3 of [the Jolt paper,](https://eprint.iacr.org/2023/1217) utilizing virtual untrusted "advice" instructions. In the context of division, the advice instructions store the quotient and remainder in **virtual registers**, which are additional registers used exclusively within virtual sequences as scratch space. The rest of the division sequence verifies the correctness of the computed quotient and remainder, finally storing the quotient in the destination register specified by the original instruction.


# Max

Content TBD


# Sigmoid

The quantized sigmoid function is defined as follows:

$$\sigma\_Q(x) = \dfrac{Q}{1 + 2^{-x}}$$

where $Q$ is the quantization (scaling) factor. We use $2$ instead of $e$ to avoid floating point operations.

### Implementation

| Step | Operation       | Description                                                                          |
| ---- | --------------- | ------------------------------------------------------------------------------------ |
| 1    | VirtualConst(0) | Create constant zero tensor for comparisons                                          |
| 2    | Gte             | Compute `ge0 = (z >= 0)` to track sign of input                                      |
| 3    | Sub             | Compute `neg_z = -z` for absolute value calculation                                  |
| 4    | Select          | Compute `abs_z = select(ge0, z, -z)` (absolute value)                                |
| 5    | VirtualPow2     | Compute \`pow2 = 2^{                                                                 |
| 6    | VirtualConst(Q) | Load quantization constant `Q`                                                       |
| 7    | Mul             | Compute `Q² = Q * Q`                                                                 |
| 8    | Div             | Compute \`div\_Q\_pow = Q / 2^{                                                      |
| 9    | Mul             | Compute \`mul\_Q\_pow = Q \* 2^{                                                     |
| 10   | Select          | Compute `a = select(ge0, div_Q_pow, mul_Q_pow)` (choose branch based on sign of `z`) |
| 11   | Add             | Compute `b = Q + a`                                                                  |
| 12   | Div             | Compute `c = Q² / b` → final quantized sigmoid output `σ_Q(z)`                       |
| 13   | VirtualMove     | Move final tensor `c` to output destination                                          |


# ArgMax

Content TBD


# Lookups

Lookup instructions require their logic to have some structure to apply the prefix-suffix sumcheck, as described in Appendix A of [Proving CPU Executions in Small Space](https://eprint.iacr.org/2025/611). That is:

* The lookup table must have an MLE that is efficiently evaluable by the verifier. The `JoltLookupTable` trait encapsulates this MLE.
* The lookup index can be split into a prefix and suffix, such that MLEs can be evaluated independently on the two parts and then recombined to obtain the desired lookup entry.
* Every prefix/suffix MLE is efficiently evaluable (constant time) on Boolean inputs.

JOLT Atlas reuses some of the instruction from the original JOLT codebase and adds new ones applying the same paradigm.&#x20;


# Abs

Content TBD


# Add


# ReLU

Content TBD


# Precompiles

Content TBD


# ReduceSum

Content TBD


# MatMul

Content TBD


# Miscellaneous

Content TBD


# Gather

The gather operator is a special memory operator: it reads `ts1` and `ts2` sources, computes addresses based on `ts1` addresses and `ts2` values, and then performs additional reads using those computed addresses. We need to account for this behaviour.

In the zkVM, we first check that the gather-read values are correct, then verify that the addresses are computed correctly from the `ts1` addresses and `ts2` values, and finally confirm that the gather reads match TdWrites.


# Select

Content TBD


# Architecture

As mentioned, JOLT Atlas is built upon JOLT and the differences are the result of replacing the RISC-V computational model with ONNX. While [JOLT can be seen as a CPU](https://jolt.a16zcrypto.com/how/architecture/architecture.html#jolt-as-a-cpu), JOLT Atlas can be seen as a DAG.

<figure><img src="/files/OCH3x1iUQulUgjNvJSlq" alt=""><figcaption></figcaption></figure>

As a DAG, JOLT Atlas doesn't need to store and load from RAM. Instead of registers, JOLT Atlas uses a heap abstraction. The rest of the diagram above remains almost exactly as the original JOLT codebase. The reader can find [the bytecode](https://jolt.a16zcrypto.com/how/architecture/bytecode.html) and [instruction execution](https://jolt.a16zcrypto.com/how/architecture/instruction_execution.html) documentation in their docs.

The prover begins by cryptographically committing to the execution trace $z$ of the ONNX VM on the appropriate input. Then, the prover in JOLT Atlas proves that:

* The state transition function of the ONNX virtual machine encoded in R1CS is enforced. It proves that $z$ is a valid execution trace assuming $z$ satisfies memory-consistency and relevant entries of $z$ are indeed in the relevant lookup tables capturing evaluation of the ONNX instructions. JOLT Atlas applies [Spartan](https://eprint.iacr.org/2019/550) to establish that z satisfies the constraint system.
* Every ONNX instruction is executed correctly. Like JOLT, This is primarily achieved through the [Shout](https://jolt.a16zcrypto.com/how/twist-shout.html#shout) lookup argument.
* At each cycle of the ONNX virtual machine, the current instruction (as indicated by the program counter) is "fetched" from the bytecode and decoded. In Jolt Atlas, this is proven by treating the bytecode as a lookup table, and fetches as lookups. To prove the correctness of these lookups, we also use the [Shout](https://jolt.a16zcrypto.com/how/twist-shout.html) lookup argument.
* The correctness of heap operations using the [Twist](https://jolt.a16zcrypto.com/how/twist-shout.html) memory checking algorithm.
* The correctness of guest program outputs via [the output check](https://jolt.a16zcrypto.com/how/architecture/ram.html#output-check) sumcheck. Memory constraints are different compared to the original JOLT zkVM.

  <br>


# Getting started

1\. Clone the repository&#x20;

2\. Install Rust and Cargo&#x20;

3\. Run the examples:

```bash
cargo run --example article_classification
cargo run --example authorization
```


# Examples

The `examples/` directory contains practical demonstrations of zkML models:

#### Article Classification

A text classification model that categorizes articles into business, tech, sport, entertainment, and politics.

```bash
cargo run --release --example article_classification
```

This example:

* Tests model accuracy on sample texts
* Generates a SNARK proof for one classification
* Verifies the proof cryptographically

#### Transaction Authorization

A financial transaction authorization model that decides whether to approve or deny transactions based on features like budget, trust score, amount, etc.

```bash
cargo run --release --example authorization
```

This example:

* Tests the model on various transaction scenarios
* Shows authorization decisions with confidence scores
* Generates and verifies a SNARK proof for one transaction


# Benchmarks

We benchmarked a multi-classification model across different zkML projects:

| Project    | Latency | Notes                        |
| ---------- | ------- | ---------------------------- |
| zkml-jolt  | \~0.7s  |                              |
| mina-zkml  | \~2.0s  |                              |
| ezkl       | 4–5s    |                              |
| deep-prove | N/A     | doesn't support gather op    |
| zk-torch   | N/A     | doesn't support reduceSum op |

We also benchmarked an MLP model:

| Project    | Latency | Notes     |
| ---------- | ------- | --------- |
| zkml-jolt  | \~800ms |           |
| deep-prove | \~200ms | lacks MCC |

#### Running benchmarks

```bash
# enter zkml-jolt-core
cd zkml-jolt-core

# multi-class benchmark
cargo run -r -- profile --name multi-class --format chrome

# sentiment benchmark
cargo run -r -- profile --name sentiment --format chrome

# mlp benchmark
cargo run -r -- profile --name mlp --format chrome
```

When using `--format chrome`, the benchmark generates trace files (trace-.json) viewable in Chrome's tracing tool:

1. Open Chrome and go to `chrome://tracing`.
2. Load the generated trace file to visualize performance.

Alternatively, use `--format default` to view performance times directly in the terminal.

Both models (preprocessing, proving, and verifying) take \~600ms–800ms.


