Skip to content

EthProofs worker: the optimized guest (in-guest JUMPDEST table, zkVM syscalls) - #26

Open
eigmax wants to merge 6 commits into
chore/ziren-v2-statelessfrom
perf/ethproofs-guest-opt
Open

eigmax wants to merge 6 commits into
chore/ziren-v2-statelessfrom
perf/ethproofs-guest-opt

Conversation

@eigmax

@eigmax eigmax commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Stacked on #23. This brings the optimized guest into the Ziren 2.0 EthProofs worker.

Why

The Fleet worker (#23 and its follow-ups) branched from the guest optimization line in April and still ships the older guest:

  • Soundness. ClientExecutorInput.bytecodes carries revm Bytecode, including the host's JumpTable. The guest checks the code hash with hash_slow, which hashes only the code bytes. A prover can therefore mark a 0x5b inside PUSH data as a valid jump target, or a real JUMPDEST as invalid, under the correct code hash. The optimized guest takes the raw code (RawCode) and derives the jump table itself.
  • Cost. The older guest lacks the zkVM syscalls for 256-bit arithmetic and the curves, the arena tries, and the revm 32-bit fast paths.

What is in it

Two commits on top of #23:

  • Prepare consecutive EthProofs blocks in parallel: the worker change the current deployment runs.

  • guest: the optimised guest line in the Ziren 2.0 EthProofs worker: the guest optimization line (feat/guest-syscalls, whose commits are public there) squashed into the worker, plus two further guest changes:

    • TrieDB and the post-state root pass reuse keccak(address) and keccak(slot) inside the zkVM.
    • The derived header takes the logs bloom that verify_receipts already compared with the receipts, instead of building every receipt bloom a second time. Optimism still accumulates it.

    Its message describes how the two lines were reconciled. In short:

    • bin/eth-proofs, the root manifest and the lock come from the worker. The git-locked Ziren 2.0 graph is kept.
    • The guest, mpt and the witness RPC database come from the optimization line.
    • The worker's storage rules are kept: untouched storage roots are preserved and empty accounts are removed (already in ArenaState::update). Omitted storage of an absent account, or of one with an authenticated empty root, reads as zero, and any other missing trie is an error; that rule is now ArenaState::storage, with its three tests.
    • Verified bytecode recovery is the optimization line's complete_account_code.

Results

Same blocks, identical committed block hashes, every proof verified:

Block Deployed guest This guest
26184581 504.6M cycles, 73.97 s 278.7M cycles, 53.73 s
26184582 557.1M cycles, 73.64 s 269.6M cycles, 53.40 s
26184583 89.0M cycles, 20.61 s 52.7M cycles, 18.01 s

The times are single-card prove times. Running as the production worker on 8 cards, the first 24 blocks after the switch averaged 7.97 s per block (median 7.82 s, p90 9.89 s). The last 100 blocks of the previous guest averaged 11.98 s (median 10.92 s, p90 16.51 s).

  • Guest ELF SHA-256: 941629f6acb9a229d810a7cb32dbd0c5d57ad050bf1ad43b5edd7d2a21454e1b
  • Program vk: 0x00e87eb3c2f0e827d29811dddb265bf03653e65acc08ca48e88536c83ed9de79
  • cargo test -p mpt: 24 passed.

Further guest changes

Four more commits make the guest cheaper and close one more input hole:

  • perf(guest): bind the chain to mainnet, gate the precompile tracker. The proof commits only the block hash, so the chain spec and the beneficiary override in the input came from the prover unchecked. The guest now accepts mainnet inputs only, with no custom beneficiary. The per-precompile cycle-tracker wrapper, which turned the precompile set into a map probed on every call, is now behind a precompile-tracking feature.
  • perf(guest): walk the JUMPDEST map in assembly on the zkVM. The in-guest jump-table walk is hand-scheduled MIPS (jumpdest.s): six instructions per opcode instead of nine, with the branch delay slots filled and a read-ahead that stays inside the code's zero padding. It was checked against the Rust walk on every code of two mainnet blocks and on 2,000 random codes. Native builds keep the Rust walk.
  • perf(guest): revm fork with cheaper dispatch and jumps. This moves to ziren-patches/revm patch-31.0.2-mips32-dispatch (interpreter: cheaper dispatch and jumps on 32-bit ziren-patches/revm#4): static gas charged on the low word of the remaining gas, JUMPDEST folded into the taken jump that lands on it, and PUSH2 + JUMP / JUMPI run as one instruction.
  • deps: Ziren from perf/shamir-msm. This uses perf(zkvm-lib): Shamir's trick in the two-point MSM Ziren#562 (Shamir's trick in the zkVM lib's two-point MSM, which ECDSA recovery runs per transaction). The Ziren checkout the guest builds from is to be on that branch. The lockfile also keeps the precompile patches' main-branch zkm-lib, zkm-primitives and zkm-zkvm; those edges exist only for the zkVM target, where the guest's path patch replaces them.

Once revm#4 and Ziren#562 merge, the revm and Ziren dependencies go back to patch-31.0.2-mips32 and main.

Measured in the executor on blocks 26184948 / 26184949, with identical committed block hashes:

Change Cycles Main-trace area
Chain binding, precompile tracker gated −0.19% / −0.28% −0.11% / −0.16%
JUMPDEST walk in assembly −1.39% / −1.79% −0.80% / −1.00%
revm dispatch and jumps −3.20% / −3.44% −2.06% / −2.16%
Shamir MSM −1.05% / −1.00% −1.83% / −1.71%
All four, vs f30cf7d 402.97M → 379.91M (−5.72%) / 233.60M → 218.71M (−6.38%) −4.72% / −4.95%

These four have not been proved on a GPU yet. On the current 5-card production, proving time fits 4.44 s + 33.7 ms per million cycles over the last 200 blocks, which puts the expected gain at about 4% of the mean block time (12.45 s → ~12.0 s). The program vk changes with the guest ELF.

The Fleet worker (4849354, chore/ziren-v2-stateless + fix/ethproofs-consecutive)
forked from the guest optimisation line at 9215aa6 and shipped the older guest:
no in-guest JUMPDEST table (the host's revm JumpTable was trusted under a code
hash that covers only the code bytes), no zkVM syscalls for 256-bit arithmetic
and the curves, no arena tries. This squashes perf/guest-hashed-key-memo (7147f4e:
feat/guest-syscalls 4a15fee plus the trie-key memo and the checked logs bloom).

Resolution:
- bin/eth-proofs, the root Cargo.toml and Cargo.lock: the worker's. The worker
  keeps the git-locked Ziren 2.0 graph; the opt line's sibling-checkout [patch]
  section is dropped.
- Guest, mpt and the witness RPC db: the opt line's. The worker's
  2c0245a (preserve untouched storage roots, remove empty accounts) was already
  in ArenaState::update; its 41317cd (omitted storage of an absent account or
  an authenticated empty storage root reads zero, any other missing storage
  trie is an error) is now ArenaState::storage, with its three tests.
  a75cb33 (verified bytecode recovery) is the opt line's complete_account_code;
  the duplicated structured-header decoder is kept once.
- bin/guest keeps the opt line's graph (precompile forks); its zkm-zkvm/zkm-lib
  [patch] points at the sibling Ziren checkout, whose guest-side crates
  (crates/zkvm, crates/build) are identical to 0dd65a15 and 31f6e8eb.

Validation:
- cargo test -p mpt: 24 passed (the arena storage rule and the update rule).
- host (this tree) fetched and executed 26184581-26184583 natively: 278.70M,
  269.57M, 52.72M cycles, block hashes as the chain's.
- GPU, one card through Fleet (image e786dd = Ziren 31f6e8eb), guest
  941629f6: 53.73/53.40/18.01 s, every proof verified against a 31f6e8eb
  client, public values identical to the deployed guest's
  (334b45b8: 73.97/73.64/20.61 s on the same blocks, -24.5% overall).
@eigmax
eigmax force-pushed the perf/ethproofs-guest-opt branch from d572e65 to f30cf7d Compare October 15, 2026 01:18
The proof commits only the block hash, so the chain spec and the
beneficiary override in the input came from the prover unchecked. The
guest now accepts mainnet inputs only, with no custom beneficiary.

The cycle-tracker wrapper around every precompile call turned the
precompile set into a map probed on each call. It is now behind the
precompile-tracking feature, for profiling builds.

Measured on the executor (blocks 26184948 / 26184949, same committed
hashes): cycles -0.19% / -0.28%, main-trace area -0.11% / -0.16%.
The guest derives every executed code's jump table itself, a walk over
~3 M opcodes per mainnet block. Compiled, the walk is nine instructions
per opcode, two of them nops in branch delay slots. jumpdest.s fills the
slots (the cursor advance, and the next opcode's load, which reads at
most 32 bytes into the code's zero padding): six per opcode. Off the
zkVM the Rust walk stays.

Checked in the executor against the Rust walk on every code of two
mainnet blocks and 2,000 random codes. Measured (blocks 26184948 /
26184949, same committed hashes): cycles -1.39% / -1.79%, main-trace
area -0.80% / -1.00%.
Moves the guest to the fork's patch-31.0.2-mips32-dispatch branch, which
adds to the 32-bit fast paths: static gas charged on the low word of the
remaining gas, JUMPDEST folded into the taken jump that lands on it, and
PUSH2 + JUMP / JUMPI run as one instruction.

Measured on the executor (blocks 26184948 / 26184949, same committed
hashes): cycles -3.20% / -3.44%, main-trace area -2.06% / -2.16%.
Every Ziren dependency now names the perf/shamir-msm branch (Ziren
#562: Shamir's trick in the zkVM lib's two-point MSM, which ECDSA
recovery runs per transaction), and the guest's comment says the Ziren
checkout next to this repo, which its zkvm crates come from, is to be
on that branch.

The precompile patches still declare Ziren unbranched, and cargo cannot
redirect a git source to another branch of it, so the lockfile also
keeps their main-branch zkm-lib, zkm-primitives and zkm-zkvm. Those
edges exist only for the zkVM target, where the guest's path patch
replaces both with the one checkout. Back to main when #562 lands.

Measured on the executor (blocks 26184948 / 26184949, same committed
hashes): main-trace area -1.83% / -1.71%, cycles -1.05% / -1.00%.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant