Thanks to Justin, Vitalik, Kev, Alex, Tau, Ignacio for the discussions, feedback and comments.
EIP-8288 lets transactions depend on post-quantum signatures (leanSPHINCS) and STARK proofs (leanSTARK) that are verified outside the EVM. Each one is declared in a dependency frame of an EIP-8141 frame transaction and checked by nodes before they forward the transaction, and each block carries a single recursive STARK proving that all its dependencies are valid.
This gives privacy applications a native way to make their proofs post-quantum secure at scale. Today, a privacy pool verifies a Groth16 proof inside its contract with the BN254 pairing precompile, and a quantum computer that breaks BN254 could forge such proofs. With EIP-8288, nodes verify the proofs that users make on their own devices. Instead of running a verifier, the pool contract only checks that the transaction depends on a proof for its own circuit with the expected public inputs, then updates its state.
EIP-8288 currently identifies the computation behind each proof by a verification key hash, a placeholder until Ethereum decides what to expose. A proof-level verification key would change with every update to the proof system of leanVM, the zkVM that produces leanSTARK proofs, and break any pool that cannot be upgraded. Each proof could instead show that a program ran correctly, such as the code that checks a pool’s spends against its rules. The dependency would name a hash of that program, and proofs for every application would be checked against the same fixed rules (the relation). But Ethereum would then have to pick the instruction set (ISA) those programs are written in. That choice determines how fast users can prove on their devices, which new cryptography applications can use without waiting for a new precompile, and what Ethereum has to keep supporting for as long as immutable contracts depend on it. The closest work today is evm-asm, which proves that its RISC-V code for each EVM opcode follows the EVM spec. It does this once for all programs rather than for each one, and is not finished yet.
In this post, we compare three ISAs Ethereum could expose: the EVM, RISC-V and an Ethereum ISA designed for proving (eISA). Which one fits best depends mostly on what leanVM itself runs. We then explain how the exposed ISA can change over time without breaking immutable smart contracts, and describe the one change this requires in EIP-8288: replacing its generic leanSTARK scheme with one that identifies a program and its ISA version.
Three candidate ISAs
The three candidates mainly trade readiness against proving efficiency. The EVM could ship first, since Ethereum already maintains it. The user’s prover would not run EVM programs directly: it would run one fixed EVM interpreter compiled to its own ISA, such as RISC-V, and that interpreter executes every EVM program. Opcodes are interpreted one by one, which is slow to prove, while a precompile call runs as the interpreter’s own compiled code and costs about what it would in a RISC-V program. New cryptography therefore either pays the interpretation cost in EVM code or needs a new precompile, which has to be added to L1 since there is only one EVM.
RISC-V sits in between: slower to ship than the EVM, but faster to prove. It has the largest zkVM ecosystem and tooling, and a RISC-V prover runs its programs directly, so new cryptography is ordinary code proved at native speed rather than a new precompile. But Ethereum would first have to specify what the zkVM standard leaves open, such as the memory map (which addresses hold a program’s code, data and stack), because a compiled program has those addresses built in.
An eISA would be an ISA that Ethereum designs for proving. It could look like Valida, a RISC-inspired instruction set that works directly on memory, or like leanVM’s own leanISA, which has only six instructions over the prover’s field. The closer it is to the prover, the cheaper it is to prove, but the more often it may need new versions as the prover change and no eISA has been designed for Ethereum yet. Going one step further, Ethereum could expose a constraint format such as an AIR (the polynomial constraints a STARK checks) and let each application bring its own machine and precompiles. But those constraints are written over the prover’s field, so if a later prover used a different field, every immutable pool would break unless that prover emulated the old one.
Which of these makes sense depends on what leanVM ends up running natively, and its repository explores two options: RISC-V and its own leanISA. If leanVM runs RISC-V, RISC-V programs need no interpreter, and an eISA only matters if a later prover moves away from RISC-V. If leanVM runs leanISA, Ethereum could expose leanISA directly, or an eISA on top of it if leanISA is likely to keep changing, and RISC-V would then need an interpreter. The EVM needs an interpreter in both cases. The table below assumes leanVM runs RISC-V (RV64IM), as its RISC-V branch does today, and can prove any RISC-V program under one fixed relation.
| EVM | RISC-V* | eISA | |
|---|---|---|---|
| Proving efficiency | About 2× RISC-V for hand-written bytecode when a precompile does the heavy work, 78–163× when EVM code does | Native speed, but hashes need a zkVM precompile to be fast | Could be the best once a prover runs it natively, but not yet measured |
| New primitives (e.g. a new signature scheme) | EVM code (78–163× the cycles for BLAKE2s) or a new L1 precompile | Ordinary code at native speed | Ordinary code, once a compiler targets it |
| Stability cost | Low, since L1 already defines each fork’s rules, though the interpreter has to keep them all for older programs | Low, since the base ISA is frozen, though every exposed zkVM precompile must be supported forever | Higher, since it will likely need new versions as its prover evolves |
| Social consensus | Easiest, but potentially needs a new hash precompile | Medium, with the zkVM Standards as a starting point | Hardest |
| Mismatch with leanVM | Always needs an interpreter | Small, since leanVM mainly needs to use the memory map Ethereum pins | Needs an interpreter until leanVM runs it natively |
| Tooling | Solidity, Yul, EVM tools | Compilers, crypto libraries, Sail formal model | Needs a new compiler, like the one Valida built on LLVM for Rust and C |
| Formal verification | The largest, covering every fork’s EVM rules, the interpreter and the precompiles | The smallest today, with one frozen ISA already modeled in Sail plus the exposed zkVM precompiles | A new spec to write, though leanISA has only six instructions, one of them a BLAKE2s compression |
| Readiness | Earliest if its interpreter can be formally verified in time, since its rules already exist | Needs a spec for what the standard leaves open, such as the memory map | Needs to be designed first |
| Path dependence | Pressure to add more L1 precompiles | Tech debt if an eISA follows | A bet made before it exists |
* By RISC-V, we mean RV64IM as in the Ethereum zkVM Standards target, plus the zkVM precompiles we expose from the standard’s list, such as Keccak-256. The standard also requires misaligned access (Zicclsm), which some execution guest programs rely on, but a canonical guest written for Ethereum would not need it. Every zkVM precompile we expose adds to the mismatch with leanVM, the stability cost and the formal verification work.
The EVM numbers come from a benchmark in which we proved the same privacy pool spend as a RISC-V program and as an EVM program on leanVM’s RISC-V branch. The spend’s only heavy operation is BLAKE2s, which that branch computes with a custom instruction. For the EVM program to use it, we assumed a fork adds a BLAKE2s precompile (L1’s 0x09 computes BLAKE2b). With it, our EVM bytecode takes about 2× the cycles of a RISC-V program using the same instruction, and 2.3× the proof time. This is close to the EVM’s best case: the bytecode is hand-written, and the only heavy operation is covered by a precompile. Without that precompile, BLAKE2s written in EVM code takes 78 to 163× the cycles of a RISC-V program hashing on base instructions, and still 17 to 20× if compiled ahead of time to RISC-V, because the EVM works on 256-bit words.
Whatever ISA we pick, the prover still needs the same features: zero knowledge, continuations (proving a long run in pieces) if we want programs to outgrow one proof, recursion to aggregate dependency proofs, and a fast hash for programs to call. leanVM has not chosen that hash yet and uses BLAKE2s as a placeholder while it considers SHA-2, SHA-3 and BLAKE3. If it picks a hash L1 already has, such as SHA-256, neither EVM programs nor pools need a new precompile. If it keeps BLAKE2s, EVM programs need one, as does any pool that also uses BLAKE2s onchain, for example to update its Merkle tree.
Upgrading the exposed ISA
A privacy pool that cannot be upgraded has to keep accepting its users’ proofs for as long as it holds funds, even as the exposed ISA and the prover change.
This works if each program’s identity includes its ISA version: program_id = hash(ISA version, program bytes), computed with a hash that later forks never change. The version of an EVM program is its fork. For RISC-V, the version also has to pin what the zkVM standard leaves to each zkVM, such as the memory map and how programs call zkVM precompiles. The pool contract stores this program_id, and since a version’s existing behavior never changes once a fork activates it, the program_id always means the same program running under the same rules. As on L1, a fork can still add instructions or zkVM precompiles, but a change to anything a program can already observe needs a new version. L1 lets contracts follow the latest fork’s rules even then, because their code is onchain and core developers can see which contracts a fork would break. A pool’s program is not onchain, so nobody can check what such a change would do to it, which is why it keeps the rules it was written for.
The protocol then has to keep every activated version provable. The current prover runs each program under that program’s own version, so users always prove with the current prover, and nodes check every proof with the current fork’s verifier alone.
| Change | Existing programs | New programs |
|---|---|---|
| New instruction or zkVM precompile | Unaffected unless they call it, as on L1 | Can use it |
| Change to existing behavior (new version) | Keep their version | Use the new version |
| New prover or proof system, by fork | Proved with it from then on, under the same program ids | Same |
| New machine inside the prover (the ISA it runs natively), e.g. an eISA | Proved through an interpreter for the new machine, usually more slowly | Can target it if a fork exposes it |
Since every program is proved with the current proof system, a pool is unaffected if an old proof system later turns out to be broken. Within one ISA, a new version only adds a version check to the prover, which every later prover has to carry, much as an execution client carries every fork’s rules. The bigger price is that an ISA the prover no longer runs natively needs an interpreter forever, and that interpreter has to be ported and verified again for every new machine. Each fork’s relation and verifier also need formal verification for every activated version, and at least one maintained prover has to keep supporting all of them. Keeping every version provable therefore stays cheap only while the exposed ISA is small, grows only by addition and rarely changes.
One alternative is wrapping (h/t Vitalik): users keep proving with the old prover, then make a second proof in the new system showing that the old verifier accepts the first. Later forks then no longer prove old programs, so they need no interpreters for old ISAs. But if the old proof system is ever broken, an attacker can forge a first proof, and the second proof will only confirm that the old verifier accepts it. The second proof could also be relatively expensive, since the new prover has to run the whole old verifier every time.
Another interesting option is to never change the exposed ISA (h/t Alex). Ethereum would pick one canonical ISA, such as RISC-V, and users could prove that their program ran in any language or instruction set they like, such as WebAssembly, along with a second proof that the program compiles correctly to the canonical ISA. A new language would then need no new ISA version, but it would need its own spec, maintained alongside the canonical ISA’s, because the second proof can only show that the compilation is correct against both specs.
What this means for EIP-8288
The one change EIP-8288 needs for the ISA is to replace its generic leanSTARK scheme (0x11) with a program scheme, in which the third field holds program_id instead of a verification key hash, data_hash is the program’s public output, and each fork specifies the verifier. The program computes that output by hashing the spend’s public inputs, and the pool contract recomputes the hash from the inputs it receives. The mempool still only receives proofs, never the programs, so nodes never run or meter them.
A private transfer is then a single frame transaction with four frames:
- The recent root verifier frame of EIP-8272 checks that the spend’s Merkle root is one that a root source recently wrote.
- The pool’s
VERIFYframe reads that frame and the dependency withFRAMEPARAMandFRAMEDATACOPY. It checks that the dependency names the pool’s pinnedprogram_idand commits to the spend’s public inputs, that the spend’s nullifiers are the pool’s EIP-8250 nonce keys at sequence 0, and that the root was written by the pool’s own root source. Because EIP-8250 consumes the nullifiers at approval even if the transfer then fails, the pool also checks that theSENDERframe has enough gas and its tree has room for the new notes before it approves. - A dependency frame after the
VERIFYframe carries(scheme, data_hash, program_id), while the proof itself travels beside the transaction. Placing it there keeps a validation prefix the public mempool recognizes. - The
SENDERframe runs the transfer.
Nodes verify each proof before forwarding the transaction, and the block builder adds one recursive STARK over all of the block’s dependencies, so no verifier runs in the EVM and no verifier precompile is ever needed.
One gap outside the ISA remains before this can work in practice. In EIP-8288, the leanSPHINCS example puts the EIP-8141 signature hash in a dependency frame that this hash covers, so it can never be built. A zero data_hash could stand for the signature hash, as an empty msg does in EIP-8141, so that nothing has to be left out of the signature hash. The pool above does not need this rule, since its proof commits to public inputs that the pool checks itself.