October 20, 2026
|

Written by Anne-Laure Schmitt, Bruce Collie and Samuel Balco

At Category Labs, we have spent the last few months working on a version of the Monad execution client that supports zero-knowledge (ZK) execution and proving, with the aim of joining the Ethereum Foundation's Guest Diversity program in the Ethproofs initiative. Given an input state and a block of transactions, an execution client that supports ZK execution can produce a proof that the resulting state root was computed correctly. Other participants in the network can verify this proof at a fraction of the cost of re-executing all of the transactions themselves.

At the time of writing, our work in progress represents a significant performance improvement over the current best guest program known to us: on a representative 30-block benchmark, our guest program reduced median block proving time by 17% on a single RTX 5090 compared with ziskethone, with a median block proving time of 13 seconds. This blog post gives an overview of our autoresearch-based optimization workflow and performance results.


Background

Zero-knowledge proofs are underpinned by the notion of arithmetic circuits, which can represent arbitrary loop-free computation. However, faithfully encoding a program as a circuit is laborious and requires deep technical expertise: it is easy to under-constrain the circuit and introduce soundness bugs. Consequently, the most common approach taken to produce zero-knowledge proofs of Ethereum block execution is to compile the Ethereum state transition function (also referred to as the guest program) to an intermediate Instruction Set Architecture (ISA) and prove the execution of that binary in a verified circuit which faithfully encodes the semantics of the ISA. The system that executes the binary and proves its execution against circuits encoding the ISA's semantics is commonly referred to as a zkVM. The industry has largely settled on RISC-V as a target ISA for zkVMs.

A zkVM is a bare metal environment with no persistent filesystem, so all the data the guest needs for block execution must be provided as an up-front binary blob (a "witness"). This means the witness must contain not only the block data, but also all the accounts and storage slots needed for the block execution.

Executing a guest program on an input witness using a zkVM produces a public output (e.g., a new blockhash), as well as a proof certificate that can be cheaply verified.

We initially decided to target two zkVMs, SP1 and ZisK, both of which support Reth as a guest program. The team behind ZisK also recently released ziskethone v1.13.0: a new, more efficient guest program based on evmone. Early experiments suggested that, by measured wall-clock time, ZisK was likely to provide the best possible performance for our guest program. The work described in this blog post therefore focused on optimizing for ZisK; improving support for other zkVMs is intended future work.


Why optimizing for a zkVM is different

Producing ZK proofs of program execution is expensive. The time to execute and prove a single RISC-V cycle is at least two orders of magnitude more than it takes to execute on a regular CPU. As the developers of a guest program, we can reduce the proving time in two ways:

  • Optimize the performance of our guest program's execution.
  • Improve the binary representation of the witness input data to reduce parsing and serialization costs.

Optimizing a program running inside a zkVM is similar to traditional optimization for "normal" CPU targets. However, the program characteristics that produce a fast program can be very different in a zkVM. For example, on traditional CPU architectures, an uncached memory access is approximately two orders of magnitude slower than a register access. In a zkVM proof, a register file access costs approximately the same as a memory access, and there are no cache effects to reason about. Conversely, unaligned accesses are far more heavily penalized in zkVM architectures than on traditional CPUs: one of our discovered optimizations (described below) relies on this.

We initially achieved large performance gains by optimizing the format of serialized witness data in our guest program. At one point in early development, 50% of proving costs came just from deserializing the witness. Since there is no common witness data encoding, guest programs all employ specific encoding techniques to minimize the amount of decoding and copying needed to extract block, account and contract code into their internal representation. This work was also coupled with necessary changes to parts of the Monad execution stack that were not compatible with zkVM execution. For example, MonadDB, our Merkle Patricia Trie (MPT) backing the state storage, is tightly coupled to raw NVMe commands and cannot run in a zkVM. We therefore implemented a replacement in-memory database that efficiently loads its state from the binary witness.

While an interesting topic on its own, describing the design of the zkVM version of an MPT and witness format is outside the scope of this post. We will instead focus on the performance optimizations of the guest program, achieved in large part by an automated and constrained agentic loop.


Benchmarks and setup

Our benchmarking setup was designed to be deterministic and reproducible. Some environments (like Ethproofs) report wall-clock time from block availability to proof production as their relevant performance metric, but for our purposes doing so would introduce additional sources of variation that obscure legitimate changes in performance.

Instead of wall-clock time, our benchmarking methodology uses the COST metric defined by ZisK, which estimates the proving workload associated with a block's RISC-V execution trace by accounting for the different proving costs of its operations. COST serves as a useful proxy for proving time and, critically, can be computed deterministically, allowing a reproducible comparison between the Monad guest program and other guests.

To establish both the COST and soundness of each optimization, we sampled a set of 200 random recent Ethereum blocks for our benchmark set. We generated input witnesses for each guest program and relied on the known state roots from block execution to ensure each optimization was sound.


