Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
a8438b6
feat(zkevm-bench): crate skeleton + CLI
iovoid Jul 7, 2026
bc95a74
feat(zkevm-bench): report schema + JSON
iovoid Jul 7, 2026
d98eb58
feat(zkevm-bench): real-block Cache loader
iovoid Jul 7, 2026
c2a69ea
fix(zkevm-bench): drop dead eip-8025 cfg guard in cache test
iovoid Jul 7, 2026
a768f2c
feat(zkevm-bench): workload manifest
iovoid Jul 7, 2026
e021353
feat(zkevm-bench): compare + threshold
iovoid Jul 7, 2026
2a06a02
chore(zkevm-bench): capture ziskemu -X AIR-cost output format (spike)
iovoid Jul 7, 2026
c6053e6
feat(prover): parse ziskemu AIR-cost breakdown
iovoid Jul 7, 2026
74ff29d
fix(prover): use unwrap (allowed in tests) in air-cost parser test
iovoid Jul 8, 2026
8ad7921
feat(prover): ZiskBackend::execute_profiled + clear latent zisk lints
iovoid Jul 8, 2026
3c330d7
feat(zkevm-bench): run command + smoke fixture
iovoid Jul 8, 2026
4f3d6d1
fix(zkevm-bench): isolate per-workload input-build failures in run
iovoid Jul 8, 2026
d395069
feat(zkevm-bench): EEST micro workload path
iovoid Jul 8, 2026
6367e72
feat(zkevm-bench): add EEST micro workload to manifest
iovoid Jul 8, 2026
3671799
feat(zkevm-bench): curate command
iovoid Jul 8, 2026
8ae1cde
feat(zkevm-bench): curated 10 real-block fixtures + manifest
iovoid Jul 8, 2026
1345625
docs(zkevm-bench): README + smoke test
iovoid Jul 8, 2026
977f85d
fix(zkevm-bench): skip micro test when EEST vectors absent
iovoid Jul 8, 2026
9bf140d
feat(zkevm-bench): record guest zkVM RAM usage metric
iovoid Jul 8, 2026
4420b84
feat(zkevm-bench): stress-fixture loader + tiered run modes
iovoid Jul 8, 2026
21aeabf
feat(zkevm-bench): ethrex-native stress-fixture generator
iovoid Jul 8, 2026
9788627
feat(zkevm-bench): worst-case stress fixtures + manifest tiers
iovoid Jul 8, 2026
bc3bde0
refactor(zkevm-bench): route stress workloads through Cache loader; d…
iovoid Jul 8, 2026
a501300
feat(zkevm-bench): manifest-relative source paths + setup target + RE…
iovoid Jul 8, 2026
f57df02
fix(zkevm-bench): keep quick tier download-free (micro -> medium)
iovoid Jul 8, 2026
9b181c1
fix(zkevm-bench): compare flags failed/missing workloads; drop dead -…
iovoid Jul 8, 2026
154e66e
Merge remote-tracking branch 'origin/main' into feat/zkevm-execution-…
iovoid Jul 13, 2026
9707f16
chore: fix Cargo.lock versions after merge
iovoid Jul 13, 2026
a236453
fix: make zkevm_bench a standalone workspace
iovoid Jul 13, 2026
ae9fe53
fix: address zkevm_bench review comments
iovoid Jul 13, 2026
39756cb
Merge remote-tracking branch 'origin/main' into feat/zkevm-execution-…
iovoid Jul 16, 2026
632dcbc
fix: regenerate zkevm_bench Cargo.lock
iovoid Jul 17, 2026
b3353c3
fix: regenerate tooling Cargo.lock
iovoid Jul 17, 2026
1ea1257
Merge branch 'main' into feat/zkevm-execution-benchmark
iovoid Jul 17, 2026
27f8dd1
migrate zkevm_bench to zisk v1.0.0-alpha
iovoid Jul 17, 2026
b5cb510
ci(l2): pin ZisK Rust toolchain version
iovoid Jul 17, 2026
7d26eb8
Merge origin/main into feat/zkevm-execution-benchmark
ilitteri Aug 18, 2026
3537aa4
Feed the zkevm bench the spec's statelessInputBytes
ilitteri Aug 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
fixtures/blockchain/2000-blocks.rlp filter=lfs diff=lfs merge=lfs -text
fixtures/blockchain/l2-1k-erc20.rlp filter=lfs diff=lfs merge=lfs -text
tooling/zkevm_bench/fixtures/**/*.json.gz filter=lfs diff=lfs merge=lfs -text
14 changes: 14 additions & 0 deletions .github/workflows/pr-main_l2_prover.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,20 @@ jobs:
run: |
git diff --exit-code -- crates/guest-program/bin/${{ matrix.backend }}/Cargo.lock

# zkevm_bench is a standalone workspace excluded from the root workspace
# (it hard-depends on x86-64-only zisk), so it must be linted explicitly.
# --all-targets compiles the smoke test without running it (the test needs
# a full guest ELF, which CI skips). --locked doubles as a lockfile check.
- name: Clippy zkevm_bench
if: matrix.backend == 'zisk'
run: |
cargo clippy --locked --manifest-path tooling/zkevm_bench/Cargo.toml --all-targets

- name: Check zkevm_bench Cargo.lock modified but not committed
if: matrix.backend == 'zisk'
run: |
git diff --exit-code -- tooling/zkevm_bench/Cargo.lock

lint_exec:
name: Lint exec backend
needs: detect-changes
Expand Down
10 changes: 8 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ members = [
"tooling/repl",
"test",
]
exclude = ["crates/vm/levm/bench/revm_comparison"]
# zkevm_bench is a standalone workspace: it hard-depends on zisk (lib-c),
# which is x86-64-only (nasm + x86 asm) and cannot build on arm/macos. Keeping
# it out of the root workspace keeps default builds, lint and Docker portable.
exclude = [
"crates/vm/levm/bench/revm_comparison",
"tooling/zkevm_bench",
]
resolver = "2"

default-members = ["cmd/ethrex"]
Expand Down Expand Up @@ -84,7 +90,7 @@ ethrex-l2 = { path = "./crates/l2" }
ethrex-l2-common = { path = "./crates/l2/common", version = "24.0.0" }
ethrex-sdk = { path = "./crates/l2/sdk", version = "24.0.0" }
ethrex-l2-prover = { path = "./crates/l2/prover" }
ethrex-prover = { path = "./crates/prover" }
ethrex-prover = { path = "./crates/prover", default-features = false }
ethrex-guest-program = { path = "./crates/guest-program", default-features = false }
ethrex-storage-rollup = { path = "./crates/l2/storage", version = "24.0.0" }
ethrex = { path = "./cmd/ethrex" }
Expand Down
14 changes: 13 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.PHONY: build lint test clean run-image build-image clean-vectors \
setup-hive test-pattern-default run-hive run-hive-debug clean-hive-logs \
load-test-fibonacci load-test-io run-hive-eels-blobs run-hive-eels-amsterdam \
run-hive-eels-bal-quick run-hive-build-block bench-rlp
run-hive-eels-bal-quick run-hive-build-block bench-rlp zkevm-bench-setup

help: ## 📚 Show help for each of the Makefile recipes
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
Expand Down Expand Up @@ -228,6 +228,15 @@ sort-genesis-files:
bench-rlp: ## ⚡ Bench the RLP decoder/encoder
cd ./crates/common/rlp && cargo bench

zkevm-bench-setup: ## Install ZisK v1.0.0-alpha toolchain for the zkEVM benchmark (Linux)
sudo apt-get update
sudo apt-get install -y xz-utils jq curl build-essential qemu-system libomp-dev libgmp-dev nlohmann-json3-dev protobuf-compiler uuid-dev libgrpc++-dev libsecp256k1-dev libsodium-dev libpqxx-dev nasm libopenmpi-dev openmpi-bin openmpi-common libclang-dev clang gcc-riscv64-unknown-elf
mkdir -p $(HOME)/.zisk/bin
curl -fsSL "https://raw.githubusercontent.com/0xPolygonHermez/zisk/v1.0.0-alpha/ziskup/ziskup" -o $(HOME)/.zisk/bin/ziskup
chmod +x $(HOME)/.zisk/bin/ziskup
$(HOME)/.zisk/bin/ziskup -v 1.0.0-alpha --nokey -y
@echo "Add $(HOME)/.zisk/bin to PATH (e.g. export PATH=$(HOME)/.zisk/bin:$$PATH). --nokey skips the (large) proving key — emulation doesn't need it."

# Using & so make calls this recipe only once per run
mermaid-init.js mermaid.min.js &:
@# Required for mdbook-mermaid to work
Expand Down Expand Up @@ -255,6 +264,7 @@ update-cargo-lock: ## 📦 Update Cargo.lock files
cargo tree --manifest-path crates/guest-program/bin/openvm/Cargo.toml
cargo tree --manifest-path crates/l2/tee/quote-gen/Cargo.toml
cargo tree --manifest-path crates/vm/levm/bench/revm_comparison/Cargo.toml
cargo tree --manifest-path tooling/zkevm_bench/Cargo.toml
cargo tree --manifest-path tooling/Cargo.toml
cargo tree --manifest-path tooling/ef_tests/state/Cargo.toml

Expand All @@ -277,5 +287,7 @@ check-cargo-lock: ## 🔍 Check Cargo.lock files are up to date
cargo metadata --locked --manifest-path crates/guest-program/stateless-validator/bin/openvm/Cargo.toml > /dev/null
cargo metadata --locked --manifest-path crates/l2/tee/quote-gen/Cargo.toml > /dev/null
cargo metadata --locked --manifest-path crates/vm/levm/bench/revm_comparison/Cargo.toml > /dev/null
# zkevm_bench is a standalone workspace (x86-64-only zisk dep); metadata avoids needing the toolchain
cargo metadata --locked --manifest-path tooling/zkevm_bench/Cargo.toml > /dev/null
cargo metadata --locked --manifest-path tooling/Cargo.toml > /dev/null
cargo metadata --locked --manifest-path tooling/ef_tests/state/Cargo.toml > /dev/null
6 changes: 6 additions & 0 deletions crates/prover/src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ pub mod sp1;
#[cfg(feature = "zisk")]
pub mod zisk;

#[cfg(feature = "zisk")]
pub mod zisk_profile;

#[cfg(feature = "openvm")]
pub mod openvm;

Expand All @@ -35,6 +38,9 @@ pub use sp1::Sp1Backend;
#[cfg(feature = "zisk")]
pub use zisk::ZiskBackend;

#[cfg(feature = "zisk")]
pub use zisk_profile::{ZiskAirCost, parse_air_cost};

#[cfg(feature = "openvm")]
pub use openvm::OpenVmBackend;

Expand Down
28 changes: 28 additions & 0 deletions crates/prover/src/backend/zisk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::{
use ethrex_common::types::prover::{ProofFormat, ProverOutput, ProverType};
use ethrex_guest_program::{ZKVM_ZISK_PROGRAM_ELF, input::ProgramInput};

use crate::backend::zisk_profile::{ZiskAirCost, parse_air_cost};
use crate::backend::{BackendError, ProverBackend};

const INPUT_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/zisk_input.bin");
Expand All @@ -22,6 +23,8 @@ pub struct ZiskProveOutput(pub Vec<u8>);
/// This backend drives the `cargo-zisk` CLI (ZisK v1.0.0-alpha and later) to
/// execute and prove programs: `cargo-zisk execute` for a dry run and
/// `cargo-zisk prove` / `cargo-zisk verify` for proving and verification.
/// Profiling ([`ZiskBackend::execute_profiled`]) instead uses the `ziskemu`
/// emulator with `-X`, the only tool that emits the AIR-cost breakdown.
#[derive(Default)]
pub struct ZiskBackend;

Expand Down Expand Up @@ -124,6 +127,31 @@ impl ZiskBackend {

Ok(ZiskProveOutput(proof_bytes))
}

/// Run the guest under ziskemu with profiling and return the AIR-cost
/// breakdown. No proof generated.
pub fn execute_profiled(&self, input: ProgramInput) -> Result<ZiskAirCost, BackendError> {
Self::write_elf_file()?;
self.serialize_input(&input)?;
const PROFILE_FLAGS: &[&str] = &["-X"];
let mut args: Vec<&str> = vec!["--elf", ELF_PATH, "--inputs", INPUT_PATH];
args.extend_from_slice(PROFILE_FLAGS);
let output = Command::new("ziskemu")
.args(&args)
.stdin(Stdio::inherit())
.output()
.map_err(BackendError::execution)?;
if !output.status.success() {
return Err(BackendError::execution(format!(
"ziskemu profiled execution failed: {}",
String::from_utf8_lossy(&output.stderr)
)));
}
let mut combined = String::from_utf8_lossy(&output.stdout).into_owned();
combined.push('\n');
combined.push_str(&String::from_utf8_lossy(&output.stderr));
parse_air_cost(&combined)
}
}

impl ProverBackend for ZiskBackend {
Expand Down
121 changes: 121 additions & 0 deletions crates/prover/src/backend/zisk_profile.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use crate::backend::BackendError;

/// AIR-cost breakdown parsed from `ziskemu`'s `COST DISTRIBUTION` report block.
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ZiskAirCost {
pub base: u64,
pub main: u64,
pub opcodes: u64,
pub precompiles: u64,
pub memory: u64,
pub total: u64,
pub steps: u64,
/// Guest's peak zkVM memory footprint in bytes, from ziskemu's `RAM
/// USAGE` line. This is a footprint measurement, not a proving-cost
/// component (unlike `memory`, the AIR cost of memory opcodes).
pub ram_usage: u64,
}

/// Parses a comma-grouped integer token (e.g. `"4,930,197,804"`) into a `u64`.
fn parse_cost_token(token: &str) -> Option<u64> {
token.replace(',', "").parse::<u64>().ok()
}

/// Parses the `COST DISTRIBUTION` summary block out of raw `ziskemu` stdout.
///
/// The exact first whitespace-separated token of each line is matched
/// against the summary labels. The value is then taken as the *first*
/// remaining whitespace token on that line that parses as a `u64` once `,`
/// grouping separators are stripped. A single extraction rule this way
/// serves both single-word labels (`BASE 293,601,280 5.96%`, where the
/// trailing percentage's `.` makes it fail to parse and get skipped) and the
/// two-word `RAM USAGE` label (where `USAGE` fails to parse and gets
/// skipped, leaving `7,304,122` as the value). This also keeps the parser
/// immune to the detailed per-opcode tables that follow the summary in the
/// real output (e.g. lines starting with `OP`, `COST BY OPCODE`, or
/// `FROPS`), since their first token never matches a summary label. It is
/// likewise order-independent, so the `VARIABLE` line (`= TOTAL - BASE`) that
/// ZisK v1.0.0-alpha added to the summary is simply ignored.
pub fn parse_air_cost(stdout: &str) -> Result<ZiskAirCost, BackendError> {
let mut air_cost = ZiskAirCost::default();
let mut found_component = false;

for line in stdout.lines() {
let mut tokens = line.split_whitespace();
let Some(label) = tokens.next() else {
continue;
};
let Some(value) = tokens.filter_map(parse_cost_token).next() else {
continue;
};

match label {
"STEPS" => air_cost.steps = value,
"BASE" => {
air_cost.base = value;
found_component = true;
}
"MAIN" => {
air_cost.main = value;
found_component = true;
}
"OPCODES" => {
air_cost.opcodes = value;
found_component = true;
}
"PRECOMPILES" => {
air_cost.precompiles = value;
found_component = true;
}
"MEMORY" => {
air_cost.memory = value;
found_component = true;
}
"TOTAL" => air_cost.total = value,
"RAM" => air_cost.ram_usage = value,
_ => {}
}
}

if !found_component {
return Err(BackendError::execution(
"ziskemu output contained no COST DISTRIBUTION block",
));
}

Ok(air_cost)
}

#[cfg(test)]
mod tests {
use super::*;

// A committed real ZisK v1.0.0-alpha `ziskemu -X` capture of the
// `mainnet_25087668_light` benchmark block. Path is relative to this crate.
const SAMPLE: &str =
include_str!("../../../../tooling/zkevm_bench/fixtures/ziskemu_sample.txt");

#[test]
fn parses_air_cost_from_sample() {
let ac = parse_air_cost(SAMPLE).unwrap();
assert_eq!(ac.steps, 8_573_814);
assert_eq!(ac.base, 293_601_280);
assert_eq!(ac.main, 583_019_352);
assert_eq!(ac.opcodes, 124_468_967);
assert_eq!(ac.precompiles, 227_527_215);
assert_eq!(ac.memory, 80_638_696);
assert_eq!(ac.total, 1_309_255_510);
assert_eq!(ac.ram_usage, 3_697_848);
// invariant: components sum to total (still holds in v1.0.0-alpha; the
// added `VARIABLE` line — total minus base — is ignored by the parser).
assert_eq!(
ac.total,
ac.base + ac.main + ac.opcodes + ac.precompiles + ac.memory
);
}

#[test]
fn errors_on_missing_cost_block() {
assert!(parse_air_cost("no distribution here\nOP add 1 2%\n").is_err());
}
}
74 changes: 74 additions & 0 deletions tooling/ef_tests/blockchain/test_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use ethrex_blockchain::{
};
#[cfg(not(feature = "stateless"))]
use ethrex_common::types::block_access_list::BlockAccessList;
use ethrex_common::types::block_execution_witness::ExecutionWitness;
#[cfg(feature = "stateless")]
use ethrex_common::types::block_execution_witness::RpcExecutionWitness;
use ethrex_common::{
Expand Down Expand Up @@ -475,6 +476,79 @@ pub async fn build_store_for_test(test: &TestUnit) -> Store {
store
}

/// Builds the blocks and a self-generated execution witness for a blockchain
/// test, using ethrex's own execution machinery instead of any witness the
/// fixture might carry. This is the ethrex-native counterpart to the
/// external eth-act `witness-generator-cli` path: it turns a plain EEST
/// `blockchain_test` (genesis/`pre` state + blocks, no embedded witness)
/// into a `(blocks, ExecutionWitness)` pair usable for stateless-execution
/// benchmarking.
///
/// Every block is first driven through `add_block_pipeline` (mirroring
/// `run()`), so that for multi-block tests the parent state of block
/// `i > 0` is already committed to the store before
/// `generate_witness_for_blocks` re-executes every block with a
/// `TrieLogger`. `generate_witness_for_blocks` only has the state at the
/// *first* block's parent available for free (from `build_store_for_test`);
/// its own doc comment notes it otherwise fails on the second block of a
/// batch.
///
/// A test whose blocks include an expected exception (invalid-block test) or
/// that decodes to zero blocks is rejected with an `Err` rather than
/// silently producing a partial/incorrect fixture, so batch callers (e.g.
/// `generate-stress`) can skip it and continue.
pub async fn blocks_and_witness_for_test(
test: &TestUnit,
) -> Result<(Vec<CoreBlock>, ExecutionWitness), String> {
let store = build_store_for_test(test).await;
let blockchain = Blockchain::default_with_store_and_pool(store.clone(), merkle_pool());

let mut blocks: Vec<CoreBlock> = Vec::with_capacity(test.blocks.len());
for block_fixture in test.blocks.iter() {
if block_fixture.expect_exception.is_some() {
return Err(
"test has a block expecting an exception; not a witness-generation target"
.to_string(),
);
}
let block_data = block_fixture
.block()
.ok_or_else(|| "block fixture has no decodable block (RLP-only test)".to_string())?;
let block: CoreBlock = block_data.clone().into();
let hash = block.hash();

blockchain
.add_block_pipeline(block.clone(), None)
.map_err(|e| {
format!(
"add_block_pipeline failed for block {}: {e:?}",
block.header.number
)
})?;
apply_fork_choice(&store, hash, hash, hash, None)
.await
.map_err(|e| {
format!(
"apply_fork_choice failed for block {}: {e:?}",
block.header.number
)
})?;

blocks.push(block);
}

if blocks.is_empty() {
return Err("no blocks".to_string());
}

let witness = blockchain
.generate_witness_for_blocks(&blocks)
.await
.map_err(|e| format!("witness gen: {e}"))?;

Ok((blocks, witness))
}

/// Checks db is correct after setting up initial state
/// Panics if any comparison fails
fn check_prestate_against_db(test_key: &str, test: &TestUnit, db: &Store) {
Expand Down
Loading
Loading