Today we are releasing Zeth, an open-source ZK block prover for Ethereum built on the RISC Zero zkVM. Zeth makes it possible to prove that a given Ethereum block is valid without relying on the validator or sync committees by doing all of the work needed to construct a new block within the zkVM. Zeth has been verified to work on several real-world blocks from Ethereum mainnet and passes all relevant tests from the official Ethereum testsuite. Building on zkVM’s Rust support and robust crates including revm, ethers, and alloy, we were able to achieve this level of compatibility in under 4 weeks of work. Through zkVM’s support for continuations and the Bonsai proving service, Zeth can generate these proofs in minutes. And with our support for onchain verification, anyone can cheaply verify those proofs on chain. In this post we will fill in the details, and if you’d like to dig in to the code check out the source and visit the RISC Zero developer’s portal.
Ethereum was not originally designed around ZK-friendliness, so there are many parts of the Ethereum protocol that take a large amount of computation to ZK-prove. Type 1 aims to replicate Ethereum exactly, and so it has no way of mitigating these inefficiencies.
At present, proofs for Ethereum blocks take many hours to produce.
While that may have been the case historically, today we are pleased to announce that Ethereum blocks can be proven in minutes, not hours, using RISC Zero’s zkVM and Bonsai proving service.
Zeth: verifiable Ethereum block construction
Today we are releasing Zeth, an open-source ZK block prover for Ethereum built on the RISC Zero zkVM.
Zeth makes it possible to prove that a given Ethereum block is valid (i.e., is the result of applying the given list of transactions to the parent block) without relying on the validator or sync committees. This is because Zeth does all of the work needed to construct a new block from within the zkVM, including:
Verifying transaction signatures.
Verifying account & storage state against the parent block’s state root.
Applying transactions.
Paying fees to the block author.
Updating the state root.
Etc.
After constructing the new block, Zeth calculates and outputs its hash. By running this process within the zkVM, we obtain a ZK proof that the new block is valid.
Zeth has been verified to work on several real-world blocks from Ethereum mainnet and passes all relevant tests from the official Ethereum testsuite.
Because Zeth constructs standard Ethereum blocks, it can be thought of as a Type 1 zkEVM. But it’s more than that: because Zeth was built using standard Rust crates — the same crates used by popular full nodes like Reth — we like to think of it as a Type 0 zkEVM: full protocol compatibility, together with significant code re-use.
We believe this milestone represents a major step forward for ZK technology and the Ethereum ecosystem. In this post, we discuss how we were able to write the first version of Zeth in a matter of weeks, its performance, how it works, and what this means for ZK projects.
RISC Zero is making it easier to build ZK rollups, zkEVMs, light clients, and bridges
We wrote Zeth for two reasons:
To make it easier for other teams to build their own ZK-powered infra: ZK rollups, zkEVMs, ZK light clients, ZK bridges, etc. Zeth provides everything you need generate ZK proofs for EVM-based blocks. This is a key component in any zkEVM or bridge. Zeth is open source and based on revm, so you can easily modify and use it in your own projects. The proofs can be verified onchain (perfect for bridges and L2s) or in native apps (perfect for full nodes and light clients).
To investigate the performance of EVM in our zkVM, specifically for Ethereum-related tasks. (See below for an analysis of our findings.)
As a Type 0 zkEVM, Zeth enables developers to build ZK rollups with completely native EVM and Ethereum support. Together with our support for onchain proof verification, it’s never been easier to build an L2 powered by ZK.
Existing ZK rollups and zkEVM circuits are monolithic in design and lack upgradability without a high level understanding of ZK cryptography from the developer. In contrast, our zkVM based approach to Zeth enables any developer to customize and modify to suit their needs.
Zeth is open source, based on revm, and can easily be adapted to support other zkEVMs and EVM-compatible chains. This ensures easy updates to Zeth for future EIPs while providing modularity that enables developers to build their own block construction logic into Zeth.
We hope these foundations will democratize ZK rollups and zkEVMs, which previously have required multiple years of development and 100+ million dollars of funding making them unreachable for the majority of projects.
Light clients and bridges
The introduction of beacon chains is, without a doubt, a boon for light clients and bridges. These technologies, which build on Ethereum’s now-well-established proof of stake model, make it easy for light clients and bridges to verify recent blocks without rebuilding those blocks — assuming everyone is playing by the rules.
Of course, the entire point of staking is to provide an economic incentive to play by the rules. But the threat of slashing is not a guarantee that something bad will never happen. External incentives can “tip the scale” in favor of shenanigans — and designing a light client or bridge that handles those shenanigans correctly is hard.
With tools like Zeth, the risk of shenanigans is greatly reduced. Light clients can integrate with Zeth simply by adding a few calls to our zkVM; and onchain applications, such as bridges, can integrate with Zeth using our onchain proof verification contracts.
In the near future, one could imagine light clients and bridges that use ZK proofs to be certain that a given block is valid. This approach would significantly reduce risk without significantly increasing the cost of verifying a block.
This is particularly important for app chains, the modular ecosystem, and new chains that don’t yet have the same level of security offered by Ethereum’s large community of full nodes.
An easy project, thanks to good foundations
Zeth is built on the RISC Zero zkVM, which provides a familiar programming experience powered by the RISC-V instruction set architecture. But our zkVM isn’t just a RISC-V core. We also have accelerator circuits for common cryptographic tasks, such as hashing and signature verification.
This hybrid approach (a general-purpose CPU core augmented with accelerator circuits) gives us the best of both worlds:
Support for mainstream programming languages.
No-compromise performance for critical cryptographic operations.
Thus, we were able to rapidly build Zeth using existing Rust crates from revm, ethers, and alloy. By reusing existing crates, we were able to get the first version of Zeth working in under 4 weeks. This engineering velocity would not have been possible in less mature ecosystems.
For performance, Zeth takes advantage of our accelerator circuit for ECDSA signature verification, as well as continuations — a novel feature of our ZK framework that makes it easy to quickly prove large computations using clusters of GPUs working in parallel (using either nVidia CUDA or Apple Metal). Continuations are easy to use: the feature is transparently provided to all guest programs running in the zkVM. You don’t need to make any changes to your code; it just works.
With our zkVM, we are able to cheaply generate a ZK proof of the validity an Ethereum block in minutes, not hours.
Performance
In this section we describe the performance of the Zeth block builder. Zeth is still new, so these numbers are subject to change; still, we wanted to provide some concrete data as a baseline against which future efforts can be compared.
When it comes to performance, there’s several factors to consider:
Computational resources required to generate a proof.
“Wall time” required to generate a proof (i.e., how long the user needs to wait to get their proof).
Total cost ($) for proof generation.
Continuations
With our zkVM it’s possible to tune performance through the use of continuations. Therefore, we need to pause for a moment to discuss how continuations work.
Our zkVM implements a standard RISC-V processor. Therefore, execution proceeds in cycles. (In our circuit, most RISC-V instructions take only 1 cycle to execute, though there are some exceptions.) Simple programs typically only require a few hundred thousand cycles to execute, but more complex programs can easily require several billion cycles.
In typical ZK systems, these cycles of execution are gathered together into a single proof; as the number of cycles increases, so too does the time and memory required to generate that proof. But our zkVM is not typical. Earlier this year, we were the first to launch a new feature — continuations — that improves on this paradigm significantly.
With continuations, the proving process is split into three phases:
We perform the desired computation in a non-proving emulator. As we do so, we count the number of cycles that have been run so-far. At a configurable interval, we take snapshots of the program’s state. This effectively splits the execution into multiple segments. Each segment is small, typically representing 1M cycles or fewer.
The segments are distributed to a pool of workers. The workers generate ZK proofs for their given segments. Importantly, they are able to do this in parallel. With enough workers, all of the segments can be proven in the time required to prove only 1 segment. Because the segments are small, this time is typically very short (a couple dozen seconds).
As the segment proofs are generated, they are “rolled up.” Each rollup operation takes a pair of sequential segment proofs and generates a new proof for the composition of the segments. For example, if Segment 1 proves that the program transitioned from state A to state B, and Segment 2 proves that the program transitioned from state B to state C, then the rollup proves that the program transitioned from state A to state C. With enough workers, this can be done in log(N) time, where N is the number of segments.
We’ll see these phases in action as we dive into the numbers.
How hard is it to construct an Ethereum block?
Without further ado, let’s look at some numbers!
First, let’s look at the complexity of building Ethereum blocks. In the table that follows, we picked some real-world Ethereum blocks and reconstructed them using Zeth from within the zkVM.
Block no.
Transactions
Gas
Segments
Total cycles (upper bound)
17034871
267
29,970,493
4675
4,902,092,800
17049942
206
29,987,306
3261
3,419,406,336
17095624
163
12,684,901
2095
2,196,766,720
17106222
105
10,781,405
1816
1,904,214,016
17181191
139
14,560,621
2007
2,104,492,032
17408122
135
26,256,143
2543
2,666,528,768
17495654
112
20,791,473
2986
3,131,047,936
17546837
168
17,279,191
2549
2,672,820,224
17606237
174
15,957,890
4079
4,277,141,504
17606771
139
12,595,376
2131
2,234,515,456
17640464
156
13,493,687
2167
2,272,264,192
17647876
101
10,542,312
2013
2,110,783,488
17682361
104
13,157,104
2105
2,207,252,480
17691128
112
14,838,663
9098
9,539,944,448
17735424
182
16,580,448
3242
3,399,483,392
For example, block 17606771 generated 2131 segments. Each segment represents at-most 2^20 cycles of execution, so the entire computation required at-most 2,234,515,456 cycles of execution.
In general, we see that a typical Ethereum block requires 2-4 billion cycles to construct, but sometimes as many as 9.5 billion. (At first we were surprised to see that these differences are not reflected in the transaction’s gas. But on further reflection, this makes sense: the gas system was designed with conventional execution in mind, not ZK proving.)
With continuations, this scale is easy to manage. Based on these data, a peer-to-peer network with 10,000 nodes running the zkVM prover would be sufficient to achieve maximum parallel proving performance for the largest blocks — a tiny fraction of the 700,000 validators Ethereum currently has.
How long does it take to generate a proof?
To gather some basic performance data, we spun up a test instance of Bonsai with 64 GPU workers. We then asked it to prove block 17735424 (182 transactions, 3242 segments, or roughly 3.4B cycles) using Zeth.
To generate the proof, the zkVM must first split execution into segments. In the screenshot below, this is captured by the Executor task, which took 10 minutes to run. (Much of that time was spent doing AWS things, like writing to network storage. On local machines, this same task completes in under 6 minutes. We expect to lower this time significantly over the coming year.)
The executor ultimately split execution into 3242 segments. That’s a lot of segments for a mere 64 GPUs. As such, each worker node had to generate 50 segment proofs. As shown in the screenshot below, this took 35 minutes. If we had 50x as many workers, this would have only required 42 seconds.
After the segment proofs were complete, the rollup began. With 3242 segments, we need to do log_2(3242) = 12 rounds of the rollup procedure. In the early stages of the rollup, there was more work than workers; so the first stage took 1 minute, the second stage took 35 seconds, the third took 25, etc. By the 7th stage, times stabilized at a little over 5 seconds. Again, if we had more workers, every stage would have only taken 5 seconds.
Lastly, after the rollup is complete, the result is finalized, which takes another minute.
Therefore, with our under-sized cluster, we were able to generate the proof in about 50 minutes end to end (giving an effective speed of 1.1 MHz). With a properly sized cluster, we estimate the proof could be generated much quicker:
With full parallelism, the proving steps could be completed in 42 + 12 * 5 + 60 seconds, or 2 minutes and 42 seconds.
If we conservatively round up and include the executor time, this comes to somewhere between 9 and 12 minutes (giving an effective speed of 4.7 MHz - 6.3 MHz.)
With continued improvements to our executor and proving framework, we are optimistic that this time will be reduced greatly over the coming year.
How expensive is it to generate a proof?
The test cluster described above was deployed to AWS. It consisted of 64 g5.xlarge proving nodes and 1 m5zn.xlarge executor node. According to Amazon, each g5.xlarge node has:
1 GPU with 24 GiB of GPU memory
4 vCPUs with 16 GiB of memory
At time of writing, the on-demand price for these instances is $1.006/hr, with a reduced price of $0.402/hr for reserved instances. Meanwhile, the Amazon spec sheet indicates our m5zn.xlarge node has:
4 vCPU with 16 GB of memory
At time of writing, the on-demand price for this instance is $0.3303/hr.
We can use these numbers to generate a rough estimate for the cost of the proof describe above for block 17735424.
Recall that we deployed 64 proving nodes, and with that deployment, the proof required 50 minutes to generate (end-to-end). Ignoring idle worker time, 64 proving nodes, plus one executive node, for 50 minutes, comes to 50/60 * (64 * $0.402 + $0.3303) = $21.72. This is an over-estimate, since it assumes we are paying for idle workers. If we exclude the cost of idle workers (say, by shutting them down or having them do other work), the cost is roughly $19.61.
This block had 182 transactions, which comes out to $0.11 per transaction.
The total transaction value was 1.125045057 Eth, or roughly $2137.59. Thus, each dollar of proving secured $109.01 of user funds.
That same block paid 0.117623263003047027 Eth in rewards (not including transaction fees). At time of writing, this is roughly $223.48. Therefore, our proof cost roughly 8.7% of the block reward to generate.
Transaction fees added up to 0.03277635 Eth, or $62.28, which is more than 3x the cost of our proof.
Remarkably, these dollar estimates are independent of the size of the cluster! All that matters is the number of segments. This is because 1 machine doing 2 jobs in sequence costs the same as 2 machines doing 1 job each in parallel. Consequently, with a larger cluster, the proof would be faster to generate but no more expensive.
There are several ways to reduce this cost even further. In addition to continued performance improvements to our zkVM, and perhaps the addition of a Keccak accelerator, we could shop around for cheaper instances. Importantly, given the low specs of the machines we used (and our zkVM’s support for nVidia Cuda and Apple Metal), this work could easily be done by a p2p network consisting of ordinary consumer PCs and Macs.
If we view the transaction inputs as UTF-8 data, we see that this proof corresponds to block 17735424.
Using Bonsai, the conversion from STARK to SNARK took about 40 seconds. Verifying the SNARK onchain consumed 245,129 gas (roughly $5.90 at time of writing.)
Of course, one of the nice things about zkVM is that it can rollup several proofs into one. With this feature, an entire set of proofs can be verified onchain without using any additional gas. In this way, the onchain verification cost can be amortized across the set, resulting in lower fees for everyone involved.
What this means for Ethereum
As previously noted, Ethereum was not designed with ZK-friendliness in mind. As the landscape of zkEVMs shows, there are many things that could be done differently, notably around opcodes, digital signatures, and hash functions.
While these changes do improve performance, we were still able to achieve solid performance without them. When Vitalik wrote about the different types of zkEVMs last year, proving the validity of an Ethereum block took hours; today we can do it in minutes. ZK performance is improving at a rapid pace and there’s good reason to believe this trend will continue over the next several years.
Ready to build on zkVM?
Check out these resources, and come say hi on Discord.
Roughly speaking, Zeth builds blocks the same way full nodes do: we start with a parent block, a list of transactions, and a block author; then we do a bunch of computation (verify signatures, run transactions, update the global state, etc); then we return (the hash of) the new block.
But unlike a full node, we do this from within the zkVM. This means we obtain a ZK proof that the block with the given hash is valid.
Of course, this is not without its challenges. In this section, we describe those challenges and how we addressed them.
Cryptography
The first challenge is cryptography. Constructing an Ethereum block requires doing a lot of it, most notably hashing (Keccak-256) and signature verification (ECDSA with secp256k1).
But when it comes to hashing, we aren’t so lucky. Our zkVM has accelerated support for Sha2-256, but (at time of writing) not Keccak-256. So for now, we’re simply using the Keccak implementation from the sha3 Rust crate. Through profiling, we know that a significant number of cycles goes into this. This is not optimal, but our zkVM can handle it, and we can always loop back and add a Keccak accelerator later.
Accounts and storage: performance and security
In Ethereum, accounts and storage are tracked by a global Merkle Patricia Trie (MPT).
According to Etherscan, at time of writing this tree contains state for nearly 250,000,000 unique Ethereum addresses. That’s not a lot of data in the grand scheme of things, but it is enough data that one must be careful with how its stored and used. In particular, MPT performance is crucial.
But performance is not the only factor; we must also consider security.
Zeth’s block builder runs as a guest within the zkVM. This means it does not have direct access to the Ethereum p2p network or other RPC providers. Instead, it must rely on data provided by a separate program that runs outside the zkVM.
When analyzing the security of ZK apps, one must assume that outside programs are malicious, and consequently, might supply malicious data. To protect against this, ZK apps must verify that the data given to them is valid.
For the Zeth block builder, this means verifying the state of all relevant accounts and storage (i.e., the accounts and storage needed to run the given list of transactions).
Luckily, a mechanism for doing this is provided by EIP-1186, which defines a standard way to prove the state of a given account (and its storage) via Merkle inclusion.
In principle, Zeth’s block builder could verify account and storage states simply by verifying a set of EIP-1186 inclusion proofs. But this approach is not optimal.
Instead, it is preferable to use the data from EIP-1186 inclusion proofs to construct a partial MPT. This is an MPT that contains only the nodes that are relevant to the given list of transactions; the irrelevant branches are simply represented by their corresponding hashes. You can think of a partial MPT as being a kind of “union” of Merkle inclusion proofs; or, if you prefer, you can think of it as a Merkle subset proof.
The process for verifying a partial MPT is basically the same as verifying an ordinary EIP-1186 proof: calculate the root hash and compare to the parent block’s state root. If they’re equal, then you can trust the integrity of the accounts and storage held within.
After the partial MPT has been verified, the transactions can be applied and the partial MPT can be updated. The new state root can then be obtained by calculating the partial MPT’s new root hash.
This is the approach we took with the Zeth block builder. To summarize,
Before we run the Zeth block builder, we run the list of transactions in a sandbox to determine which accounts and storage are relevant. (This process also lets us determine the oldest relevant predecessor block, which is needed to support blockhash() queries.)
We fetch EIP-1186 inclusion proofs for each of the relevant accounts and storage. (We also fetch the relevant predecessor blocks.)
We use those inclusion proofs to construct a partial MPT that contains all of the relevant data.
We launch the zkVM, tell it to run the Zeth block builder, and supply the partial MPT together with the other inputs (the parent block, the list of transactions, etc).
From within the zkVM, the Zeth block builder:
Verifies that the partial MPT root matches the parent block’s state root.
Verifies the hash chain of predecessor blocks back to the parent.
Applies the transactions.
Updates the partial MPT.
Uses the partial MPT’s new root hash as the state root for the new block.
When the Zeth block builder is finished, it outputs the hash of the new block.
This hash includes a commitment to the parent block, and thus the parent’s state root (which was used to verify the original partial MPT). This means a malicious prover cannot supply invalid data for accounts and storage without also supplying an invalid parent block.
In other words: if the parent block is valid, so too is the new block generated by Zeth.
Therefore, if someone gives you a new block and a ZK proof generated by Zeth, you can check the block’s validity by checking 3 things:
Make sure the ZK proof is valid and came from Zeth. For off-chain applications, this can be checked using a function provided by the zkVM Rust crate. For on-chain applications, this can be checked using our onchain proof verifier.
Make sure the ZK proof commits to the hash of the new block.
Make sure the parent block has the hash you expect it to have.
If those things all check out, then the new block is valid.
Limitations and future work
Our goal with this project was to investigate the performance of block construction. For this goal, we decided to limit our scope to post-merge blocks.
Additionally, while Zeth is able to prove that a given block is valid, it does not currently prove consensus (i.e., that the block is actually contained in the canonical chain). This might change in the future, perhaps by adding a check for the verifier or sync committee signatures from within the zkVM.
Lastly, Zeth is new software. While we have performed some testing (including the Ethereum test suite and various real-world blocks), it’s possible that Zeth still contains some bugs. At time of writing, it should be regarded as experimental.