Autoresearch

The Monad execution client (which our guest program is derived from) is highly specialized for a particular validator hardware configuration. At the time we began this research, no specific effort had been spent on optimizing it for the substantially different environment of a zkVM execution and proof.

We opted for a largely automatic research procedure to optimize the guest program: a coding agent backed by a frontier language model would propose, implement and evaluate optimizations to our code in a loop (shown below). Our first step was to define a set of success criteria for a proposed optimization:

  • The modified code must faithfully execute all blocks in our benchmark suite; the output blockhash produced by the modified code must be correct.
  • The median block's COST must be lower than before.
Autoresearch loop: inspect, hypothesize, implement, measure, then commit if the blockhash is correct and median COST is lower
Autoresearch loop

We additionally evaluated some optimization results in edge cases by hand. In some cases, a regression on the median block's cost was accepted in exchange for an improvement on the 10% of blocks with the highest ZisK cycle counts.

The autoresearch loop did not perform detailed checks for optimization soundness beyond the test suite of known valid block hashes; each proposed change was subsequently simplified and independently reviewed by core engineers as part of our regular code review lifecycle. Work is ongoing at Category Labs to formally verify the full execution client; extending this verification to proposed optimizations would substantially improve the automated workflow.


Optimizations

The optimizations we applied to our guest program (both those discovered by our autoresearch loop, and lower-hanging fruit implemented by hand) can be divided into three rough categories:

  • Specific to zkVM execution: we can exploit differences between traditional CPU architectures and the zkVM architecture to optimize our code:
    • There is no need to deallocate memory when executing inside a zkVM; all required memory has been allocated as an up-front slab.
    • Expensive operations (e.g., hashing or cryptography) are provided as precompiled circuits by the zkVM.
  • Specific to ZisK: it is also possible to optimize for particular characteristics of the ZisK VM (distinct from general zkVM properties):
    • Keccak operations are expensive enough in ZisK (even when backed by a precompile) that it can be worth selectively memoizing them.
    • ZisK penalizes unaligned memory accesses very heavily; optimizations that improve alignment are generally productive.
    • Some data structures can be worth replacing wholesale when executing in ZisK. For example, replacing a hash map with a linear scan through an array can improve performance because of differing performance tradeoffs on each architecture.
  • Specific to our client: we can simplify parts of the Monad execution stack that are unnecessary in a zkVM:
    • In a zkVM, there are no parallel threads of execution, so Monad's optimistic parallel execution can be removed.
    • There is no need for tracing or metric collection in zkVM execution, which allows us to eliminate dead code.

Our search identified more than 300 incremental improvements—far too many to describe fully in this blog post. This section examines three of the more impactful and interesting optimizations in detail.

ZK-friendly hashes (−1.58%)

The execution client relies on hash maps extensively; its x86 implementation uses Komihash, which is very efficient on a regular CPU, but is not ZK-friendly. Instead of using a single generic hash function, we analyzed the two main types of values used as keys in hash maps:

  • 20-byte addresses (indexing EVM accounts) and
  • 32-byte keys (indexing contract storage, pages and bytecode)

We implemented dedicated hash functions that produce 8-byte hash values, both seeded from the blockhash and adapted to each input size. They are built to enable enough dispersion (too many collisions mean more work to retrieve the values), while also minimizing the amount of computation. We tested a number of different candidate implementations to identify the best-performing choice for each type of key.

The initial optimization of the hash function took a simple approach based on XOR mixing and Fibonacci hashing. However, we later discovered this approach was vulnerable to attacks where an attacker crafts a block filled with addresses and storage slots with a large number of collisions. This can cause a significant slowdown in the prover in adversarial scenarios: an attacker can force the prover to do substantially more work than for a similar block with different address and slot values. Every cycle executed matters in a real-time proving scenario, and so avoiding these possible attacks is important.

To mitigate this kind of attack, we seeded hash functions with the blockhash, which reduces the attacker's ability to precompute collisions (changing the value of an address or slot would change the block's own hash, and therefore the hash seed).

The final versions of the two hash functions are presented below:

EVM addresses

a = key[0..7]   ^ blockhash[0..7];
b = key[8..15]  ^ blockhash[8..15];
c = key[16..19] ^ blockhash[16..23];

ab = a * b // 16 bytes

return (ab[0..7] ^ ab[8..15]) + c * (blockhash[24..31] | 1);

Contract storage, page and bytecode keys

a = key[0..7]   ^ blockhash[0..7];
b = key[8..15]  ^ blockhash[8..15];
c = key[16..23] ^ blockhash[16..23];
d = key[24..31] ^ (blockhash[24..31] | 1);

ab = a * b // 16 bytes
cd = c * d // 16 bytes

return (ab[0..7] ^ ab[8..15]) ^ (cd[0..7] ^ cd[8..15]);

Both hash functions share the same seeding code, which forces blockhash[24..31] | 1 to be odd. This makes the multiplication in the address hash invertible, so distinct inputs stay distinct in that term. It has no effect on the storage hash.

The table below shows the impact these custom hashes have on the COST metric:

Komihash COST per callCustom hash COST per call
20-byte addresses (aligned)77421474
32-byte keys (aligned)90691908
20-byte addresses (unaligned)80921824
32-byte keys (unaligned)97692608
Native word sizes (−0.28%)

The next interesting optimization target was the hash map container itself. The mutable hash maps in Monad use ankerl's unordered_dense::map, which structures the map into two arrays: one contains the full keys and their values, while the other contains buckets that allow the quick retrieval of a key from the first array.

struct Bucket {
   uint32_t m_dist_and_fingerprint;
   uint32_t m_value_idx;
};

Counterintuitively, a useful optimization is simply to increase the size of the bucket fields:

struct Bucket {
   uint64_t m_dist_and_fingerprint;
   uint64_t m_value_idx;
};

To understand this, consider ZisK's memory COST model for reads and writes:

ZisK memory COST for aligned 64-bit, 32-bit and unaligned accesses
Doubling the bucket size is cheaper: aligned 64-bit access costs ~10× less than any 32-bit access.

ZK proving a read or a write to a sub-word incurs additional COST over a simple aligned word access, since the prover has to establish the correct relationship between the full word and the accessed part. Having to establish these conditions across the boundary of two adjacent words makes unaligned reads and writes even more expensive. Given the number of times a bucket is accessed during the execution, the savings of 64-bit aligned access in ZisK far outweigh the cost of doubling the memory used by the bucket.

This optimization pattern was common: switching other values within the codebase from 32- to 64-bit words helped reduce similar instances of more costly 32-bit operations.

Keccak memoization (−1.65%)

The Keccak function is among the most costly functions in all surveyed guest clients, including Monad. To address this, ZisK 1.2.0 introduced a memoization mechanism, which saves encountered pairs of inputs and outputs to the Keccak permutation function. However, handling the lookups in this table directly in the guest program is too costly (a naive implementation added ~9% COST on ZisK 1.1). ZisK 1.2.0 therefore delegates the lookup to the executor, which returns the index of the matching input bytes in the table as a ZisK hint. When no index matches, the guest computes the Keccak permutation and adds a new entry to the cache. This mechanism alone reduced COST by 0.97%.

However, our experiments have shown that using the cache naively can in fact lead to worse performance. Hashing long bytecode produces a lot of Keccak input-output pairs that will never be seen again: keeping them in the cache mechanism would add a useless copy of those states without any advantage on lookup. Consequently, removing the Keccak memoization specifically for bytecode brought a COST improvement of 0.51%.

Additional analysis revealed a further optimization. While bytecode blobs in the witness are distinct, some share a common prefix. Looking for those common prefixes and only memoizing the permutations that involve these common prefixes produces an additional COST improvement of 0.17%.

Long tail

While these three optimizations individually represent only marginal improvements, the power of the agentic autoresearch comes from many small optimizations compounding to large overall gains. Over just a few weeks, the autoresearch loop produced more than 300 incremental optimizations resulting in a net 57% prover COST reduction relative to our starting point.

The chart below illustrates how these incremental improvements compounded over the course of the optimization process.

Chart of cumulative prover COST reduction over the course of the autoresearch optimization process
Autoresearch optimizations - prover COST drops to 43% of baseline and executed steps to 30% over 252 commits.


Results

While our work isn't yet finished, by applying a carefully specified autoresearch workflow, we have been able to optimize the in-development Monad-based Ethereum guest program for ZK proving of Ethereum blocks on the ZisK zkVM. At the time of writing, across the 200-block benchmark described above, the Monad guest program executed with 26% lower COST than ziskethone v1.13.0. This makes it the lowest-COST guest program on ZisK known to us. The optimization process also reduced COST by 57% relative to our initial implementation.

On one RTX 5090, on a sample of 30 blocks, this represented a reduction of 17% of the proving time compared to ziskethone for the median block, reducing the proving time to 13 seconds with ZisK 1.3.1-alpha.

Work is ongoing at Category Labs to produce a full client implementation using this guest program that can be integrated into the Ethproofs initiative. We believe that, modulo hardware configuration and future optimization (on both the zkVM and guest program), the Monad guest program running on ZisK is likely to offer among the best possible performance for real-time Ethereum proving.


Acknowledgments

Substantial credit is due to the ZisK team, who not only authored an extremely performant guest program, but also produced the zkVM on which all of our experiments are based. Our experiments and work in this blog post would not have been possible without their effort.

Optimizing the Monad Client for zkVM Execution