Skip to content

Instantly share code, notes, and snippets.

@pirapira
Created August 31, 2026 21:53
Show Gist options
  • Select an option

  • Save pirapira/2c5418c6eab7a66a061b01187bc9fe6a to your computer and use it in GitHub Desktop.

Select an option

Save pirapira/2c5418c6eab7a66a061b01187bc9fe6a to your computer and use it in GitHub Desktop.
evm-asm-bench-20260831.md

ZisK Stateless Guest: Block 115260 Benchmark

Date: 2026-08-31
Block: 115260 (glamsterdam-devnet-7, chainId 7082904758)
Guest: stateless_guest (EvmAsm, Linux93 halt)
Emulator: ZisK ziskemu v1.2.0-alpha (fbbc69b, 2026-08-26)


Block Details

Field Value
Network glamsterdam-devnet-7
Chain ID 7082904758
Block number 115260
Gas used 65,318,413
statelessInputBytes size 568,669 bytes
statelessOutputBytes size 69 bytes
Fixture archive 115260-115269.tar.zst

Expected output (statelessOutputBytes, hex):

7734570c97a937506b9b771b328a2e5bdb8b74af65c54c747603e4b3d1e8d7ce
0125000000b68c2ca6010000000c0000000400000008000000080000000000000000000000

(69 bytes: 32-byte post-state root || 37-byte StatelessValidationResult)


Prerequisites

  • Lean 4 + Lake (for building the guest ELF)
  • riscv64-elf-ld (RISC-V cross-linker)
  • ziskemu v1.2.0+ at ~/.zisk/bin/ziskemu
  • Python 3 (for fixture parsing)
  • zstd (for decompressing the archive)

Step 1 – Build the Guest ELF

# Inside the evm-asm repo
lake exe codegen --program stateless_guest --halt linux93 -o gen-out/stateless_guest

This produces gen-out/stateless_guest.o.

Linker workaround for ZisK 1.2.0

ZisK 1.2.0 rejects any ELF whose PT_LOAD segments fall below 0x80000000 (ROM start). GNU ld places ELF program headers at 0x7ffff000 when text is at 0x80000000, which triggers the rejection. Fix with an explicit PHDRS block:

# /tmp/zisk_clean.ld
PHDRS {
  text PT_LOAD FLAGS(5);
  data PT_LOAD FLAGS(6);
  bss  PT_LOAD FLAGS(6);
  rlp  PT_LOAD FLAGS(6);
  ssz  PT_LOAD FLAGS(6);
}
SECTIONS {
  . = 0x80000000;
  .text : { *(.text .text.*) *(.rodata .rodata.*) } :text
  . = 0xa0b00000;
  .data : { *(.data .data.*) *(.sdata .sdata.*) } :data
  . = 0xa0b70000;
  .bss (NOLOAD) : { *(.bss .bss.*) *(COMMON) } :bss
  . = 0xbf5e2000;
  .rlp_recursive_frame (NOLOAD) : { *(.rlp_recursive_frame) } :rlp
  . = 0xbf980000;
  .sszscratch (NOLOAD) : { *(.sszscratch) } :ssz
  /DISCARD/ : { *(.riscv.attributes) *(.note.*) *(.comment) }
}
riscv64-elf-ld -T /tmp/zisk_clean.ld -nostdlib --no-relax \
  -o gen-out/stateless_guest_clean.elf gen-out/stateless_guest.o

The resulting ELF starts at 0x80000000 and is accepted by ZisK 1.2.0.

Note: The first PT_LOAD segment is still PF_X | PF_R; ZisK warns that PF_X (execute-and-not-read) would give maximum performance. This affects proving performance but not emulation correctness.


Step 2 – Prepare the Input

The fixture's statelessInputBytes field is a schema-prefixed SSZ blob. ZisK's input convention requires the blob to be prefixed with an 8-byte little-endian length word and zero-padded to 8-byte alignment.

#!/usr/bin/env python3
import json, struct, pathlib, tarfile, io

# Extract the fixture
archive = pathlib.Path("115260-115269.tar.zst")
# (requires zstd support; use: tar -I zstd -xf 115260-115269.tar.zst)

with tarfile.open(archive, "r:zst") as tf:
    # Find the block 115260 fixture
    names = [n for n in tf.getnames() if "115260" in n and n.endswith(".json")]
    fixture_name = names[0]
    data = json.loads(tf.extractfile(fixture_name).read())

# Navigate to statelessInputBytes (schema: {network: {block: {_info, statelessInputBytes, ...}}})
network = next(iter(data.values()))
block   = next(iter(network.values()))
hex_input = block["statelessInputBytes"]
blob = bytes.fromhex(hex_input[2:] if hex_input.startswith("0x") else hex_input)

# Pack: 8-byte LE length prefix + blob + padding
length_prefix = struct.pack("<Q", len(blob))
payload = length_prefix + blob
padded  = payload + b"\x00" * ((8 - len(payload) % 8) % 8)

out = pathlib.Path("block115260.input")
out.write_bytes(padded)
print(f"blob={len(blob)} bytes, packed={len(padded)} bytes -> {out}")

Expected output: blob=568669 bytes, packed=568680 bytes -> block115260.input


Step 3 – Run ZisK Emulator

~/.zisk/bin/ziskemu \
  -e gen-out/stateless_guest_clean.elf \
  -i block115260.input \
  -o block115260.output \
  -m -X --sdk -S

Flag meanings:

  • -e ELF binary (the guest)
  • -i input file (packed blob)
  • -o output file (where the guest writes its result)
  • -m log metrics (steps, cost)
  • -X extended stats (cost distribution by category)
  • --sdk SDK-style cost report
  • -S load ELF symbols for symbol-annotated traces

Results

Performance (emulation only, no proving)

Metric Value
Steps 4,345,958,375
ZisK cost 676,878,588,802
Wall-clock (emulation) ~142.5 s
Throughput ~30.5 Msteps/s

Cost distribution

Category Cost Share
Main (EVM interpreter loop) 295,525,169,500 43.7%
Precompiles (keccak256, etc.) 276,199,796,808 40.8%
Memory 67,278,850,968 9.9%
Opcodes 37,587,461,702 5.6%
Base 287,309,824 0.0%
Total 676,878,588,802 100%

The dominant cost is EVM execution + keccak precompile (together >84%), which is consistent with the block's 65 Mgas workload and keccak-heavy witness hashing.

Raw summary from ziskemu

╔══════════════════════════════════════════════════════════════╗
║  ◆ REPORT SUMMARY                                            ║
╠══════════════════════════════════════════════════════════════╣
║  STEPS                                        4,345,958,375  ║
║  COST                                       676,878,588,802  ║
║  RAM                                    0.00 MB /   0.00 MB  ║
╚══════════════════════════════════════════════════════════════╝

╔══════════════════════════════════════════════════════════════╗
║  ◆ COST DISTRIBUTION SUMMARY                                 ║
╠══════════════════════════════════════════════════════════════╣
║  Base         ░░░░░░░░░░░░░     287,309,824   0.0%           ║
║  Main         ██████████████ 295,525,169,500  43.7%          ║
║  Opcodes      ████░░░░░░░░░░  37,587,461,702   5.6%          ║
║  Precompiles  █████████████░ 276,199,796,808  40.8%          ║
║  Memory       ████░░░░░░░░░░  67,278,850,968   9.9%          ║
╚══════════════════════════════════════════════════════════════╝

process_rom() steps=4345958375 duration=142.5259 tp=30.4924 Msteps/s

Known issue: output file

The guest writes its 69-byte StatelessValidationResult to OUTPUT_ADDR = 0xa0010000. In ZisK v1.2.0-alpha, the emulator's -o file is populated with 256 bytes of zeros regardless of what the guest writes to RAM. This appears to be a change in the output-file convention between ZisK versions (the project targets ZisK 1.2.0, which may expect a different output-flush mechanism). The execution still runs to completion (all 4.35B steps), so the performance metrics above are valid.


Comparison with Other Stateless Clients

Reth (Ress project)

Ress is reth's stateless execution mode. According to Ethereum Foundation zkEVM benchmarking work (Benchmarking zkVMs for Ethereum), Ress achieves P99 < 1 s block validation latency on Holesky — running natively on the host CPU with state access replaced by witness data.

Ethrex

Ethrex participates in benchmarkoor and has been validated on all glamsterdam devnets. Stateless execution times are measured through the zkevm-benchmark-workload harness. Typical native (non-zkVM) stateless validation is in the tens-of-milliseconds range for similar gas loads.

ZisK (this run) — proving path

Layer Value
Emulation (this run) ~142 s
Estimated proving overhead 1.3–1.5× vs execution-only (from zkEVM benchmark data)
Estimated total proving time ~200–250 s (single machine, no parallelism)

The ZisK cost figure (676 B) is the metric the ZisK prover uses to schedule parallel proving segments. Proving time scales roughly linearly with cost and depends heavily on hardware. With ZisK's segmented parallel prover and sufficient GPUs, proving latency is expected to drop well below wall-clock emulation time.

Summary

Client Mode Latency
Reth (Ress) Native stateless P99 < 1 s
Ethrex Native stateless O(10 ms) typical
EvmAsm / ZisK zkVM emulation ~142 s (emulation), ~200–250 s (estimated proof)

The zkVM path is 2–3 orders of magnitude slower than native stateless execution, but produces a succinct cryptographic proof of correct block execution — the tradeoff is proof overhead for verifiability.


File Inventory

File Description
115260-115269.tar.zst Source fixture archive
gen-out/stateless_guest.o Codegen output (RISC-V object)
gen-out/stateless_guest_clean.elf Re-linked ELF (ZisK 1.2.0 compatible)
/tmp/zisk_clean.ld Custom linker script (explicit PHDRS)
block115260.input Packed ziskemu input (568,680 bytes)
block115260.output ziskemu output (all zeros — see known issue)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment