diff --git a/.github/workflows/benchmark-gpu.yml b/.github/workflows/benchmark-gpu.yml
index 040b51c89..11fcbf222 100644
--- a/.github/workflows/benchmark-gpu.yml
+++ b/.github/workflows/benchmark-gpu.yml
@@ -6,10 +6,11 @@ name: Benchmark GPU (PR)
# It builds the cli at the PR head and at main, runs N interleaved pairs on the GPU,
# posts the paired-t + Wilcoxon verdict back to the PR, then ALWAYS destroys the box.
#
-# Triggered by a "/bench-gpu [N] [cont[TX]|mono[TX]]" comment on a PR (N = pair count,
-# default 14) or via workflow_dispatch. Workload default: ethrex 100tx --continuations;
-# "mono[TX]" = legacy monolithic prove. Cont proofs need rkyv pointer_width_64 on both
-# sides, so PR branches older than that fix must rebase before a cont bench.
+# Triggered by a "/bench-gpu [N]" comment on a PR (N = pair count, default 14) or via
+# workflow_dispatch.
+#
+# Workload: the real block (see tooling/ethrex-block-converter/README.md), proven with
+# --continuations at the calibrated epoch size below.
# Orchestration runs on a GitHub-hosted runner; all GPU work happens on the rented
# Vast box (provisioned by the template onstart).
#
@@ -23,9 +24,6 @@ on:
pairs:
description: "Number of A/B/B/A pairs"
default: "14"
- mode:
- description: "Workload: cont[TX] (--continuations, TX defaults to 100) or mono[TX] (monolithic, TX defaults to 5)"
- default: "cont100"
issue_comment:
types: [created]
@@ -47,6 +45,25 @@ env:
VAST_IMAGE_DISK: "64"
# cli features for the ABBA build — the GPU (cuda) prover path plus jemalloc heap stats.
BENCH_FEATURES: "jemalloc-stats,prover/cuda"
+ # Continuation epoch for the REAL-BLOCK path, from the RTX 5090 calibration on
+ # 2026-07-31 against main @9ccdaf2 (raw traces:
+ # ~/workspace/lambda_vm_bench_cache/gpu_epoch_calib_2026-07-31/, PROVENANCE.txt).
+ # Measured on the 32,607 MiB card, same fixture and CLI, one prove per setting:
+ #
+ # 2^21 70.52 s wall 19,193 MiB VRAM (58.9%) 25 epochs 1.65 GB proof
+ # 2^22 59.87 s wall 23,193 MiB VRAM (71.1%) 13 epochs 1.12 GB proof
+ # 2^23 OOM at 32,079 MiB (98.4%) after 9.7 s — needs ~44 GiB
+ #
+ # So 2^22 is the largest setting that fits a 32 GiB card, and it is ~15% faster than
+ # 2^21 (equivalently, 2^21 is ~18% slower) with 28.9% VRAM headroom left. 2^23 is out
+ # of reach for every card below 48 GiB, not just this one.
+ #
+ # GPU PATH ONLY, and deliberately so. It is NOT pushed into bench_abba.sh's default
+ # (2^20, which the CPU /bench-abba uses) nor into the CLI's
+ # DEFAULT_CONTINUATION_EPOCH_SIZE_LOG2 (also 20): 2^22 needs ~28 GiB of HOST memory on
+ # a CPU build, which would break laptops. VRAM is the binding constraint here and host
+ # RAM is the binding constraint there, so the two defaults are not the same question.
+ GPU_REAL_EPOCH_LOG2: "22"
# Unique per-run label set on the instance, for easy identification in the Vast console.
RUN_LABEL: "gpu-bench-${{ github.run_id }}-${{ github.run_attempt }}"
# Pin the Vast CLI to an immutable commit (a PyPI version can be re-published; a commit
@@ -63,8 +80,8 @@ jobs:
github.event.issue.pull_request &&
startsWith(github.event.comment.body, '/bench-gpu') &&
contains(fromJSON('["MEMBER","OWNER","COLLABORATOR"]'), github.event.comment.author_association))
- # Provisioning + dual cuda build (~30 min) + 2*pairs proves (~3.5 min each at
- # the default cont100). Sized for the 32-pair worst case (~4.5 hr) with headroom;
+ # Provisioning + dual cuda build + 2*pairs real-block proves (~2 min each,
+ # host-CPU-dependent). Sized for the 32-pair worst case with generous headroom;
# teardown still always destroys the box.
timeout-minutes: 330
steps:
@@ -76,47 +93,30 @@ jobs:
COMMENT_BODY: ${{ github.event.comment.body }}
PR_NUM: ${{ github.event.issue.number }}
DISPATCH_PAIRS: ${{ github.event.inputs.pairs }}
- DISPATCH_MODE: ${{ github.event.inputs.mode }}
DISPATCH_REF: ${{ github.ref_name }}
run: |
if [ "$EVENT_NAME" = "issue_comment" ]; then
# Pin the head SHA (works for fork PRs; avoids a force-push race mid-run).
HEAD_SHA=$(gh pr view "$PR_NUM" --repo "$GITHUB_REPOSITORY" --json headRefOid -q .headRefOid)
OUT_PR_NUM="$PR_NUM"; OUT_HEAD_SHA="$HEAD_SHA"; OUT_BRANCH=""
- # Everything after "/bench-gpu" on its line, tokens in any order:
- # a number = pair count; cont[TX]/mono[TX] = workload (see loop below).
+ # Everything after "/bench-gpu" on its line: the only token is an
+ # optional pair count.
ARGS=$(printf '%s' "$COMMENT_BODY" | tr -d '\r' | sed -n 's|^/bench-gpu||p' | head -n1)
PAIRS=14
else
# workflow_dispatch: compare this branch vs main.
OUT_PR_NUM=""; OUT_HEAD_SHA=""; OUT_BRANCH="$DISPATCH_REF"
- ARGS="$DISPATCH_MODE"
+ ARGS=""
PAIRS=${DISPATCH_PAIRS:-14}
fi
- # Defaults: continuations with the 100-transfer fixture (production mode).
- CONTINUATIONS=1; TX_COUNT=100
set -f # tokens must not glob-expand against the runner's CWD
for tok in $ARGS; do
case "$tok" in
- cont) CONTINUATIONS=1; TX_COUNT=100 ;;
- mono) CONTINUATIONS=0; TX_COUNT=5 ;;
- cont[0-9]*) CONTINUATIONS=1; TX_COUNT="${tok#cont}" ;;
- mono[0-9]*) CONTINUATIONS=0; TX_COUNT="${tok#mono}" ;;
[0-9]*) PAIRS="$tok" ;;
*) echo "::warning::ignoring unrecognized token '$tok'" ;;
esac
done
- # TX_COUNT and PAIRS are interpolated into the remote bash -lc below: enforce
- # digits-only. Mono is capped at 5tx (monolithic peak heap grows with the
- # trace; 20tx needs ~78 GB, over the 48 GB box floor).
- if [ "$CONTINUATIONS" = "1" ]; then TX_DEFAULT=100; TX_MAX=100; else TX_DEFAULT=5; TX_MAX=5; fi
- case "$TX_COUNT" in
- ''|*[!0-9]*) echo "::warning::invalid tx count '$TX_COUNT'; using $TX_DEFAULT"; TX_COUNT=$TX_DEFAULT ;;
- esac
- if [ "$TX_COUNT" -lt 1 ] || [ "$TX_COUNT" -gt "$TX_MAX" ]; then
- echo "::warning::tx count $TX_COUNT out of range [1,$TX_MAX] for this mode; using $TX_DEFAULT"
- TX_COUNT=$TX_DEFAULT
- fi
+ # PAIRS is interpolated into the remote bash -lc below: enforce digits-only.
case "$PAIRS" in
''|*[!0-9]*) echo "::warning::invalid pair count '$PAIRS'; using 14"; PAIRS=14 ;;
esac
@@ -132,39 +132,14 @@ jobs:
PAIRS=$((PAIRS + 1))
echo "::notice::rounded odd pair count up to $PAIRS so AB/BA orders balance"
fi
- if [ "$CONTINUATIONS" = "1" ]; then
- WORKLOAD="ethrex ${TX_COUNT}tx continuations"
- else
- WORKLOAD="ethrex ${TX_COUNT}tx monolithic"
- fi
- # Outputs land before the fail-fast below so the always() result
- # comment is fully labeled even when this step exits early.
+ WORKLOAD="ethrex real block, continuations"
{
echo "pr_num=$OUT_PR_NUM"
echo "head_sha=$OUT_HEAD_SHA"
echo "branch=$OUT_BRANCH"
echo "pairs=$PAIRS"
- echo "continuations=$CONTINUATIONS"
- echo "tx_count=$TX_COUNT"
echo "workload=$WORKLOAD"
} >> "$GITHUB_OUTPUT"
- # Fail fast if the PR side predates the pointer_width_64 fix: a >=40tx
- # continuation proof exceeds rkyv's old 2 GiB cap and only dies after the
- # full dual build (~1 hr of GPU rental). Skip the check when the fetch
- # fails: gh api prints HTTP error bodies to stdout, so gate on its exit
- # status AND on the payload looking like the manifest (it declares
- # rkyv) — never treat an error blob as a missing feature. Purely
- # textual; deletable once every open branch postdates the fix.
- if [ "$CONTINUATIONS" = "1" ] && [ "$TX_COUNT" -ge 40 ]; then
- REF="${OUT_HEAD_SHA:-$OUT_BRANCH}"
- if SIDE=$(gh api "repos/$GITHUB_REPOSITORY/contents/prover/Cargo.toml?ref=$REF" \
- -H "Accept: application/vnd.github.raw" 2>/dev/null) \
- && printf '%s' "$SIDE" | grep -q '^rkyv' \
- && ! printf '%s' "$SIDE" | grep -q pointer_width_64; then
- echo "::error::PR branch predates the rkyv pointer_width_64 fix — a ${TX_COUNT}tx continuation proof cannot serialize. Rebase onto main, or bench with cont10/mono."
- exit 1
- fi
- fi
echo "Using $PAIRS A/B/B/A pairs on $WORKLOAD"
- name: Acknowledge (react + occupancy notice)
@@ -182,7 +157,12 @@ jobs:
// Post the "started" notice under the SAME marker the result step uses, so the
// result updates this comment in place (and re-runs reuse it rather than stacking).
const marker = 'GPU Benchmark (ABBA)';
- const body = `## GPU Benchmark (ABBA) — running…\n\n⏳ Renting an RTX 5090 on Vast.ai and running ${process.env.PAIRS} interleaved pairs (PR vs main) of ${process.env.WORKLOAD} on the CUDA prover path. This takes ~2.5 hr at the default workload; the result will replace this comment.`;
+ // Reference: 4 pairs measured ~20 min end-to-end (rental + dual build +
+ // 8 proves). Per-prove wall varies with the rented host's CPU (the prover
+ // is partly host-CPU-bound), so scale expectations from pair count, not
+ // from a fixed per-run figure.
+ const mins = 12 + Number(process.env.PAIRS) * 2;
+ const body = `## GPU Benchmark (ABBA) — running…\n\n⏳ Renting an RTX 5090 on Vast.ai and running ${process.env.PAIRS} interleaved pairs (PR vs main) of ${process.env.WORKLOAD} on the CUDA prover path. Rough ETA ~${mins} min. The result will replace this comment.`;
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner, repo: context.repo.repo,
issue_number: context.issue.number, per_page: 100,
@@ -234,10 +214,11 @@ jobs:
# because vast can't numerically compare the driver_version string server-side.
MIN_DRIVER: "580"
run: |
- # cpu_ram filter is in GB. Floor 48 GB: continuation proves are flat-memory
- # (~10 GB) and the legacy 5tx monolithic prove also fits — far below the old
- # 20-transfer prove (~78 GB heap) that set the previous 96 GB floor. 48 GB
- # widens the dedicated pool (~15 vs ~11 offers).
+ # cpu_ram filter is in GB. Floor 48 GB: the real block at epoch 2^22 peaks at
+ # ~36 GB host RSS on the CUDA path (measured, main vintage) — ~25% headroom.
+ # Continuation peak is set by the epoch size, not the block, so bigger blocks
+ # don't move it; raising the epoch would (see the calibration tables in
+ # tooling/ethrex-block-converter/README.md).
# gpu_frac=1 requires a WHOLE-MACHINE offer (you rent every GPU on the host), so
# Vast places no other tenant on the box: CPU cores, RAM/memory bandwidth, PCIe,
# and NVMe are fully dedicated. Without it the "most expensive" sort below lands on
@@ -364,6 +345,7 @@ jobs:
KEY: ${{ steps.sshkey.outputs.key_path }}
run: |
SSH="ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 -o BatchMode=yes -i $KEY -p $PORT root@$HOST"
+
echo "Waiting for the template onstart script to finish (Rust + LLVM + sysroot + clone)..."
# The bootstrap's final stdout line is "=== done ===". Vast captures onstart
# output to /var/log/onstart.log; fall back to checking the artifacts it leaves.
@@ -395,8 +377,6 @@ jobs:
HEAD_SHA: ${{ steps.config.outputs.head_sha }}
BRANCH: ${{ steps.config.outputs.branch }}
PAIRS: ${{ steps.config.outputs.pairs }}
- CONTINUATIONS: ${{ steps.config.outputs.continuations }}
- TX_COUNT: ${{ steps.config.outputs.tx_count }}
run: |
SSH="ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 -o BatchMode=yes -i $KEY -p $PORT root@$HOST"
@@ -421,7 +401,9 @@ jobs:
# explicit and robust to the template default changing.) The harness still builds the
# cli at REF_A (the PR) and origin/main in isolated worktrees, runs PAIRS interleaved
# A/B/B/A proves, and prints the paired-t CI + Wilcoxon verdict. BENCH_FEATURES routes
- # the build through the CUDA prover path; CONTINUATIONS/TX_COUNT pick the workload.
+ # the build through the CUDA prover path. bench_abba.sh is shared with the CPU ABBA
+ # flow and has its own defaults, so its knobs are pinned explicitly here: the real
+ # block, continuations, the calibrated epoch.
# The harness runs from main, so workflow/script changes take effect post-merge.
# REBUILD=1: each Vast box is fresh, GPU-specific hardware — always rebuild both
# binaries (cubin is compiled for the detected arch); never trust a cached binary.
@@ -437,7 +419,7 @@ jobs:
git fetch --force origin main; $FETCH; \
git checkout -f origin/main; \
REBUILD=1 CUDARC_PIN=cuda-12080 SYSROOT_DIR=/opt/lambda-vm-sysroot BENCH_FEATURES='$BENCH_FEATURES' \
- CONTINUATIONS=$CONTINUATIONS TX_COUNT=$TX_COUNT \
+ WORKLOAD=real CONTINUATIONS=1 EPOCH_SIZE_LOG2=$GPU_REAL_EPOCH_LOG2 \
scripts/bench_abba.sh $REF_A origin/main $PAIRS"
# pipefail so a failed remote bench (e.g. a prove that dies) propagates through the
diff --git a/.github/workflows/benchmark-pr.yml b/.github/workflows/benchmark-pr.yml
index fd74c7b3f..f9c83ea3b 100644
--- a/.github/workflows/benchmark-pr.yml
+++ b/.github/workflows/benchmark-pr.yml
@@ -12,6 +12,13 @@ on:
- 'executor/**'
- 'bin/cli/**'
- 'tooling/ethrex-fixtures/**'
+ # A baseline is only valid for the workload it measured, and these two
+ # define the real-block workload: the converter decides the fixture's
+ # bytes, the Makefile decides which block it is. Without them a repointed
+ # block would leave main's baseline stale until some prover file happened
+ # to change — and the comparison guard would suppress the table until then.
+ - 'tooling/ethrex-block-converter/**'
+ - 'Makefile'
# Uncomment to auto-run on PRs:
# pull_request:
# branches: [main]
@@ -32,18 +39,80 @@ concurrency:
cancel-in-progress: false
env:
- # Headline program: the ethrex guest ELF proven against a 20-transfer block
- # (distinct sender -> distinct recipient per tx). One ELF; the workload is the
- # private input (rkyv ProgramInput), generated in-job and gitignored (see the
- # "Generate ethrex bench fixtures" step).
+ # `/bench` proves ONE workload: a real Ethereum block. The synthetic N-transfer
+ # screen that used to run alongside it was removed deliberately — if you are about
+ # to add it back, these are the two reasons it went:
+ #
+ # 1. Its only unique coverage was the MONOLITHIC prove path, which is vestigial
+ # (reportedly slower than a 1-epoch continuation). Coverage of a path we intend
+ # to delete is not a reason to spend runner time.
+ # 2. Its crypto mix is one no real block has: 8.83 ECSM per Mcycle against a real
+ # block's ~1.3-1.6. Screening against it tunes the prover for a worst case that
+ # cannot occur.
+ #
+ # The synthetic fixtures themselves are NOT gone — /bench-growth still sweeps them to
+ # get a heap-vs-block-size slope, which needs a family of blocks and so cannot come
+ # from one real one. Only the headline screen was dropped.
+ #
+ # Cycle counts below are for the ELF THIS JOB BUILDS (see "Build ethrex guest ELF"),
+ # as of main @ 9ccdaf2 with clang 21. They move with guest optimisation (#861 gave the
+ # guest thin LTO) and ~2% with the clang major on PATH, so a count quoted against a
+ # different ELF reads as a regression — always pin the ELF when repeating one.
ELF: executor/program_artifacts/rust/ethrex.elf
- INPUT: executor/tests/ethrex_bench_20.bin
- BENCH_RUNS_PR: 3
- # Cheap-tier screen: catches regressions down to ~1.5% on its own and leaves
- # smaller/ambiguous deltas to the manual drift-free ABBA tiebreaker. Pushing either
- # side past 5 buys little here (the cached comparison can't beat the ~1% drift wall),
- # so the per-PR run count is also capped at 5 (clamp below).
- BENCH_RUNS_BASELINE: 5
+ # The workload: a real Ethereum block. WHICH block lives in
+ # the Makefile and nowhere else — nothing in this file names one, so a repoint
+ # moves this job without editing it. At the current default that is 50.78M cycles,
+ # 10,478 keccak calls and 116 ecsm calls: ~5.8x the work of the synthetic block with a
+ # ~18x different keccak:ecrecover mix. That is the whole point — a prover change
+ # can move the synthetic number and the real one in opposite directions.
+ #
+ # The path is resolved from the Makefile (`make -s print-real-block-fixture`) into
+ # REAL_INPUT at run time. The ~1 MB .bin is gitignored and FETCHED by URL + sha256
+ # (see "Fetch ethrex real-block fixture"); while that URL is unset the whole
+ # section degrades to a warning rather than failing the job.
+ #
+ # Continuations are mandatory here, not a preference: a monolithic prove costs
+ # ~4.9 GB of peak heap per million cycles on this workload family (from the
+ # measured growth fit, 10,728 MB + 2,007 MB/transfer at R^2 = 0.998), so the
+ # current default would need ~240 GB and a heavier candidate far more.
+ # `--continuations` makes peak heap a function of the epoch size instead of the
+ # trace length. Costs move with the block; the per-candidate table is in
+ # tooling/ethrex-block-converter/README.md.
+ #
+ # Budget ~1.2 GB of disk for the bundle each run — hence the `rm -f` after every
+ # prove. A heavier block pushes it past 2 GiB, which needs rkyv `pointer_width_64`;
+ # a PR branch predating that fix fails at write time rather than mismeasuring.
+ #
+ # Epoch 2^22, from the CPU sweep on 2026-07-31 (124 GiB / 32-core box, real block,
+ # branch vintage; full table in tooling/ethrex-block-converter/README.md):
+ #
+ # 2^21 464.26 s 18.43 GiB RSS 26 epochs 1.72 GB proof
+ # 2^22 397.88 s 32.21 GiB RSS 13 epochs 1.15 GB proof
+ # 2^23 356.47 s 60.01 GiB RSS 7 epochs 0.90 GB proof
+ #
+ # This runner's floor is >=64 GiB, so 2^22's 32.2 GiB fits with ~50% headroom while
+ # 2^23's 60 GiB does not fit safely — memory, not speed, is what picks 2^22 here.
+ # Moving off 2^21 is worth ~14% wall on the calibration box. That box is NOT this
+ # runner (it measured 8.6 s/Mcycle against this runner's 5.3-5.6), so treat the
+ # ratio as transferable and the absolute seconds as not: the first /bench run at
+ # this epoch establishes this runner's actual time.
+ #
+ # Deliberately NOT the CLI's DEFAULT_CONTINUATION_EPOCH_SIZE_LOG2, which stays 20 so
+ # a laptop can still prove; and not the GPU path's 2^22, which happens to coincide
+ # but is chosen by VRAM rather than host RAM (see benchmark-gpu.yml).
+ REAL_BLOCK_EPOCH_LOG2: "22"
+ # Sampled like the synthetic screen, 3 runs, and reported the same way (median +
+ # spread). Fewer than the synthetic's 5 because each run is minutes rather than
+ # seconds; enough that one slow run shows up as spread instead of moving the median.
+ #
+ # THIS IS THE DIAL. The runner's per-run real-block time is still unmeasured — the
+ # calibration ran on a different box (see REAL_BLOCK_EPOCH_LOG2 above). If the first
+ # runs land above ~6 min each, /bench becomes a ~25 min occupancy of a runner every
+ # other bench queues behind, and this count is what to turn down (2, or 1) before
+ # reaching for anything else.
+ # `/bench N` overrides this, clamped to [1,5]. Past 5 the cached comparison can't
+ # beat the ~1% session-drift wall anyway — that is what /bench-abba is for.
+ BENCH_RUNS_REAL: 3
# Memory-scaling sweep: same ELF, different N-transfer inputs. GROWTH_PROGRAMS
# are the generated (gitignored) fixture basenames in executor/tests/; GROWTH_STEPS
# the matching transfer counts (x-axis; slope is MB per transfer).
@@ -53,7 +122,11 @@ env:
jobs:
benchmark:
runs-on: [self-hosted, bench]
- # Skip unless: push to main, workflow_dispatch, or "/bench" comment on a PR
+ # Skip unless: push to main, workflow_dispatch, or "/bench" comment on a PR.
+ # "/bench-growth" is handled by THIS job (it is prefixed by "/bench" and
+ # deliberately absent from the exclusion list below); it only switches whether the
+ # growth sweep runs, in the "Determine run count" step. The real block needs no
+ # token — it runs on every invocation.
if: >-
github.event_name == 'push' ||
github.event_name == 'workflow_dispatch' ||
@@ -135,20 +208,57 @@ jobs:
echo "run_growth=false" >> "$GITHUB_OUTPUT"
fi
+ # The real block runs on EVERY invocation — plain `/bench`, push to main, and
+ # workflow_dispatch alike. It is the number that means something, so it should
+ # not need a second command to ask for; the synthetic screen rides along
+ # unchanged as the cheap, high-sensitivity half.
+ #
+ # The cost is real and lands on a shared runner: ~4.5-4.8 min per run x
+ # BENCH_RUNS_REAL, so every /bench and every push to main now occupies the
+ # bench server for roughly 15-20 min, and /bench-abba and /bench-verify queue
+ # behind it. That trade was made deliberately — see BENCH_RUNS_REAL above for
+ # the dial if it proves too expensive.
+ RUN_REAL=true
+
+ # The fixture is FETCHED from a pinned URL + sha256, not built (see the
+ # Makefile). The guard covers the window after a repoint but before the new
+ # artifact is uploaded: degrade to the synthetic sections with a warning
+ # rather than failing, so a push to main cannot go red over an upload nobody
+ # in CI can perform.
+ if [ "$RUN_REAL" = "true" ] && [ -z "$(make -s print-real-block-fixture-url)" ]; then
+ echo "::warning::Real-block benchmark skipped: ETHREX_REAL_BLOCK_FIXTURE_URL is unset in the Makefile."
+ RUN_REAL=false
+ fi
+ echo "run_real=$RUN_REAL" >> "$GITHUB_OUTPUT"
+
+ # Resolve the real-block fixture path from its single source of truth (the
+ # Makefile) and pin it in the job env. Captured HERE, before the baseline
+ # step's `git checkout origin/main`, so the PR and main sides agree on the
+ # workload even when main's Makefile names a different block.
+ echo "REAL_INPUT=$(make -s print-real-block-fixture)" >> "$GITHUB_ENV"
+
+ # Every metrics writer appends; the step that used to create this file was the
+ # synthetic screen, which is gone. Truncate here (this step always runs) so a
+ # previous run's values can't survive on the persistent self-hosted runner.
+ : > /tmp/metrics.txt
+
+ # `/bench N` sets the real-block sample count. Both sides use the same count
+ # (push-to-main publishes the baseline), so there is no PR-vs-baseline split
+ # any more — an asymmetric count would put the two sides' noise on different
+ # footings, which is what sampling is for.
+ RUNS=$BENCH_RUNS_REAL
if [ "$EVENT_NAME" = "issue_comment" ]; then
CUSTOM_N=$(echo "$COMMENT_BODY" | sed -n 's|^/bench[[:space:]]*\([0-9]\+\).*|\1|p')
- RUNS=${CUSTOM_N:-$BENCH_RUNS_PR}
- elif [ "$EVENT_NAME" = "push" ] || [ "$EVENT_NAME" = "workflow_dispatch" ]; then
- RUNS=$BENCH_RUNS_BASELINE
- else
- RUNS=$BENCH_RUNS_PR
+ RUNS=${CUSTOM_N:-$BENCH_RUNS_REAL}
fi
# Clamp to 1-5. Beyond 5 the single-session cached comparison barely improves
# (it can't beat the ~1% drift wall); use the ABBA tiebreaker for finer deltas.
+ # At ~4-5 min a run this is also the difference between a 15 min and a 25 min
+ # occupancy of the one bench runner.
if [ "$RUNS" -lt 1 ] 2>/dev/null || [ "$RUNS" -gt 5 ] 2>/dev/null; then
- echo "::warning::Run count $RUNS out of range [1,5], defaulting to $BENCH_RUNS_PR"
- RUNS=$BENCH_RUNS_PR
+ echo "::warning::Run count $RUNS out of range [1,5], defaulting to $BENCH_RUNS_REAL"
+ RUNS=$BENCH_RUNS_REAL
fi
echo "runs=$RUNS" >> "$GITHUB_OUTPUT"
@@ -169,74 +279,19 @@ jobs:
echo "Using $RUNS iterations, TABLE_PARALLELISM=default"
fi
- - name: Benchmark PR
- id: pr
- env:
- RUNS: ${{ steps.config.outputs.runs }}
- TABLE_PARALLELISM: ${{ steps.config.outputs.table_parallelism }}
+ - name: Fetch ethrex real-block fixture
+ if: steps.config.outputs.run_real == 'true'
run: |
- if [ -n "$TABLE_PARALLELISM" ]; then
- export TABLE_PARALLELISM
- echo "TABLE_PARALLELISM=$TABLE_PARALLELISM"
- fi
- TIMES=""
- HEAPS=""
- for i in $(seq 1 $RUNS); do
- echo "--- Run $i/$RUNS ---"
- ./target/release/cli prove "$ELF" --private-input "$INPUT" -o /tmp/proof.bin --time \
- | tee /tmp/cli_output_$i.txt
- rm -f /tmp/proof.bin
-
- T=$(grep -o 'Proving time: [0-9.]*' /tmp/cli_output_$i.txt | awk '{print $3}')
- H=$(grep -o 'Peak heap: [0-9]*' /tmp/cli_output_$i.txt | awk '{print $3}')
-
- if [ -z "$T" ] || [ -z "$H" ]; then
- echo "::error::Failed to parse metrics from run $i"
- cat /tmp/cli_output_$i.txt
- exit 1
- fi
-
- TIMES="$TIMES $T"
- HEAPS="$HEAPS $H"
- done
-
- # Median position (works for odd N; for even N picks lower-middle)
- MEDIAN_POS=$(( (RUNS + 1) / 2 ))
- TIME_MEDIAN=$(echo $TIMES | tr ' ' '\n' | sort -n | awk "NR==$MEDIAN_POS")
- HEAP_MEDIAN=$(echo $HEAPS | tr ' ' '\n' | sort -n | awk "NR==$MEDIAN_POS")
-
- if [ -z "$HEAP_MEDIAN" ] || [ -z "$TIME_MEDIAN" ]; then
- echo "::error::Failed to compute median metrics"
- exit 1
- fi
-
- # Spread: (max - min) / median * 100
- TIME_MIN=$(echo $TIMES | tr ' ' '\n' | sort -n | head -1)
- TIME_MAX=$(echo $TIMES | tr ' ' '\n' | sort -n | tail -1)
- TIME_SPREAD=$(awk "BEGIN { if ($TIME_MEDIAN > 0) printf \"%.1f\", (($TIME_MAX - $TIME_MIN) / $TIME_MEDIAN) * 100; else print \"0.0\" }")
-
- HEAP_MIN=$(echo $HEAPS | tr ' ' '\n' | sort -n | head -1)
- HEAP_MAX=$(echo $HEAPS | tr ' ' '\n' | sort -n | tail -1)
- HEAP_SPREAD=$(awk "BEGIN { if ($HEAP_MEDIAN > 0) printf \"%.1f\", (($HEAP_MAX - $HEAP_MIN) / $HEAP_MEDIAN) * 100; else print \"0.0\" }")
-
- ALL_TIMES=$(echo $TIMES | tr ' ' '\n' | paste -sd '/' -)
- ALL_HEAPS=$(echo $HEAPS | tr ' ' '\n' | paste -sd '/' -)
-
- echo "peak_mb=$HEAP_MEDIAN" >> "$GITHUB_OUTPUT"
- echo "time_s=$TIME_MEDIAN" >> "$GITHUB_OUTPUT"
- echo "time_spread=$TIME_SPREAD" >> "$GITHUB_OUTPUT"
- echo "heap_spread=$HEAP_SPREAD" >> "$GITHUB_OUTPUT"
- echo "all_times=$ALL_TIMES" >> "$GITHUB_OUTPUT"
- echo "all_heaps=$ALL_HEAPS" >> "$GITHUB_OUTPUT"
- echo "runs=$RUNS" >> "$GITHUB_OUTPUT"
-
- echo "peak_mb=$HEAP_MEDIAN" > /tmp/metrics.txt
- echo "time_s=$TIME_MEDIAN" >> /tmp/metrics.txt
- echo "time_spread=$TIME_SPREAD" >> /tmp/metrics.txt
- echo "heap_spread=$HEAP_SPREAD" >> /tmp/metrics.txt
- echo "all_times=$ALL_TIMES" >> /tmp/metrics.txt
- echo "all_heaps=$ALL_HEAPS" >> /tmp/metrics.txt
- echo "runs=$RUNS" >> /tmp/metrics.txt
+ # ~1 MB, gitignored, and never in the checkout: fetch it rather than failing
+ # on a missing file, the same way the synthetic fixtures are generated above.
+ # This is a URL + sha256 download, not a build — no converter, no ethrex host
+ # dependency tree, no ethrex-replay cache. The step is already gated on the
+ # URL being set (see "Determine run count").
+ #
+ # Untracked, so like the ELF it survives `git checkout origin/main` and both
+ # sides prove the identical block.
+ make ethrex-real-block-fixture
+ ls -l "$REAL_INPUT"
- name: Memory growth (PR)
id: pr-growth
@@ -312,6 +367,70 @@ jobs:
echo "growth_slope_mb=$SLOPE" >> /tmp/metrics.txt
echo "growth_r2=$R2" >> /tmp/metrics.txt
+ - name: Real block (PR)
+ id: pr-real
+ if: steps.config.outputs.run_real == 'true'
+ env:
+ RUNS: ${{ steps.config.outputs.runs }}
+ TABLE_PARALLELISM: ${{ steps.config.outputs.table_parallelism }}
+ run: |
+ if [ -n "$TABLE_PARALLELISM" ]; then
+ export TABLE_PARALLELISM
+ echo "TABLE_PARALLELISM=$TABLE_PARALLELISM"
+ fi
+ TIMES=""
+ HEAPS=""
+ EPOCHS=""
+ for i in $(seq 1 "$RUNS"); do
+ echo "--- Real block run $i/$RUNS (continuations, epoch 2^$REAL_BLOCK_EPOCH_LOG2) ---"
+ ./target/release/cli prove "$ELF" --private-input "$REAL_INPUT" \
+ --continuations --epoch-size-log2 "$REAL_BLOCK_EPOCH_LOG2" \
+ -o /tmp/real_proof.bin --time | tee /tmp/real_output_$i.txt
+ rm -f /tmp/real_proof.bin
+
+ T=$(grep -o 'Proving time: [0-9.]*' /tmp/real_output_$i.txt | awk '{print $3}')
+ # Peak heap is optional here, unlike the monolithic step: the continuation
+ # prove path only learned to report it alongside this benchmark, so a
+ # baseline built from an older main prints no such line. Missing heap
+ # degrades one table cell; a missing time makes the run meaningless.
+ H=$(grep -o 'Peak heap: [0-9]*' /tmp/real_output_$i.txt | awk '{print $3}')
+ E=$(grep -o 'Epochs: [0-9]*' /tmp/real_output_$i.txt | awk '{print $2}')
+
+ if [ -z "$T" ]; then
+ echo "::error::Failed to parse real-block proving time from run $i"
+ cat /tmp/real_output_$i.txt
+ exit 1
+ fi
+ if [ -z "$H" ]; then
+ echo "::warning::No 'Peak heap' line on the real-block run (CLI predates continuation heap reporting)"
+ fi
+
+ TIMES="$TIMES $T"
+ if [ -n "$H" ]; then HEAPS="$HEAPS $H"; fi
+ EPOCHS="$E"
+ done
+
+ # Same summary as the synthetic screen: median for the headline, spread so a
+ # single slow run is visible rather than silently shifting the verdict, and
+ # every raw value so a reader can judge for themselves.
+ MEDIAN_POS=$(( (RUNS + 1) / 2 ))
+ TIME_MEDIAN=$(echo $TIMES | tr ' ' '\n' | sort -n | awk "NR==$MEDIAN_POS")
+ HEAP_MEDIAN=$(echo $HEAPS | tr ' ' '\n' | sort -n | awk "NR==$MEDIAN_POS")
+ TIME_MIN=$(echo $TIMES | tr ' ' '\n' | sort -n | head -1)
+ TIME_MAX=$(echo $TIMES | tr ' ' '\n' | sort -n | tail -1)
+ TIME_SPREAD=$(awk "BEGIN { if ($TIME_MEDIAN > 0) printf \"%.1f\", (($TIME_MAX - $TIME_MIN) / $TIME_MEDIAN) * 100; else print \"0.0\" }")
+ ALL_TIMES=$(echo $TIMES | tr ' ' '\n' | paste -sd '/' -)
+
+ {
+ echo "real_time_s=$TIME_MEDIAN"
+ echo "real_peak_mb=$HEAP_MEDIAN"
+ echo "real_epochs=$EPOCHS"
+ echo "real_runs=$RUNS"
+ echo "real_time_spread=$TIME_SPREAD"
+ echo "real_all_times=$ALL_TIMES"
+ echo "real_input=$(basename "$REAL_INPUT")"
+ } | tee -a /tmp/metrics.txt >> "$GITHUB_OUTPUT"
+
- name: Upload metrics artifact
uses: actions/upload-artifact@v4
with:
@@ -338,17 +457,15 @@ jobs:
BASELINE_FILE=$(ls -t baseline/*/metrics.txt 2>/dev/null | head -1)
if [ -n "$BASELINE_FILE" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
- echo "peak_mb=$(grep 'peak_mb=' "$BASELINE_FILE" | cut -d= -f2)" >> "$GITHUB_OUTPUT"
- echo "time_s=$(grep 'time_s=' "$BASELINE_FILE" | cut -d= -f2)" >> "$GITHUB_OUTPUT"
- echo "time_spread=$(grep 'time_spread=' "$BASELINE_FILE" | cut -d= -f2)" >> "$GITHUB_OUTPUT"
- echo "heap_spread=$(grep 'heap_spread=' "$BASELINE_FILE" | cut -d= -f2)" >> "$GITHUB_OUTPUT"
- echo "all_times=$(grep 'all_times=' "$BASELINE_FILE" | cut -d= -f2)" >> "$GITHUB_OUTPUT"
- echo "all_heaps=$(grep 'all_heaps=' "$BASELINE_FILE" | cut -d= -f2)" >> "$GITHUB_OUTPUT"
- echo "runs=$(grep 'runs=' "$BASELINE_FILE" | cut -d= -f2)" >> "$GITHUB_OUTPUT"
- echo "growth_heaps=$(grep 'growth_heaps=' "$BASELINE_FILE" | cut -d= -f2)" >> "$GITHUB_OUTPUT"
- echo "growth_times=$(grep 'growth_times=' "$BASELINE_FILE" | cut -d= -f2)" >> "$GITHUB_OUTPUT"
- echo "growth_slope_mb=$(grep 'growth_slope_mb=' "$BASELINE_FILE" | cut -d= -f2)" >> "$GITHUB_OUTPUT"
- echo "growth_r2=$(grep 'growth_r2=' "$BASELINE_FILE" | cut -d= -f2)" >> "$GITHUB_OUTPUT"
+ # Anchored (`^key=`): `real_time_s` contains `time_s`, so an unanchored
+ # grep could match two lines and write a multi-line step output.
+ get() { grep "^$1=" "$BASELINE_FILE" | head -1 | cut -d= -f2; }
+ for key in growth_heaps growth_times growth_slope_mb growth_r2 \
+ real_time_s real_peak_mb real_input; do
+ # A baseline predating the real block simply has no real_* keys; empty
+ # values hide the table rather than producing a bogus comparison.
+ echo "$key=$(get "$key")" >> "$GITHUB_OUTPUT"
+ done
exit 0
fi
fi
@@ -363,6 +480,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
RUNS: ${{ steps.config.outputs.runs }}
RUN_GROWTH: ${{ steps.config.outputs.run_growth }}
+ RUN_REAL: ${{ steps.config.outputs.run_real }}
TABLE_PARALLELISM: ${{ steps.config.outputs.table_parallelism }}
run: |
if [ -n "$TABLE_PARALLELISM" ]; then
@@ -380,56 +498,6 @@ jobs:
cargo build --release -p cli --features jemalloc-stats
- # --- Primary benchmark (ethrex 20 transfers) ---
- TIMES=""
- HEAPS=""
- for i in $(seq 1 $RUNS); do
- echo "--- Baseline run $i/$RUNS ---"
- ./target/release/cli prove "$ELF" --private-input "$INPUT" -o /tmp/proof.bin --time \
- | tee /tmp/baseline_output_$i.txt
- rm -f /tmp/proof.bin
-
- T=$(grep -o 'Proving time: [0-9.]*' /tmp/baseline_output_$i.txt | awk '{print $3}')
- H=$(grep -o 'Peak heap: [0-9]*' /tmp/baseline_output_$i.txt | awk '{print $3}')
-
- if [ -z "$T" ] || [ -z "$H" ]; then
- echo "::error::Failed to parse baseline metrics from run $i"
- cat /tmp/baseline_output_$i.txt
- exit 1
- fi
-
- TIMES="$TIMES $T"
- HEAPS="$HEAPS $H"
- done
-
- MEDIAN_POS=$(( (RUNS + 1) / 2 ))
- TIME_MEDIAN=$(echo $TIMES | tr ' ' '\n' | sort -n | awk "NR==$MEDIAN_POS")
- HEAP_MEDIAN=$(echo $HEAPS | tr ' ' '\n' | sort -n | awk "NR==$MEDIAN_POS")
-
- if [ -z "$HEAP_MEDIAN" ] || [ -z "$TIME_MEDIAN" ]; then
- echo "::error::Failed to compute baseline median metrics"
- exit 1
- fi
-
- TIME_MIN=$(echo $TIMES | tr ' ' '\n' | sort -n | head -1)
- TIME_MAX=$(echo $TIMES | tr ' ' '\n' | sort -n | tail -1)
- TIME_SPREAD=$(awk "BEGIN { if ($TIME_MEDIAN > 0) printf \"%.1f\", (($TIME_MAX - $TIME_MIN) / $TIME_MEDIAN) * 100; else print \"0.0\" }")
-
- HEAP_MIN=$(echo $HEAPS | tr ' ' '\n' | sort -n | head -1)
- HEAP_MAX=$(echo $HEAPS | tr ' ' '\n' | sort -n | tail -1)
- HEAP_SPREAD=$(awk "BEGIN { if ($HEAP_MEDIAN > 0) printf \"%.1f\", (($HEAP_MAX - $HEAP_MIN) / $HEAP_MEDIAN) * 100; else print \"0.0\" }")
-
- ALL_TIMES=$(echo $TIMES | tr ' ' '\n' | paste -sd '/' -)
- ALL_HEAPS=$(echo $HEAPS | tr ' ' '\n' | paste -sd '/' -)
-
- echo "peak_mb=$HEAP_MEDIAN" >> "$GITHUB_OUTPUT"
- echo "time_s=$TIME_MEDIAN" >> "$GITHUB_OUTPUT"
- echo "time_spread=$TIME_SPREAD" >> "$GITHUB_OUTPUT"
- echo "heap_spread=$HEAP_SPREAD" >> "$GITHUB_OUTPUT"
- echo "all_times=$ALL_TIMES" >> "$GITHUB_OUTPUT"
- echo "all_heaps=$ALL_HEAPS" >> "$GITHUB_OUTPUT"
- echo "runs=$RUNS" >> "$GITHUB_OUTPUT"
-
# --- Growth benchmarks (default parallelism, 1 sample each) ---
# Only run if /bench-growth, push, or workflow_dispatch
if [ "$RUN_GROWTH" != "true" ]; then
@@ -495,6 +563,41 @@ jobs:
echo "growth_r2=$R2" >> "$GITHUB_OUTPUT"
fi # end run_growth check
+ # --- Real block (continuations, 1 sample) ---
+ # Only reached when no cached baseline exists, which is the expensive case:
+ # push-to-main normally publishes the real-block numbers, so a /bench comment
+ # pays for the PR side alone. $REAL_INPUT was resolved before this
+ # checkout and the fixture is untracked, so main proves the identical block.
+ if [ "$RUN_REAL" != "true" ]; then
+ echo "Skipping real-block baseline (fixture URL unset)"
+ else
+ # Sampled to the same count as the PR side. A 3-vs-1 comparison would put
+ # the two sides' noise on different footings, which is precisely the error
+ # sampling exists to avoid.
+ RTIMES=""; RHEAPS=""
+ for i in $(seq 1 "$RUNS"); do
+ echo "--- Baseline real block run $i/$RUNS (epoch 2^$REAL_BLOCK_EPOCH_LOG2) ---"
+ ./target/release/cli prove "$ELF" --private-input "$REAL_INPUT" \
+ --continuations --epoch-size-log2 "$REAL_BLOCK_EPOCH_LOG2" \
+ -o /tmp/real_proof.bin --time | tee /tmp/baseline_real_$i.txt
+ rm -f /tmp/real_proof.bin
+ T=$(grep -o 'Proving time: [0-9.]*' /tmp/baseline_real_$i.txt | awk '{print $3}')
+ # Optional, as on the PR side: a main that predates continuation heap
+ # reporting prints no such line, and that must not fail the comparison.
+ H=$(grep -o 'Peak heap: [0-9]*' /tmp/baseline_real_$i.txt | awk '{print $3}')
+ if [ -z "$T" ]; then
+ echo "::error::Failed to parse baseline real-block proving time from run $i"
+ cat /tmp/baseline_real_$i.txt
+ exit 1
+ fi
+ RTIMES="$RTIMES $T"
+ if [ -n "$H" ]; then RHEAPS="$RHEAPS $H"; fi
+ done
+ RMED_POS=$(( (RUNS + 1) / 2 ))
+ echo "real_time_s=$(echo $RTIMES | tr ' ' '\n' | sort -n | awk "NR==$RMED_POS")" >> "$GITHUB_OUTPUT"
+ echo "real_peak_mb=$(echo $RHEAPS | tr ' ' '\n' | sort -n | awk "NR==$RMED_POS")" >> "$GITHUB_OUTPUT"
+ fi
+
# Restore PR checkout
git checkout "$PR_SHA"
@@ -506,110 +609,99 @@ jobs:
env:
# Baseline artifact outputs
BASELINE_FOUND: ${{ steps.baseline-artifact.outputs.found }}
- BA_PEAK_MB: ${{ steps.baseline-artifact.outputs.peak_mb }}
- BA_TIME_S: ${{ steps.baseline-artifact.outputs.time_s }}
- BA_TIME_SPREAD: ${{ steps.baseline-artifact.outputs.time_spread }}
- BA_HEAP_SPREAD: ${{ steps.baseline-artifact.outputs.heap_spread }}
- BA_ALL_TIMES: ${{ steps.baseline-artifact.outputs.all_times }}
- BA_ALL_HEAPS: ${{ steps.baseline-artifact.outputs.all_heaps }}
- BA_RUNS: ${{ steps.baseline-artifact.outputs.runs }}
BA_GROWTH_HEAPS: ${{ steps.baseline-artifact.outputs.growth_heaps }}
BA_GROWTH_TIMES: ${{ steps.baseline-artifact.outputs.growth_times }}
BA_GROWTH_SLOPE: ${{ steps.baseline-artifact.outputs.growth_slope_mb }}
BA_GROWTH_R2: ${{ steps.baseline-artifact.outputs.growth_r2 }}
+ BA_REAL_TIME: ${{ steps.baseline-artifact.outputs.real_time_s }}
+ BA_REAL_PEAK: ${{ steps.baseline-artifact.outputs.real_peak_mb }}
+ BA_REAL_INPUT: ${{ steps.baseline-artifact.outputs.real_input }}
# Baseline run outputs
- BR_PEAK_MB: ${{ steps.baseline-run.outputs.peak_mb }}
- BR_TIME_S: ${{ steps.baseline-run.outputs.time_s }}
- BR_TIME_SPREAD: ${{ steps.baseline-run.outputs.time_spread }}
- BR_HEAP_SPREAD: ${{ steps.baseline-run.outputs.heap_spread }}
- BR_ALL_TIMES: ${{ steps.baseline-run.outputs.all_times }}
- BR_ALL_HEAPS: ${{ steps.baseline-run.outputs.all_heaps }}
- BR_RUNS: ${{ steps.baseline-run.outputs.runs }}
BR_GROWTH_HEAPS: ${{ steps.baseline-run.outputs.growth_heaps }}
BR_GROWTH_TIMES: ${{ steps.baseline-run.outputs.growth_times }}
BR_GROWTH_SLOPE: ${{ steps.baseline-run.outputs.growth_slope_mb }}
BR_GROWTH_R2: ${{ steps.baseline-run.outputs.growth_r2 }}
- # PR outputs
- CURRENT_PEAK: ${{ steps.pr.outputs.peak_mb }}
- CURRENT_TIME: ${{ steps.pr.outputs.time_s }}
- PR_TIME_SPREAD: ${{ steps.pr.outputs.time_spread }}
- PR_HEAP_SPREAD: ${{ steps.pr.outputs.heap_spread }}
- PR_ALL_TIMES: ${{ steps.pr.outputs.all_times }}
- PR_ALL_HEAPS: ${{ steps.pr.outputs.all_heaps }}
- PR_RUNS: ${{ steps.pr.outputs.runs }}
+ BR_REAL_TIME: ${{ steps.baseline-run.outputs.real_time_s }}
+ BR_REAL_PEAK: ${{ steps.baseline-run.outputs.real_peak_mb }}
# PR growth outputs
PR_GROWTH_HEAPS: ${{ steps.pr-growth.outputs.growth_heaps }}
PR_GROWTH_TIMES: ${{ steps.pr-growth.outputs.growth_times }}
PR_GROWTH_SLOPE: ${{ steps.pr-growth.outputs.growth_slope_mb }}
PR_GROWTH_R2: ${{ steps.pr-growth.outputs.growth_r2 }}
+ # PR real-block outputs
+ PR_REAL_TIME: ${{ steps.pr-real.outputs.real_time_s }}
+ PR_REAL_PEAK: ${{ steps.pr-real.outputs.real_peak_mb }}
+ PR_REAL_EPOCHS: ${{ steps.pr-real.outputs.real_epochs }}
+ PR_REAL_INPUT: ${{ steps.pr-real.outputs.real_input }}
+ PR_REAL_RUNS: ${{ steps.pr-real.outputs.real_runs }}
+ PR_REAL_TIME_SPREAD: ${{ steps.pr-real.outputs.real_time_spread }}
+ PR_REAL_ALL_TIMES: ${{ steps.pr-real.outputs.real_all_times }}
run: |
# Pick baseline source
if [ "$BASELINE_FOUND" = "true" ]; then
- BASELINE_PEAK="$BA_PEAK_MB"
- BASELINE_TIME="$BA_TIME_S"
BASELINE_SRC="cached"
- BASELINE_TIME_SPREAD="$BA_TIME_SPREAD"
- BASELINE_HEAP_SPREAD="$BA_HEAP_SPREAD"
- BASELINE_ALL_TIMES="$BA_ALL_TIMES"
- BASELINE_ALL_HEAPS="$BA_ALL_HEAPS"
- BASELINE_RUNS="$BA_RUNS"
BASELINE_GROWTH_HEAPS="$BA_GROWTH_HEAPS"
BASELINE_GROWTH_TIMES="$BA_GROWTH_TIMES"
BASELINE_GROWTH_SLOPE="$BA_GROWTH_SLOPE"
BASELINE_GROWTH_R2="$BA_GROWTH_R2"
+ BASELINE_REAL_TIME="$BA_REAL_TIME"
+ BASELINE_REAL_PEAK="$BA_REAL_PEAK"
+ BASELINE_REAL_INPUT="$BA_REAL_INPUT"
else
- BASELINE_PEAK="$BR_PEAK_MB"
- BASELINE_TIME="$BR_TIME_S"
BASELINE_SRC="built from main"
- BASELINE_TIME_SPREAD="$BR_TIME_SPREAD"
- BASELINE_HEAP_SPREAD="$BR_HEAP_SPREAD"
- BASELINE_ALL_TIMES="$BR_ALL_TIMES"
- BASELINE_ALL_HEAPS="$BR_ALL_HEAPS"
- BASELINE_RUNS="$BR_RUNS"
BASELINE_GROWTH_HEAPS="$BR_GROWTH_HEAPS"
BASELINE_GROWTH_TIMES="$BR_GROWTH_TIMES"
BASELINE_GROWTH_SLOPE="$BR_GROWTH_SLOPE"
BASELINE_GROWTH_R2="$BR_GROWTH_R2"
+ BASELINE_REAL_TIME="$BR_REAL_TIME"
+ BASELINE_REAL_PEAK="$BR_REAL_PEAK"
+ # Freshly proven on this runner from $REAL_INPUT, so by construction the
+ # same block the PR side used; the cached path carries its own label.
+ BASELINE_REAL_INPUT="$PR_REAL_INPUT"
fi
- if [ -z "$BASELINE_PEAK" ] || [ "$BASELINE_PEAK" -eq 0 ] 2>/dev/null ||
- [ -z "$BASELINE_TIME" ]; then
- echo "::error::Invalid baseline values: peak=$BASELINE_PEAK time=$BASELINE_TIME"
- exit 1
- fi
+ echo "baseline_src=$BASELINE_SRC" >> "$GITHUB_OUTPUT"
- PEAK_DIFF=$((CURRENT_PEAK - BASELINE_PEAK))
- PEAK_PCT=$(awk "BEGIN { printf \"%.1f\", ($PEAK_DIFF * 100) / $BASELINE_PEAK }")
- TIME_DIFF=$(awk "BEGIN { printf \"%.3f\", $CURRENT_TIME - $BASELINE_TIME }")
- TIME_PCT=$(awk "BEGIN { printf \"%.1f\", (($CURRENT_TIME - $BASELINE_TIME) * 100) / $BASELINE_TIME }")
+ # A missing real-block baseline is NOT an error: it is what a PR sees before
+ # main has published one, and the comment renders the PR side alone. Only a
+ # missing PR-side number means the run failed to measure anything, and the
+ # real-block step already exits non-zero in that case.
+ if [ -z "$BASELINE_REAL_TIME" ]; then
+ echo "::notice::No real-block baseline available; reporting the PR side only."
+ fi
- echo "baseline_peak=$BASELINE_PEAK" >> "$GITHUB_OUTPUT"
- echo "baseline_time=$BASELINE_TIME" >> "$GITHUB_OUTPUT"
- echo "baseline_src=$BASELINE_SRC" >> "$GITHUB_OUTPUT"
- echo "peak_diff=$PEAK_DIFF" >> "$GITHUB_OUTPUT"
- echo "peak_pct=$PEAK_PCT" >> "$GITHUB_OUTPUT"
- echo "time_diff=$TIME_DIFF" >> "$GITHUB_OUTPUT"
- echo "time_pct=$TIME_PCT" >> "$GITHUB_OUTPUT"
- echo "pr_time_spread=$PR_TIME_SPREAD" >> "$GITHUB_OUTPUT"
- echo "pr_heap_spread=$PR_HEAP_SPREAD" >> "$GITHUB_OUTPUT"
- echo "pr_all_times=$PR_ALL_TIMES" >> "$GITHUB_OUTPUT"
- echo "pr_all_heaps=$PR_ALL_HEAPS" >> "$GITHUB_OUTPUT"
- echo "pr_runs=$PR_RUNS" >> "$GITHUB_OUTPUT"
- echo "baseline_time_spread=$BASELINE_TIME_SPREAD" >> "$GITHUB_OUTPUT"
- echo "baseline_heap_spread=$BASELINE_HEAP_SPREAD" >> "$GITHUB_OUTPUT"
- echo "baseline_all_times=$BASELINE_ALL_TIMES" >> "$GITHUB_OUTPUT"
- echo "baseline_all_heaps=$BASELINE_ALL_HEAPS" >> "$GITHUB_OUTPUT"
- echo "baseline_runs=$BASELINE_RUNS" >> "$GITHUB_OUTPUT"
-
- # Growth comparison
- echo "pr_growth_heaps=$PR_GROWTH_HEAPS" >> "$GITHUB_OUTPUT"
- echo "pr_growth_times=$PR_GROWTH_TIMES" >> "$GITHUB_OUTPUT"
- echo "pr_growth_slope=$PR_GROWTH_SLOPE" >> "$GITHUB_OUTPUT"
- echo "pr_growth_r2=$PR_GROWTH_R2" >> "$GITHUB_OUTPUT"
- echo "baseline_growth_heaps=$BASELINE_GROWTH_HEAPS" >> "$GITHUB_OUTPUT"
- echo "baseline_growth_times=$BASELINE_GROWTH_TIMES" >> "$GITHUB_OUTPUT"
- echo "baseline_growth_slope=$BASELINE_GROWTH_SLOPE" >> "$GITHUB_OUTPUT"
- echo "baseline_growth_r2=$BASELINE_GROWTH_R2" >> "$GITHUB_OUTPUT"
+ # Real-block comparison. Rendered only when BOTH sides have a number AND
+ # they are the same block: a baseline captured before the Makefile was
+ # repointed measures a different workload, and showing that as a delta
+ # would invent a regression out of a fixture swap.
+ echo "pr_real_time=$PR_REAL_TIME" >> "$GITHUB_OUTPUT"
+ echo "pr_real_peak=$PR_REAL_PEAK" >> "$GITHUB_OUTPUT"
+ echo "pr_real_epochs=$PR_REAL_EPOCHS" >> "$GITHUB_OUTPUT"
+ echo "pr_real_input=$PR_REAL_INPUT" >> "$GITHUB_OUTPUT"
+ echo "pr_real_runs=$PR_REAL_RUNS" >> "$GITHUB_OUTPUT"
+ echo "pr_real_time_spread=$PR_REAL_TIME_SPREAD" >> "$GITHUB_OUTPUT"
+ echo "pr_real_all_times=$PR_REAL_ALL_TIMES" >> "$GITHUB_OUTPUT"
+ echo "baseline_real_time=$BASELINE_REAL_TIME" >> "$GITHUB_OUTPUT"
+ echo "baseline_real_peak=$BASELINE_REAL_PEAK" >> "$GITHUB_OUTPUT"
+ echo "baseline_real_input=$BASELINE_REAL_INPUT" >> "$GITHUB_OUTPUT"
+
+ if [ -n "$PR_REAL_TIME" ] && [ -n "$BASELINE_REAL_TIME" ]; then
+ if [ -n "$BASELINE_REAL_INPUT" ] && [ "$BASELINE_REAL_INPUT" != "$PR_REAL_INPUT" ]; then
+ echo "::warning::Baseline real block ($BASELINE_REAL_INPUT) differs from the PR's ($PR_REAL_INPUT); not comparing."
+ echo "real_mismatch=$BASELINE_REAL_INPUT" >> "$GITHUB_OUTPUT"
+ else
+ REAL_TIME_DIFF=$(awk "BEGIN { printf \"%.3f\", $PR_REAL_TIME - $BASELINE_REAL_TIME }")
+ REAL_TIME_PCT=$(awk "BEGIN { printf \"%.1f\", (($PR_REAL_TIME - $BASELINE_REAL_TIME) * 100) / $BASELINE_REAL_TIME }")
+ echo "real_time_diff=$REAL_TIME_DIFF" >> "$GITHUB_OUTPUT"
+ echo "real_time_pct=$REAL_TIME_PCT" >> "$GITHUB_OUTPUT"
+ if [ -n "$PR_REAL_PEAK" ] && [ -n "$BASELINE_REAL_PEAK" ]; then
+ REAL_PEAK_DIFF=$((PR_REAL_PEAK - BASELINE_REAL_PEAK))
+ REAL_PEAK_PCT=$(awk "BEGIN { printf \"%.1f\", ($REAL_PEAK_DIFF * 100) / $BASELINE_REAL_PEAK }")
+ echo "real_peak_diff=$REAL_PEAK_DIFF" >> "$GITHUB_OUTPUT"
+ echo "real_peak_pct=$REAL_PEAK_PCT" >> "$GITHUB_OUTPUT"
+ fi
+ fi
+ fi
# Growth slope comparison
if [ -n "$BASELINE_GROWTH_SLOPE" ] && [ -n "$PR_GROWTH_SLOPE" ]; then
@@ -623,24 +715,7 @@ jobs:
if: github.event_name != 'push' && github.event_name != 'workflow_dispatch'
uses: actions/github-script@v7
env:
- PR_PEAK: ${{ steps.pr.outputs.peak_mb }}
- PR_TIME: ${{ steps.pr.outputs.time_s }}
- BASELINE_PEAK: ${{ steps.compare.outputs.baseline_peak }}
- BASELINE_TIME: ${{ steps.compare.outputs.baseline_time }}
BASELINE_SRC: ${{ steps.compare.outputs.baseline_src }}
- PEAK_PCT: ${{ steps.compare.outputs.peak_pct }}
- TIME_PCT: ${{ steps.compare.outputs.time_pct }}
- PEAK_DIFF: ${{ steps.compare.outputs.peak_diff }}
- TIME_DIFF: ${{ steps.compare.outputs.time_diff }}
- PR_RUNS: ${{ steps.compare.outputs.pr_runs }}
- PR_TIME_SPREAD: ${{ steps.compare.outputs.pr_time_spread }}
- PR_HEAP_SPREAD: ${{ steps.compare.outputs.pr_heap_spread }}
- PR_ALL_TIMES: ${{ steps.compare.outputs.pr_all_times }}
- PR_ALL_HEAPS: ${{ steps.compare.outputs.pr_all_heaps }}
- BASE_TIME_SPREAD: ${{ steps.compare.outputs.baseline_time_spread }}
- BASE_HEAP_SPREAD: ${{ steps.compare.outputs.baseline_heap_spread }}
- BASE_ALL_TIMES: ${{ steps.compare.outputs.baseline_all_times }}
- BASE_ALL_HEAPS: ${{ steps.compare.outputs.baseline_all_heaps }}
PR_GROWTH_HEAPS: ${{ steps.compare.outputs.pr_growth_heaps }}
PR_GROWTH_TIMES: ${{ steps.compare.outputs.pr_growth_times }}
PR_GROWTH_SLOPE: ${{ steps.compare.outputs.pr_growth_slope }}
@@ -651,28 +726,30 @@ jobs:
BASE_GROWTH_R2: ${{ steps.compare.outputs.baseline_growth_r2 }}
GROWTH_SLOPE_DIFF: ${{ steps.compare.outputs.growth_slope_diff }}
GROWTH_SLOPE_PCT: ${{ steps.compare.outputs.growth_slope_pct }}
+ PR_REAL_TIME: ${{ steps.compare.outputs.pr_real_time }}
+ PR_REAL_PEAK: ${{ steps.compare.outputs.pr_real_peak }}
+ PR_REAL_EPOCHS: ${{ steps.compare.outputs.pr_real_epochs }}
+ PR_REAL_INPUT: ${{ steps.compare.outputs.pr_real_input }}
+ BASE_REAL_TIME: ${{ steps.compare.outputs.baseline_real_time }}
+ BASE_REAL_PEAK: ${{ steps.compare.outputs.baseline_real_peak }}
+ REAL_TIME_DIFF: ${{ steps.compare.outputs.real_time_diff }}
+ REAL_TIME_PCT: ${{ steps.compare.outputs.real_time_pct }}
+ REAL_PEAK_DIFF: ${{ steps.compare.outputs.real_peak_diff }}
+ REAL_PEAK_PCT: ${{ steps.compare.outputs.real_peak_pct }}
+ REAL_MISMATCH: ${{ steps.compare.outputs.real_mismatch }}
+ REAL_EPOCH_LOG2: ${{ env.REAL_BLOCK_EPOCH_LOG2 }}
+ REAL_RUNS: ${{ steps.compare.outputs.pr_real_runs }}
+ REAL_TIME_SPREAD: ${{ steps.compare.outputs.pr_real_time_spread }}
+ REAL_ALL_TIMES: ${{ steps.compare.outputs.pr_real_all_times }}
COMMIT_SHA: ${{ steps.pr-ref.outputs.sha || github.sha }}
TABLE_PARALLELISM: ${{ steps.config.outputs.table_parallelism }}
with:
+ # Renderable offline: `node scripts/render_bench_comment.js` extracts this block
+ # and prints the markdown for a set of scenarios, so wording and formatting can
+ # be checked without occupying the bench server for 15-20 min. Add a scenario
+ # there when you add a branch here.
script: |
- const peak = process.env.PR_PEAK;
- const time = process.env.PR_TIME;
- const basePeak = process.env.BASELINE_PEAK;
- const baseTime = process.env.BASELINE_TIME;
const baseSrc = process.env.BASELINE_SRC;
- const peakPct = process.env.PEAK_PCT;
- const timePct = process.env.TIME_PCT;
- const peakDiff = process.env.PEAK_DIFF;
- const timeDiff = process.env.TIME_DIFF;
- const runs = process.env.PR_RUNS || '1';
- const prTimeSpread = process.env.PR_TIME_SPREAD;
- const prHeapSpread = process.env.PR_HEAP_SPREAD;
- const prAllTimes = process.env.PR_ALL_TIMES;
- const prAllHeaps = process.env.PR_ALL_HEAPS;
- const baseTimeSpread = process.env.BASE_TIME_SPREAD;
- const baseHeapSpread = process.env.BASE_HEAP_SPREAD;
- const baseAllTimes = process.env.BASE_ALL_TIMES;
- const baseAllHeaps = process.env.BASE_ALL_HEAPS;
// Growth data
const prGrowthHeaps = process.env.PR_GROWTH_HEAPS;
@@ -688,76 +765,86 @@ jobs:
const fmt = (v) => parseFloat(v) >= 0 ? `+${v}` : v;
const icon = (pct) => parseFloat(pct) > 5 ? '🔴' : parseFloat(pct) < -5 ? '🟢' : '⚪';
- const SPREAD_THRESHOLD = 5.0;
-
- // --- Section 1: Primary benchmark ---
- const nLabel = parseInt(runs) > 1 ? ` (median of ${runs})` : '';
- const tableParallelism = process.env.TABLE_PARALLELISM;
- const tpLabel = tableParallelism ? tableParallelism : 'auto (cores / 3)';
- let body = `## Benchmark — ethrex 20 transfers${nLabel}\n\n`;
- body += `Table parallelism: ${tpLabel}\n\n`;
- body += `| Metric | main | PR | Δ |\n`;
- body += `|--------|------|----|---|\n`;
- body += `| **Peak heap** | ${basePeak} MB | ${peak} MB | ${fmt(peakDiff)} MB (${fmt(peakPct)}%) ${icon(peakPct)} |\n`;
- body += `| **Prove time** | ${baseTime}s | ${time}s | ${fmt(timeDiff)}s (${fmt(timePct)}%) ${icon(timePct)} |\n\n`;
-
- const regression = parseFloat(peakPct) > 5 || parseFloat(timePct) > 5;
- const improvement = parseFloat(peakPct) < -5 || parseFloat(timePct) < -5;
- if (regression) {
- body += `> ⚠️ **Regression detected** — heap or time increased by more than 5%.\n`;
- } else if (improvement) {
- body += `> 🎉 **Improvement detected** — heap or time decreased by more than 5%.\n`;
- } else {
- body += `> ✅ No significant change.\n`;
- }
-
- // Tier-1 -> Tier-2 escalation: a small time speedup the cheap 3-5 run CI
- // can't confirm (it catches >=~1.5% on its own). Point at the drift-free
- // ABBA tiebreaker, which the user runs on demand via `/bench-abba`.
- const tp = parseFloat(timePct);
- if (tp < 0 && tp > -1.5) {
- body += `>\n`;
- body += `> 🔬 **Looks like a small speedup (${fmt(timePct)}%) — below what ${runs} runs can confirm.** `;
- body += `Comment \`/bench-abba\` to run the drift-free ABBA tiebreaker (paired-t CI + exact Wilcoxon). `;
- body += `Note: it occupies the bench server for ~30–40 min.\n`;
- body += `> Optional pair count: \`/bench-abba 32\` (20 resolves ~1%, 32 for ~0.6%).\n`;
- }
-
- // Spread warnings
- const prWarnings = [];
- const baseWarnings = [];
-
- if (prTimeSpread && parseFloat(prTimeSpread) > SPREAD_THRESHOLD) {
- const vals = prAllTimes ? prAllTimes.split('/').map(t => `${t}s`).join(' / ') : '';
- prWarnings.push(`Prove time spread: ${prTimeSpread}% (${vals})`);
- }
- if (prHeapSpread && parseFloat(prHeapSpread) > SPREAD_THRESHOLD) {
- const vals = prAllHeaps ? prAllHeaps.split('/').map(h => `${h} MB`).join(' / ') : '';
- prWarnings.push(`Heap spread: ${prHeapSpread}% (${vals})`);
- }
- if (baseTimeSpread && parseFloat(baseTimeSpread) > SPREAD_THRESHOLD) {
- const vals = baseAllTimes ? baseAllTimes.split('/').map(t => `${t}s`).join(' / ') : '';
- baseWarnings.push(`Baseline time spread: ${baseTimeSpread}% (${vals}) — comparison may be less reliable`);
- }
- if (baseHeapSpread && parseFloat(baseHeapSpread) > SPREAD_THRESHOLD) {
- const vals = baseAllHeaps ? baseAllHeaps.split('/').map(h => `${h} MB`).join(' / ') : '';
- baseWarnings.push(`Baseline heap spread: ${baseHeapSpread}% (${vals}) — comparison may be less reliable`);
- }
- const allWarnings = [...prWarnings, ...baseWarnings];
- if (allWarnings.length > 0) {
- body += `\n`;
- for (const w of allWarnings) {
- body += `> ⚠️ ${w}\n`;
+ // Real-block data
+ const realTime = process.env.PR_REAL_TIME;
+ const realPeak = process.env.PR_REAL_PEAK;
+ const realEpochs = process.env.PR_REAL_EPOCHS;
+ const realInput = process.env.PR_REAL_INPUT;
+ const baseRealTime = process.env.BASE_REAL_TIME;
+ const baseRealPeak = process.env.BASE_REAL_PEAK;
+ const realTimeDiff = process.env.REAL_TIME_DIFF;
+ const realTimePct = process.env.REAL_TIME_PCT;
+ const realPeakDiff = process.env.REAL_PEAK_DIFF;
+ const realPeakPct = process.env.REAL_PEAK_PCT;
+ const realMismatch = process.env.REAL_MISMATCH;
+ const realEpochLog2 = process.env.REAL_EPOCH_LOG2;
+ const realRuns = process.env.REAL_RUNS || '1';
+ const realTimeSpread = process.env.REAL_TIME_SPREAD;
+ const realAllTimes = process.env.REAL_ALL_TIMES;
+
+ // Stable marker: the "find and update the existing comment" lookup at the
+ // bottom keys off it, so section headings can change without orphaning
+ // every comment already posted. Legacy title matches stay as a fallback.
+ let body = `\n`;
+
+ // --- Section 1: Real block (headline; present whenever the fixture is fetchable) ---
+ if (realTime) {
+ body += `## Benchmark — real block${realInput ? ` (\`${realInput}\`)` : ''}${parseInt(realRuns) > 1 ? ` (median of ${realRuns})` : ''}\n\n`;
+ // Run count lives in the heading ("median of N"), as on the synthetic
+ // section — repeating it here just made the two disagree in tone.
+ body += `continuations · epoch 2^${realEpochLog2}`;
+ if (realEpochs) body += ` · ${realEpochs} epochs`;
+ body += `\n\n`;
+
+ if (realMismatch) {
+ body += `> ⚠️ Baseline measured a different block (\`${realMismatch}\`) — showing the PR side only.\n\n`;
}
- if (prWarnings.length > 0) {
- body += `> Consider re-running \`/bench\`\n`;
+
+ const haveRealCmp = !!(baseRealTime && realTimePct && !realMismatch);
+ if (haveRealCmp) {
+ body += `| Metric | main | PR | Δ |\n`;
+ body += `|--------|------|----|---|\n`;
+ if (realPeak && baseRealPeak && realPeakPct) {
+ body += `| **Peak heap** | ${baseRealPeak} MB | ${realPeak} MB | ${fmt(realPeakDiff)} MB (${fmt(realPeakPct)}%) ${icon(realPeakPct)} |\n`;
+ }
+ body += `| **Prove time** | ${baseRealTime}s | ${realTime}s | ${fmt(realTimeDiff)}s (${fmt(realTimePct)}%) ${icon(realTimePct)} |\n\n`;
+
+ // Wider bands than the synthetic screen's 5%: 3 runs of a minutes-long
+ // prove resolve less than 5 runs of a 25s one, so the middle is reported
+ // as unresolved rather than as "fine".
+ const rp = parseFloat(realTimePct);
+ if (rp > 10) {
+ body += `> ⚠️ **Regression on the real block** — prove time up ${Math.abs(rp).toFixed(1)}%.\n`;
+ } else if (rp < -10) {
+ body += `> 🎉 **Improvement on the real block** — prove time down ${Math.abs(rp).toFixed(1)}%.\n`;
+ } else if (Math.abs(rp) >= 3) {
+ body += `> ❓ **${fmt(realTimePct)}% — beyond what ${realRuns} runs resolve.** Use \`/bench-abba\` for a paired test.\n`;
+ } else {
+ body += `> ✅ No significant change.\n`;
+ }
+ if (realTimeSpread && parseFloat(realTimeSpread) > 5.0) {
+ const vals = realAllTimes ? realAllTimes.split('/').map(t => `${t}s`).join(' / ') : '';
+ body += `>\n> ⚠️ Real-block prove-time spread: ${realTimeSpread}% (${vals}) — the median above is less trustworthy than usual.\n`;
+ } else if (realTimeSpread && parseInt(realRuns) > 1) {
+ body += `>\n> Prove-time spread ${realTimeSpread}%${realAllTimes ? ` (${realAllTimes.split('/').map(t => `${t}s`).join(' / ')})` : ''}\n`;
+ }
+ } else {
+ body += `| Metric | PR |\n`;
+ body += `|--------|----|\n`;
+ if (realPeak) body += `| **Peak heap** | ${realPeak} MB |\n`;
+ body += `| **Prove time** | ${realTime}s |\n\n`;
+ if (!realMismatch) {
+ body += `> ℹ️ No real-block baseline yet — main publishes one on its next push.\n`;
+ }
}
- } else if (parseInt(runs) > 1) {
- body += `\n> ✅ Low variance (time: ${prTimeSpread || '0.0'}%, heap: ${prHeapSpread || '0.0'}%)\n`;
+ body += `\n`;
}
- // --- Section 2: Memory growth ---
+ // --- Section 3: Memory growth ---
+ // Synthetic on purpose: this plots heap against BLOCK SIZE, which needs a
+ // family of blocks that differ only in transaction count. A real block is
+ // one point and cannot produce a slope.
if (prGrowthHeaps) {
const prHeaps = prGrowthHeaps.split('/');
const baseHeaps = baseGrowthHeaps ? baseGrowthHeaps.split('/') : null;
@@ -816,6 +903,14 @@ jobs:
}
// --- Footer ---
+ // The real block runs on every invocation, so its absence means the fixture
+ // could not be fetched — say that plainly rather than offering a command to
+ // re-request it, which no longer exists.
+ if (!realTime) {
+ body += `\n> 🧱 **No prover measurement — the real-block fixture was not available.** `;
+ body += `\`/bench\` proves only the real block, so nothing was measured this run. `;
+ body += `Check that \`ETHREX_REAL_BLOCK_FIXTURE_URL\` is set in the Makefile and that the artifact is reachable; the run log carries the warning.\n`;
+ }
const sha = process.env.COMMIT_SHA.substring(0, 8);
body += `\nCommit: ${sha} · Baseline: ${baseSrc} · Runner: self-hosted bench\n`;
@@ -825,9 +920,11 @@ jobs:
issue_number: context.issue.number,
});
- // Find existing comment (check both old and new markers for transition)
+ // Find existing comment. The HTML marker is the durable key; the title
+ // matches below are the transition path for comments posted before it.
const existing = comments.find(c =>
c.user.type === 'Bot' && (
+ c.body.includes('') ||
c.body.includes('Benchmark — ethrex') ||
c.body.includes('Benchmark — fib_iterative_8M') ||
c.body.includes('Benchmark — fib_iterative_2M') ||
diff --git a/.github/workflows/ethrex-block-converter.yml b/.github/workflows/ethrex-block-converter.yml
new file mode 100644
index 000000000..53c640e9b
--- /dev/null
+++ b/.github/workflows/ethrex-block-converter.yml
@@ -0,0 +1,113 @@
+name: ethrex block-converter tests
+
+# Validation for the real-block benchmark fixture and the converter that produces
+# it. Deliberately NOT part of the required PR gate (pr_main.yaml): the fixture is a
+# benchmark input, not a correctness input — no product code reads it — so it needs
+# to be right when it changes, not on every PR. Running it there cost every PR a
+# network download plus a cold build of ~335 packages (blst, c-kzg and secp256k1-sys
+# C builds, malachite, ark-ff/asm).
+#
+# It fires on the things that can actually invalidate it: the converter, the ethrex
+# host-reference tests, and the Makefile (which holds the block pin, the fixture URL
+# and its sha256).
+on:
+ workflow_dispatch:
+ pull_request:
+ branches: ["**"]
+ paths:
+ - 'tooling/ethrex-block-converter/**'
+ - 'tooling/ethrex-tests/**'
+ - 'Makefile'
+ - '.github/workflows/ethrex-block-converter.yml'
+ push:
+ branches: ["main"]
+ paths:
+ - 'tooling/ethrex-block-converter/**'
+ - 'tooling/ethrex-tests/**'
+ - 'Makefile'
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+ cancel-in-progress: true
+
+jobs:
+ converter:
+ name: Block converter tests
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@v4
+
+ - name: Setup Rust Environment
+ uses: ./.github/actions/setup-rust
+
+ - name: Cache cargo build artifacts
+ uses: Swatinem/rust-cache@v2
+ with:
+ # Own key, not pr_main.yaml's "lambda-vm-test": a shared key across jobs
+ # that build different crate sets causes recompilation on both sides.
+ shared-key: "lambda-vm-ethrex-block-converter"
+ cache-all-crates: "true"
+ # Detached workspace (own Cargo.lock, own target dir), so the default
+ # `. -> target` would miss it and rebuild the ethrex tree every run.
+ workspaces: |
+ tooling/ethrex-block-converter -> target
+
+ # Downloads the pinned ethrex-replay cache (the converter's test input) and
+ # runs the crate's tests: host-side parity through the guest's own
+ # `LambdaVmEcsmCrypto`, the unmappable-network rejection, and the
+ # reproducibility digest. Note these do NOT screen KZG — this crate's graph
+ # links c-kzg via ethrex-config, so point evaluation (0x0a) resolves here and
+ # to nothing in the guest. The block-usability job below is what covers that.
+ - name: Run converter tests
+ run: make test-ethrex-real-block-converter
+
+ block-usable:
+ name: Real-block usability screen
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@v4
+
+ - name: Setup Rust Environment
+ uses: ./.github/actions/setup-rust
+
+ - name: Cache cargo build artifacts
+ uses: Swatinem/rust-cache@v2
+ with:
+ shared-key: "lambda-vm-real-block-usable"
+ cache-all-crates: "true"
+ workspaces: |
+ tooling/ethrex-tests -> target
+
+ # Fetch-and-verify, not build: no converter, no ethrex-replay cache, no rev pin.
+ # The guard covers the window after a repoint but before the new artifact is
+ # uploaded: the screen below is its only consumer, and failing the job on an
+ # unset URL would block PRs on an upload nobody in the PR can perform.
+ - name: Fetch real-block fixture
+ id: fixture
+ run: |
+ if [ -z "$(make -s print-real-block-fixture-url)" ]; then
+ echo "::warning::ETHREX_REAL_BLOCK_FIXTURE_URL is unset — skipping the real-block usability screen. Set it in the Makefile once the .bin is hosted."
+ echo "present=false" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+ make ethrex-real-block-fixture
+ echo "present=true" >> "$GITHUB_OUTPUT"
+
+ # `test_ethrex_real_block_native` is the screen that makes a block USABLE rather
+ # than merely realistic: this crate links no KZG backend (pinned by
+ # `no_kzg_backend_linked`, which runs in pr_main.yaml), so a block calling point
+ # evaluation (0x0a) diverges from consensus and fails here instead of silently
+ # passing and then failing in the guest.
+ #
+ # `test_ethrex_real_block_vm` stays excluded: it drives the block through the
+ # guest ELF, needs the RV64 toolchain, and its runtime is unmeasured.
+ - name: Screen the block against the guest's precompile surface
+ if: steps.fixture.outputs.present == 'true'
+ run: |
+ cd tooling/ethrex-tests && \
+ cargo test --release test_ethrex_real_block_native -- --include-ignored
diff --git a/.github/workflows/pr_main.yaml b/.github/workflows/pr_main.yaml
index a4554fda2..5d560a63a 100644
--- a/.github/workflows/pr_main.yaml
+++ b/.github/workflows/pr_main.yaml
@@ -56,6 +56,18 @@ jobs:
with:
shared-key: "lambda-vm-test"
cache-all-crates: "true"
+ # ethrex-tests is a detached workspace (own Cargo.lock, own target dir), so
+ # the default `. -> target` misses it and its release tree would rebuild
+ # from scratch every run. `cache-all-crates` only covers the registry, not
+ # compiled artifacts.
+ #
+ # ethrex-block-converter is deliberately NOT here: this job no longer builds it.
+ # That workspace is the expensive one (~335 packages, including the blst,
+ # c-kzg and secp256k1-sys C builds, malachite and ark-ff/asm) and it now
+ # builds only in ethrex-block-converter.yml, which carries its own cache entry.
+ workspaces: |
+ . -> target
+ tooling/ethrex-tests -> target
- name: Cache compiled ASM ELF artifacts
id: cache-asm-elfs
@@ -107,9 +119,24 @@ jobs:
# them from that directory to use its isolated Cargo.lock. The guest ELF
# and committed fixtures are already present from the steps above.
# --include-ignored also runs the heavier synthetic-block test.
+ #
+ # `--skip test_ethrex_real_block` is a substring match, so it drops BOTH
+ # real-block tests (the `_vm` and `_native` ones) and with them this job's need
+ # for the real-block fixture. That fixture is a benchmark input: it has to be
+ # right once, not on every PR, and fetching plus screening it here put a network
+ # download and a ~335-package build in the required gate for an artifact with no
+ # bearing on correctness. Both moved to ethrex-block-converter.yml, which runs when
+ # the converter or the block pin actually changes. `no_kzg_backend_linked` still
+ # runs here — it is a pure unit test, and it is the property the real-block
+ # screen over there depends on.
+ #
+ # This is `make test-ethrex-offline` spelled out, matching the other test steps
+ # in this job: they all bypass make so its dependency check cannot decide the
+ # cache-restored ELFs are stale and trigger a rebuild.
- name: Run ethrex host-reference tests (detached workspace)
run: |
- cd tooling/ethrex-tests && cargo test --release -- --include-ignored
+ cd tooling/ethrex-tests && \
+ cargo test --release -- --include-ignored --skip test_ethrex_real_block
test-cli:
name: CLI tests
diff --git a/Makefile b/Makefile
index 4abed4a90..a3471bdb4 100644
--- a/Makefile
+++ b/Makefile
@@ -1,12 +1,15 @@
.PHONY: deps deps-linux deps-macos compile-programs-asm compile-programs-rust compile-bench \
compile-programs compile-recursion-elfs clean-asm clean-rust clean-bench clean-shared \
clean-recursion-elfs clean test test-asm \
-test-rust test-ethrex test-executor test-syscalls test-flamegraph flamegraph-prover test-profile-recursion test-profile-recursion-single test-profile-recursion-multi \
+test-rust test-ethrex test-ethrex-offline test-executor test-syscalls test-flamegraph flamegraph-prover test-profile-recursion test-profile-recursion-single test-profile-recursion-multi \
test-profile-recursion-block recursion-profile-block-input \
test-fast test-prover test-prover-all test-prover-debug test-disk-spill test-math-cuda test-cuda-integration test-cuda-fallback \
test-prover-cuda test-prover-comprehensive-cuda \
bench-math-cuda bench-prover bench-prover-cuda build check clippy fmt lint regen-ethrex-fixtures \
-update-ethrex-fixture-checksums check-ethrex-fixture-checksums
+update-ethrex-fixture-checksums check-ethrex-fixture-checksums ethrex-real-block-fixture \
+ethrex-real-block-cache ethrex-real-block-converter-cache print-real-block-fixture \
+print-real-block-fixture-url \
+test-ethrex-real-block-converter regen-real-block-fixture
UNAME := $(shell uname)
@@ -271,10 +274,187 @@ test-asm: compile-programs-asm
test-rust: compile-programs-rust
cargo test -p executor --test rust
+# ===== Real block: the benchmark workload =====
+#
+# A genuine Ethereum block, as opposed to the synthetic N-plain-transfer blocks
+# from tooling/ethrex-fixtures. Two artifacts, both gitignored and both FETCHED
+# rather than built:
+#
+# the fixture the rkyv ProgramInput the benchmarks prove (~1 MB)
+# the cache the ethrex-replay JSON it was converted from (~2 MB), read only
+# by the converter's tests and by `regen-real-block-fixture`
+#
+# Fetching a verified binary is the same contract as prepare-sysroot above, and it
+# keeps the converter, the ~335-package ethrex host dependency tree and an
+# ethrex-replay `rev` pin off the path of everyone who just wants to run a
+# benchmark. It also decouples the block from what upstream happens to host:
+# ethrex-replay publishes a cache for Hoodi and nothing else, so any mainnet block
+# is unreachable by the convert-locally route (its cache takes ~4 minutes and ~700
+# calls against an archive RPC to produce) and trivial by this one.
+#
+# The converter still exists and is still tested — see "Real-block converter"
+# below. It is a regeneration tool for ethrex rev bumps, not a build step.
+#
+# ---- Repointing to a different block ----
+# These FIVE lines and nothing else. Every path below derives from them, the
+# benchmark scripts and CI resolve the fixture through
+# `make -s print-real-block-fixture`, and no workflow, script or env var anywhere
+# names a block. Outside this file the repoint touches only REAL_BLOCK_FIXTURE in
+# tooling/ethrex-tests, which points the usability screen at the block actually
+# being proven. The converter's own pins do NOT move — see below.
+# tooling/ethrex-block-converter/README.md carries the procedure and each candidate's
+# measured cost.
+ETHREX_REAL_BLOCK_NETWORK := mainnet
+ETHREX_REAL_BLOCK := 25368371
+ETHREX_REAL_BLOCK_FIXTURE_URL := https://github.com/yetanotherco/lambda_vm/releases/download/bench-fixtures-v1/ethrex_mainnet_25368371.bin
+ETHREX_REAL_BLOCK_FIXTURE_SHA256 := 61eba49b6b254f4a05def5a47b08a21ae3eee56f0d37bcd7b3a24b0cc1e4a300
+# The block's source cache, hosted in the same release. Only `regen-real-block-fixture`
+# reads it — the converter's TESTS use a different, upstream-pinned cache (below).
+ETHREX_REAL_BLOCK_CACHE_URL := https://github.com/yetanotherco/lambda_vm/releases/download/bench-fixtures-v1/cache_mainnet_25368371.json
+ETHREX_REAL_BLOCK_CACHE_SHA256 := 7aa88a5f7c5755b7575870f95e6c5c26186947f5e9e0d52199148c74e2a2736b
+
+ETHREX_REAL_BLOCK_ID := $(ETHREX_REAL_BLOCK_NETWORK)_$(ETHREX_REAL_BLOCK)
+ETHREX_REAL_BLOCK_FIXTURE := executor/tests/ethrex_$(ETHREX_REAL_BLOCK_ID).bin
+ETHREX_REAL_BLOCK_CACHE := tooling/ethrex-block-converter/caches/cache_$(ETHREX_REAL_BLOCK_ID).json
+
+# $(call ensure_verified,url,sha256,dest,label,url-var-name)
+#
+# Guard-then-fetch, the same shape as prepare-sysroot above: the digest of whatever
+# is already on disk is checked on EVERY invocation, so this catches the three ways
+# a wrong artifact gets there — a stale copy from before a re-upload under the same
+# block number, a corrupted file, and a hand-placed one — not just an absent file.
+# That is why these are phony targets rather than file rules: a file rule does not
+# run when its target exists, which is exactly the case that needs checking.
+# (It replaces the ethrex-replay rev stamp, which guarded the same class of
+# staleness for the one input that used to be pinned by rev.)
+#
+# On a miss it downloads to a temp file and verifies BEFORE moving into place, so an
+# interrupted or corrupted transfer cannot leave a file that later reads as valid and
+# silently changes what every benchmark measures. The "neither sha256sum nor shasum"
+# case is a hard error, as in prepare-sysroot — a skipped check would defeat the
+# point of fetching a binary at all.
+define ensure_verified
+ @set -e; \
+ dest="$(3)"; want="$(2)"; \
+ if command -v sha256sum >/dev/null 2>&1; then shacmd="sha256sum"; \
+ elif command -v shasum >/dev/null 2>&1; then shacmd="shasum -a 256"; \
+ else echo "$(4): missing sha256sum or shasum for checksum verification" >&2; exit 1; fi; \
+ sha_of() { $$shacmd "$$1" | awk '{print $$1}'; }; \
+ if [ -f "$$dest" ] && [ "$$(sha_of "$$dest")" = "$$want" ]; then \
+ exit 0; \
+ fi; \
+ if [ -f "$$dest" ]; then \
+ echo "$(4) $$dest does not match $$want - refetching."; \
+ fi; \
+ if [ -z "$(1)" ]; then \
+ echo "$(4): $(5) is unset." >&2; \
+ echo " The $(ETHREX_REAL_BLOCK_ID) $(4) is fetched, not built. Set $(5) in the" >&2; \
+ echo " Makefile to wherever the artifact is hosted; see" >&2; \
+ echo " tooling/ethrex-block-converter/README.md for how to produce and host one." >&2; \
+ exit 1; \
+ fi; \
+ mkdir -p $(dir $(3)); \
+ tmp="$$dest.tmp"; \
+ cleanup() { rm -f "$$tmp"; }; \
+ trap 'cleanup' EXIT; \
+ trap 'cleanup; exit 130' INT; \
+ trap 'cleanup; exit 143' TERM; \
+ echo "Downloading $(4) $(ETHREX_REAL_BLOCK_ID)..."; \
+ curl -fL --proto '=https' --retry 3 --retry-delay 2 --retry-all-errors "$(1)" -o "$$tmp"; \
+ echo "Verifying $(4) checksum..."; \
+ if [ "$$(sha_of "$$tmp")" != "$$want" ]; then \
+ echo "$(4): checksum mismatch for $(1)" >&2; \
+ exit 1; \
+ fi; \
+ mv "$$tmp" "$$dest"; \
+ trap - EXIT
+endef
+
+ethrex-real-block-fixture:
+ $(call ensure_verified,$(ETHREX_REAL_BLOCK_FIXTURE_URL),$(ETHREX_REAL_BLOCK_FIXTURE_SHA256),$(ETHREX_REAL_BLOCK_FIXTURE),fixture,ETHREX_REAL_BLOCK_FIXTURE_URL)
+
+ethrex-real-block-cache:
+ $(call ensure_verified,$(ETHREX_REAL_BLOCK_CACHE_URL),$(ETHREX_REAL_BLOCK_CACHE_SHA256),$(ETHREX_REAL_BLOCK_CACHE),cache,ETHREX_REAL_BLOCK_CACHE_URL)
+
+# Single source of truth for the benchmark tooling. scripts/bench_verify.sh,
+# scripts/perf_diff.sh and .github/workflows/benchmark-pr.yml read the fixture
+# path from here instead of hardcoding it, so repointing the block above moves
+# every benchmark at once. `-s` on the caller's side keeps the output clean.
+print-real-block-fixture:
+ @echo $(ETHREX_REAL_BLOCK_FIXTURE)
+
+# Lets CI ask "is the fixture hosted yet?" without parsing the Makefile. Prints
+# nothing while the URL is unset, which is the condition callers branch on.
+print-real-block-fixture-url:
+ @echo $(ETHREX_REAL_BLOCK_FIXTURE_URL)
+
+# ===== Real-block converter (regeneration tool, off the build path) =====
+#
+# Only needed when the guest's ethrex `rev` moves and the fixture has to be
+# rebuilt, or when validating a candidate block. Nothing in the benchmark or test
+# path builds this crate.
+#
+# Its TEST input is pinned to Hoodi 1265656, independently of whichever block the
+# benchmarks currently prove, and stays there across a repoint. What these tests
+# exercise is the CONVERSION — cache JSON in, correctly-laid-out rkyv out — which
+# any real block demonstrates equally well. Hoodi's is the one cache ethrex-replay
+# publishes, so pinning there costs us no hosting, cannot drift, and leaves the
+# benchmark block free to change without touching this crate.
+#
+# Pinned by immutable `rev`, as the guest pins ethrex itself: a branch ref would let
+# the converter's reproducibility digest drift under a fixed input.
+ETHREX_REPLAY_REV := 2693e0182a8734117151d8ea2891eda5afc60383
+ETHREX_CONVERTER_TEST_BLOCK := hoodi_1265656
+ETHREX_CONVERTER_CACHE := tooling/ethrex-block-converter/caches/cache_$(ETHREX_CONVERTER_TEST_BLOCK).json
+# The cache filename is keyed on the block only, and its download rule has no other
+# prerequisite, so make would treat an already-present cache as up to date across an
+# `ETHREX_REPLAY_REV` bump and silently keep reading the old input. Depending on a
+# rev-stamped marker makes a re-pin discard the stale cache; without it the mismatch
+# only surfaces downstream as a `conversion_is_reproducible` digest failure, which
+# reads as "regenerate the fixture" and points at the wrong thing.
+ETHREX_REPLAY_REV_STAMP := tooling/ethrex-block-converter/caches/.replay-rev-$(ETHREX_REPLAY_REV)
+
+$(ETHREX_REPLAY_REV_STAMP):
+ mkdir -p $(dir $@)
+ rm -f $(ETHREX_CONVERTER_CACHE) $(dir $@).replay-rev-*
+ touch $@
+
+$(ETHREX_CONVERTER_CACHE): $(ETHREX_REPLAY_REV_STAMP)
+ mkdir -p $(dir $@)
+ curl -fsSL --retry 3 --retry-delay 2 --retry-all-errors -o $@.tmp \
+ https://raw.githubusercontent.com/lambdaclass/ethrex-replay/$(ETHREX_REPLAY_REV)/caches/cache_$(ETHREX_CONVERTER_TEST_BLOCK).json
+ mv $@.tmp $@
+
+ethrex-real-block-converter-cache: $(ETHREX_CONVERTER_CACHE)
+
+# Converter correctness: host-side parity through the guest's own Crypto impl, the
+# network-rejection guard, and the reproducibility digest. Runs on changes to the
+# converter (see .github/workflows/ethrex-block-converter.yml), not on every PR.
+test-ethrex-real-block-converter: $(ETHREX_CONVERTER_CACHE)
+ cd tooling/ethrex-block-converter && cargo test --release
+
+# Manual regeneration of the BENCHMARK fixture (not the converter's test block):
+# fetches that block's own cache and re-converts it, overwriting the fixture in
+# place so you can hash the result and upload it. That upload, plus SHA256/URL at
+# the top, is how the fixture is actually replaced.
+regen-real-block-fixture: ethrex-real-block-cache
+ cd tooling/ethrex-block-converter && \
+ cargo run --release -- ../../$(ETHREX_REAL_BLOCK_CACHE) ../../$(ETHREX_REAL_BLOCK_FIXTURE)
+
# ethrex host-reference tests live in the detached `tooling/ethrex-tests`
# workspace (ethrex pins rkyv's `unaligned` feature; isolated Cargo.lock).
-test-ethrex: compile-programs-rust
- cd tooling/ethrex-tests && cargo test --release -- --include-ignored
+# Needs the real-block fixture, so it needs the fixture URL to be set; CI runs the
+# `-offline` variant below in the PR gate and this one only in ethrex-block-converter.yml.
+test-ethrex: compile-programs-rust ethrex-real-block-fixture
+ cd tooling/ethrex-tests && cargo test --release -- --include-ignored --skip test_ethrex_real_block_vm
+
+# Offline variant: no network, and what the PR gate runs. `--skip
+# test_ethrex_real_block` is a substring match, so it drops both real-block tests —
+# the `_vm` one and the `_native` one, which reads the fetched fixture and would
+# otherwise fail on a clean checkout. The committed synthetic fixtures and
+# `no_kzg_backend_linked` still run.
+test-ethrex-offline: compile-programs-rust
+ cd tooling/ethrex-tests && cargo test --release -- --include-ignored --skip test_ethrex_real_block
test-flamegraph:
cargo test -p executor --test flamegraph
diff --git a/bench_vs/run_ethrex.sh b/bench_vs/run_ethrex.sh
index 4438e1c26..b323d5866 100755
--- a/bench_vs/run_ethrex.sh
+++ b/bench_vs/run_ethrex.sh
@@ -33,10 +33,12 @@ NC='\033[0m'
BLOCKS=(
"ethrex empty block|ethrex_empty_block.bin"
"ethrex 1 tx|ethrex_simple_tx.bin"
- # ethrex_10_transfers.bin (~42M cycles) executes fine but is too heavy to
- # prove on a typical machine (OOMs ~36 GB) — software ecrecover dominates
- # (~4M cycles/transfer). Kept as a fixture; add here once ecrecover is a
- # precompile or for big-memory/nightly proving.
+ # ethrex_10_transfers.bin is 6.8M cycles (measured), NOT the ~42M this comment
+ # used to claim: that figure — and the "OOMs ~36 GB, software ecrecover
+ # dominates at ~4M cycles/transfer" reasoning built on it — predates ecrecover
+ # becoming an ECSM accelerator, which cut it ~6x. It is no longer too heavy to
+ # prove. Left out only because the two blocks above already cover the cheap
+ # end; add it if a mid-size point is wanted.
)
# --- Parse args -------------------------------------------------------------
diff --git a/bin/cli/src/main.rs b/bin/cli/src/main.rs
index 1de36220b..a04e920db 100644
--- a/bin/cli/src/main.rs
+++ b/bin/cli/src/main.rs
@@ -790,6 +790,12 @@ fn cmd_prove_continuation(
eprintln!(
"Generating continuation proof (blowup={blowup}, epoch_size_log2={epoch_size_log2}, epoch_size={epoch_size})...",
);
+ // Same tracker as the monolithic path: the peak here is the flat per-epoch
+ // working set rather than a whole-trace high-water mark, and it is the metric
+ // that decides which epoch size a machine can run, so the benchmarks need it
+ // reported identically on both paths.
+ #[cfg(feature = "jemalloc-stats")]
+ let tracker = heap_tracker::HeapTracker::start();
let start = Instant::now();
let bundle = match prover::continuation::prove_continuation(
&elf_data,
@@ -834,6 +840,11 @@ fn cmd_prove_continuation(
if time {
println!("Proving time: {:.3}s", prove_elapsed.as_secs_f64());
}
+ #[cfg(feature = "jemalloc-stats")]
+ {
+ let peak_bytes = tracker.stop();
+ println!("Peak heap: {} MB", peak_bytes / (1024 * 1024));
+ }
ExitCode::SUCCESS
}
diff --git a/executor/.gitignore b/executor/.gitignore
index 17f7497d7..1229bde5c 100644
--- a/executor/.gitignore
+++ b/executor/.gitignore
@@ -1,6 +1,13 @@
/target
/program_artifacts/rust
-/tests/ethrex_hoodi.bin
+# Real-block fixtures (~1 MB): generated by `make ethrex-real-block-fixture`
+# from an ethrex-replay cache, never committed. See tooling/ethrex-block-converter.
+# One pattern per network the converter accepts (mainnet/hoodi/sepolia — it
+# rejects anything else), so repointing ETHREX_REAL_BLOCK_NETWORK in the Makefile
+# cannot silently make a ~1 MB fixture committable.
+/tests/ethrex_mainnet_*.bin
+/tests/ethrex_hoodi*.bin
+/tests/ethrex_sepolia_*.bin
/tests/ethrex_bench_*.bin
# _4 is committed (~17 KB): used by `make recursion-profile-block-input` and
# scripts/bench_recursion_scaling.sh. Other sizes stay ignored — scaling.sh
diff --git a/executor/tests/README.md b/executor/tests/README.md
index 39fd13813..80c16d4eb 100644
--- a/executor/tests/README.md
+++ b/executor/tests/README.md
@@ -52,3 +52,13 @@ ethrex_10_transfers.bin
sha256: 38901ee4d40b99cf0aa7f642a92f0fc8db76d974bf43033a1673839020c3c28e
contents: stateless ethrex block with ten plain ETH transfer transactions
```
+
+## Real-block fixtures
+
+The blocks above are synthetic (N plain ETH transfers over a small genesis).
+For a representative workload — real contract execution, real trie depth, real
+bytecode — `make ethrex-real-block-fixture` generates
+`ethrex_hoodi_1265656.bin` from an ethrex-replay cache. It is ~1 MB, so it is
+gitignored and generated on demand rather than committed, and its checksum is
+pinned in `tooling/ethrex-block-converter/README.md` rather than above (the
+checksum script only covers committed fixtures).
diff --git a/scripts/bench_abba.sh b/scripts/bench_abba.sh
index fd0e7ad3e..061c6e407 100755
--- a/scripts/bench_abba.sh
+++ b/scripts/bench_abba.sh
@@ -10,8 +10,9 @@
# than an unpaired two-sample test.
#
# WHAT IT DOES:
-# 1. Builds the ethrex guest ELF + 5-transfer fixture once (identical for both
-# sides — a prover-only change doesn't touch the guest).
+# 1. Builds the ethrex guest ELF and obtains the workload fixture once (identical
+# for both sides — a prover-only change doesn't touch the guest). Which fixture
+# is WORKLOAD's business; see below.
# 2. Builds the `cli` prover at REF_A and REF_B (skips the build and reuses the
# cached binaries if they already exist; set REBUILD=1 to force).
# 3. Runs N_PAIRS interleaved pairs in A B B A ... order (alternating which side
@@ -35,6 +36,16 @@
# EPOCH_SIZE_LOG2= continuation epoch size (default 20; min 18).
# TX_COUNT= ethrex transfer fixture to prove (default 5; use 20 for a
# large continuation trace where GPU-residency wins are visible).
+# Ignored when WORKLOAD=real.
+# WORKLOAD=synthetic|real (default synthetic) which block to prove. `real`
+# fetches the real-block fixture (block identity lives in the Makefile) and
+# forces --continuations; TX_COUNT and CONTINUATIONS do not apply to it.
+#
+# On WORKLOAD: the synthetic default is N plain transfers — ecrecover-heavy over a
+# near-empty state — while a real block is keccak- and trie-bound, so a prover change
+# can move the two in opposite directions. `real` is what benchmark-gpu.yml runs by
+# default; `synthetic` stays the default HERE so CPU /bench-abba results remain
+# comparable with everything recorded before.
#
# Sizing (ethrex pair-noise sd ~1.2%, 80% power): ~12 pairs for a 1% effect,
# ~18 for 0.8%, ~32 for 0.6%. Default 20 -> solid on 0.8-1%, ~60% power at 0.6%
@@ -65,6 +76,18 @@ CONTINUATIONS="${CONTINUATIONS:-0}"
EPOCH_SIZE_LOG2="${EPOCH_SIZE_LOG2:-20}"
# ethrex transfer-count fixture to prove (executor/tests/ethrex_${TX_COUNT}_transfers.bin).
TX_COUNT="${TX_COUNT:-5}"
+WORKLOAD="${WORKLOAD:-synthetic}"
+case "$WORKLOAD" in
+ synthetic|real) ;;
+ *) echo "ERROR: WORKLOAD must be 'synthetic' or 'real' (got '$WORKLOAD')." >&2; exit 2 ;;
+esac
+# A real block sits far past the monolithic memory ceiling (~4.9 GB of peak heap per
+# million cycles puts it in the hundreds of GB), so continuations are forced rather
+# than offered: CONTINUATIONS=0 here could only produce an OOM.
+if [ "$WORKLOAD" = "real" ] && [ "$CONTINUATIONS" != "1" ]; then
+ echo " NOTE: WORKLOAD=real always proves with continuations (monolithic would OOM)."
+ CONTINUATIONS=1
+fi
if [ "$CONTINUATIONS" = "1" ]; then
CONT_ARGS="--continuations --epoch-size-log2 $EPOCH_SIZE_LOG2"
else
@@ -72,7 +95,8 @@ else
fi
ELF_REL="executor/program_artifacts/rust/ethrex.elf"
-INPUT_REL="executor/tests/ethrex_${TX_COUNT}_transfers.bin"
+# Resolved after the cd to the repo root (WORKLOAD=real reads it from the Makefile).
+INPUT_REL=""
WORK="/tmp/abba_run"
WT="/tmp/abba_wt"
PROOF="/tmp/abba_proof.bin"
@@ -94,6 +118,16 @@ if [ $((N_PAIRS % 2)) -ne 0 ]; then
fi
echo " pairs=$N_PAIRS (=$((N_PAIRS * 2)) prove runs)"
+# The real block's identity lives in the Makefile and nowhere else, so repointing it
+# never needs an edit here.
+if [ "$WORKLOAD" = "real" ]; then
+ INPUT_REL="$(make -s print-real-block-fixture)"
+ echo " workload=real $INPUT_REL (continuations, epoch 2^$EPOCH_SIZE_LOG2)"
+else
+ INPUT_REL="executor/tests/ethrex_${TX_COUNT}_transfers.bin"
+ echo " workload=synthetic ${TX_COUNT}tx $INPUT_REL"
+fi
+
mkdir -p "$WORK"
# --- 1. Guest ELF + fixture (identical for both sides; build once if missing) ---
@@ -103,9 +137,17 @@ if [ ! -f "$ELF_REL" ]; then
make "$ELF_REL"
fi
if [ ! -f "$INPUT_REL" ]; then
- echo "==> Generating ethrex ${TX_COUNT}-transfer fixture (missing)"
- ( cd tooling/ethrex-fixtures && cargo build --release )
- tooling/ethrex-fixtures/target/release/ethrex-fixtures "$TX_COUNT" "$INPUT_REL" distinct
+ if [ "$WORKLOAD" = "real" ]; then
+ # ~1 MB, gitignored, never in a fresh checkout — and a rented GPU box is always a
+ # fresh checkout. Fetched by URL + sha256, not built: no converter, no ethrex host
+ # dependency tree, so this costs seconds on the box.
+ echo "==> Fetching ethrex real-block fixture (missing)"
+ make ethrex-real-block-fixture
+ else
+ echo "==> Generating ethrex ${TX_COUNT}-transfer fixture (missing)"
+ ( cd tooling/ethrex-fixtures && cargo build --release )
+ tooling/ethrex-fixtures/target/release/ethrex-fixtures "$TX_COUNT" "$INPUT_REL" distinct
+ fi
fi
ELF="$(cd "$(dirname "$ELF_REL")" && pwd)/$(basename "$ELF_REL")"
INPUT="$(cd "$(dirname "$INPUT_REL")" && pwd)/$(basename "$INPUT_REL")"
diff --git a/scripts/bench_verify.sh b/scripts/bench_verify.sh
index 33c1c1464..068b4c864 100755
--- a/scripts/bench_verify.sh
+++ b/scripts/bench_verify.sh
@@ -27,7 +27,15 @@
# below 6: the exact Wilcoxon's smallest attainable two-sided p is 2/2^n, so at
# n=4 it is 0.125 and the arm can only ever report BORDERLINE, however large and
# clean the effect.
-# CONT_EPOCH_LOG2= continuation epoch size (default 20, min 18). 20 matches
+# WORKLOAD=synthetic|real (default synthetic) which BLOCK both arms prove.
+# `real` fetches the real-block fixture (identity lives in the Makefile) and runs
+# the continuation arm ONLY — a real block is hundreds of GB monolithically, so
+# that arm is skipped rather than left to OOM. See "Workload" below.
+# CONT_EPOCH_LOG2= continuation epoch size (default 20, min 18). For
+# WORKLOAD=real prefer the calibrated tier for the box you are on — 2^22 on the
+# bench runner or a 64 GiB machine, 2^23 on a 128 GiB one (see
+# tooling/ethrex-block-converter/README.md, "Choosing the epoch size"); the default
+# below is chosen for the SYNTHETIC arm. 20 matches
# scripts/bench_abba.sh, so this arm proves the same bundle shape /bench already
# proves on the same server — and 20 txs at 2^20 is strictly cheaper than the
# 100 txs at 2^20 that /bench runs by default, so it can't be the thing that OOMs
@@ -36,6 +44,17 @@
# bench_recursion_cycles.sh's BLOCK_EPOCH_LOG2=21: that arm needs FEW epochs so
# the bundle fits the guest's 512 MiB private-input cap, a constraint that
# doesn't apply to host-side verification.
+#
+# Workload. What this script measures is VERIFY cost, and verify cost is structural in
+# the proof — table mix, trace lengths, query count — so the block decides what is being
+# measured, not the loop around it. The synthetic default is 20 plain transfers:
+# ecrecover-heavy over a near-empty state. A real block inverts that mix (keccak- and
+# trie-bound), so a verifier change can move the two differently.
+#
+# `synthetic` stays the default because it is what `/bench-verify` runs and what every
+# number recorded so far used; `real` is the representative one, and costs a ~4.5-4.8
+# min continuation prove per side (cached in $WORK afterwards) before any verify run.
+# Both sides always prove the same block, so a comparison is never mixed.
set -euo pipefail
@@ -50,9 +69,22 @@ N_PAIRS="${3:-20}"
BENCH_FEATURES="${BENCH_FEATURES:-jemalloc-stats}"
CONT_PAIRS="${CONT_PAIRS:-8}"
CONT_EPOCH_LOG2="${CONT_EPOCH_LOG2:-20}"
+WORKLOAD="${WORKLOAD:-synthetic}"
+case "$WORKLOAD" in
+ synthetic|real) ;;
+ *) echo "ERROR: WORKLOAD must be 'synthetic' or 'real' (got '$WORKLOAD')." >&2; exit 2 ;;
+esac
+# WORKLOAD=real skips the monolithic arm, so CONT_PAIRS=0 on top of it would build both
+# binaries, prove nothing and report nothing. Fail before the ~30-min build rather than
+# after it.
+if [ "$WORKLOAD" = "real" ] && [ "${CONT_PAIRS}" -eq 0 ] 2>/dev/null; then
+ echo "ERROR: WORKLOAD=real runs the continuation arm only, so CONT_PAIRS=0 would measure nothing." >&2
+ exit 2
+fi
ELF_REL="executor/program_artifacts/rust/ethrex.elf"
-INPUT_REL="executor/tests/ethrex_bench_20.bin"
+# Resolved after the cd to the repo root (WORKLOAD=real reads it from the Makefile).
+INPUT_REL=""
WORK="/tmp/verify_run"
WT="/tmp/verify_wt"
PROOF_B="$WORK/proof_b.bin" # baseline's proof (cached in $WORK, keyed like the binaries)
@@ -113,7 +145,26 @@ fi
if [ $((CONT_PAIRS % 2)) -ne 0 ]; then
echo " WARNING: CONT_PAIRS=$CONT_PAIRS is odd; use an even count so AB/BA orders balance."
fi
-echo " pairs=$N_PAIRS (=$((N_PAIRS * 2)) verify runs)"
+# The real block's identity lives in the Makefile and nowhere else, so repointing it
+# never needs an edit here. A real block cannot be proven monolithically (~4.9 GB of peak
+# heap per million cycles puts it in the hundreds of GB), so that arm is skipped outright
+# rather than left to OOM halfway through a rented or shared machine's run.
+RUN_MONO=1
+if [ "$WORKLOAD" = "real" ]; then
+ INPUT_REL="$(make -s print-real-block-fixture)"
+ RUN_MONO=0
+ BLOCK_LABEL="ethrex real block $(basename "$INPUT_REL")"
+else
+ INPUT_REL="executor/tests/ethrex_bench_20.bin"
+ BLOCK_LABEL="ethrex 20-tx block"
+fi
+
+echo " workload=$WORKLOAD $INPUT_REL"
+if [ "$RUN_MONO" = "1" ]; then
+ echo " pairs=$N_PAIRS (=$((N_PAIRS * 2)) verify runs)"
+else
+ echo " monolithic arm SKIPPED (a real block exceeds the monolithic memory ceiling)"
+fi
echo " continuation pairs=$CONT_PAIRS epoch=2^$CONT_EPOCH_LOG2"
mkdir -p "$WORK"
@@ -129,9 +180,16 @@ if [ ! -f "$ELF_REL" ]; then
make "$ELF_REL"
fi
if [ ! -f "$INPUT_REL" ]; then
- echo "==> Generating ethrex 20-transfer fixture (missing)"
- ( cd tooling/ethrex-fixtures && cargo build --release )
- tooling/ethrex-fixtures/target/release/ethrex-fixtures 20 "$INPUT_REL" distinct
+ if [ "$WORKLOAD" = "real" ]; then
+ # ~1 MB, gitignored, never in a fresh checkout. Fetched by URL + sha256, not built:
+ # no converter and no ethrex host dependency tree on this path.
+ echo "==> Fetching ethrex real-block fixture (missing)"
+ make ethrex-real-block-fixture
+ else
+ echo "==> Generating ethrex 20-transfer fixture (missing)"
+ ( cd tooling/ethrex-fixtures && cargo build --release )
+ tooling/ethrex-fixtures/target/release/ethrex-fixtures 20 "$INPUT_REL" distinct
+ fi
fi
ELF="$(cd "$(dirname "$ELF_REL")" && pwd)/$(basename "$ELF_REL")"
INPUT="$(cd "$(dirname "$INPUT_REL")" && pwd)/$(basename "$INPUT_REL")"
@@ -245,12 +303,15 @@ prove_cached() { # $1=binary $2=proof-path $3=sha $4...=extra prove args
if [ -n "$ep" ]; then printf '%s\n' "$ep" > "$out.epochs"; fi
echo "$marker" > "$out.sha"
}
-prove_cached "$WORK/cli_B" "$PROOF_B" "$SHA_B" || exit 1
-prove_cached "$WORK/cli_A" "$PROOF_A" "$SHA_A" || exit 1
+SIZE_B=0; SIZE_A=0
+if [ "$RUN_MONO" = "1" ]; then
+ prove_cached "$WORK/cli_B" "$PROOF_B" "$SHA_B" || exit 1
+ prove_cached "$WORK/cli_A" "$PROOF_A" "$SHA_A" || exit 1
-# Proof sizes (bytes) for the Proof size row.
-SIZE_B="$(wc -c < "$PROOF_B" | tr -d '[:space:]')"
-SIZE_A="$(wc -c < "$PROOF_A" | tr -d '[:space:]')"
+ # Proof sizes (bytes) for the Proof size row.
+ SIZE_B="$(wc -c < "$PROOF_B" | tr -d '[:space:]')"
+ SIZE_A="$(wc -c < "$PROOF_A" | tr -d '[:space:]')"
+fi
# Decide whether both sides can VERIFY one shared proof (baseline's) — tightest
# timing precision — or must verify their own. In auto mode, distinguish a real
@@ -454,10 +515,15 @@ else:
PY
}
-decide_mode "$PROOF_B" "$PROOF_A"
-run_abba "$N_PAIRS" "$WORK/pairs.csv" || exit 1
-MONO_MODE="$MODE"
-MONO_NOTE="$PER_SIDE_NOTE"
+MONO_SKIP=""
+if [ "$RUN_MONO" = "1" ]; then
+ decide_mode "$PROOF_B" "$PROOF_A"
+ run_abba "$N_PAIRS" "$WORK/pairs.csv" || exit 1
+ MONO_MODE="$MODE"
+ MONO_NOTE="$PER_SIDE_NOTE"
+else
+ MONO_SKIP="not run: a real block exceeds the monolithic memory ceiling, so only the continuation arm below applies"
+fi
# Render the monolithic report NOW, not at the end with the continuation one. Both are
# still emitted together at the end (the extractor needs them contiguous after the
# anchor), but computing this one here means it also survives the run dying during the
@@ -465,9 +531,13 @@ MONO_NOTE="$PER_SIDE_NOTE"
# step timeout or an OOM kill takes the whole process down, and CI would then post
# "Run failed" and throw away a monolithic verdict it had already measured. bench-verify.yml
# falls back to this file in that case.
-MONO_TITLE="ethrex 20-tx block · monolithic · blowup=2, 219 queries"
-MONO_REPORT="$(print_stats "$WORK/pairs.csv" "$MONO_TITLE" \
- "$SIZE_A" "$SIZE_B" "$MONO_MODE" "$MONO_NOTE")"
+MONO_TITLE="$BLOCK_LABEL · monolithic · blowup=2, 219 queries"
+if [ -n "$MONO_SKIP" ]; then
+ MONO_REPORT="$(printf '#### %s\n\n_(Monolithic arm %s.)_\n' "$MONO_TITLE" "$MONO_SKIP")"
+else
+ MONO_REPORT="$(print_stats "$WORK/pairs.csv" "$MONO_TITLE" \
+ "$SIZE_A" "$SIZE_B" "$MONO_MODE" "$MONO_NOTE")"
+fi
printf '%s\n' "$MONO_REPORT" > "$WORK/result_mono.txt"
# --- 3b. Same measurement over a CONTINUATION bundle of the same block ---------
@@ -525,7 +595,7 @@ if [ -z "$CONT_SKIP" ]; then
fi
fi
fi
-CONT_TITLE="ethrex 20-tx block · continuations, epoch 2^$CONT_EPOCH_LOG2$CONT_EPOCHS · blowup=2, 219 queries"
+CONT_TITLE="$BLOCK_LABEL · continuations, epoch 2^$CONT_EPOCH_LOG2$CONT_EPOCHS · blowup=2, 219 queries"
printf '%s\n' "$MONO_REPORT"
if [ -n "$CONT_SKIP" ]; then
echo
diff --git a/scripts/perf_diff.sh b/scripts/perf_diff.sh
index ddadf53fd..a6c0380be 100755
--- a/scripts/perf_diff.sh
+++ b/scripts/perf_diff.sh
@@ -13,6 +13,23 @@
#
# USAGE (on the bench server):
# scripts/perf_diff.sh REF_A [REF_B=origin/main]
+# Env: WORKLOAD=synthetic|real (default synthetic) picks the block to profile;
+# EPOCH_SIZE_LOG2= (default 22) sizes the epoch, WORKLOAD=real only.
+# 22 is the calibrated bench-runner tier, matching /bench; use 23 on a
+# 128 GiB box (tooling/ethrex-block-converter/README.md, "Choosing the epoch size").
+#
+# Pick the workload that matches the run you are localizing, because the symbol
+# mix follows the block: the synthetic default is 20 plain transfers (8.73M cycles,
+# 411 keccak calls, 80 ecsm calls) and a real block inverts that (50.78M cycles,
+# 10,478 keccak, 116 ecsm), so a hot symbol in one need not be hot in the other.
+# Both counts are from the same guest ELF (merge fdb92f67, main @ 9ccdaf2, clang 21);
+# they move with guest optimisation (#861's thin LTO) and ~2% with the clang major, so
+# pin the ELF when quoting one.
+#
+# WORKLOAD=real also switches to a continuation prove (monolithic would need ~240 GB
+# at that trace length), which is ~4-5 min per recording — five recordings, so budget
+# ~25 min, plus ~1.2 GB of disk per bundle and ~32 GiB of RAM at the default epoch.
+#
# Produces:
# - two perf-diff tables (recorded twice per side, interleaved B A B A —
# symbols whose delta repeats across both tables are real, one-off
@@ -29,9 +46,17 @@ if [ $# -lt 1 ]; then
fi
REF_A="$1"
REF_B="${2:-origin/main}"
+WORKLOAD="${WORKLOAD:-synthetic}"
+# 2^22: the calibrated tier for the bench server this script targets, same as
+# /bench's real-block arm. Memory picks it, not speed — 32.2 GiB fits a >=64 GiB box with ~50%
+# headroom where 2^23's 60 GiB does not.
+EPOCH_SIZE_LOG2="${EPOCH_SIZE_LOG2:-22}"
+case "$WORKLOAD" in
+ synthetic|real) ;;
+ *) echo "ERROR: WORKLOAD must be 'synthetic' or 'real' (got '$WORKLOAD')." >&2; exit 2 ;;
+esac
ELF_REL="executor/program_artifacts/rust/ethrex.elf"
-INPUT_REL="executor/tests/ethrex_bench_20.bin"
WORK="/tmp/perf_diff"
WT="/tmp/perf_diff_wt"
PROOF="/tmp/perf_diff_proof.bin"
@@ -39,9 +64,30 @@ PROOF="/tmp/perf_diff_proof.bin"
ROOT="$(git rev-parse --show-toplevel)"
cd "$ROOT"
+# The real block's name lives in the Makefile and nowhere else, so repointing the
+# benchmark to a different block never needs an edit here.
+if [ "$WORKLOAD" = "real" ]; then
+ INPUT_REL="$(make -s print-real-block-fixture)"
+ CONT_ARGS="--continuations --epoch-size-log2 $EPOCH_SIZE_LOG2"
+else
+ INPUT_REL="executor/tests/ethrex_bench_20.bin"
+ CONT_ARGS=""
+fi
+
command -v perf >/dev/null 2>&1 || { echo "ERROR: perf not installed (linux-tools)." >&2; exit 1; }
[ -f "$ELF_REL" ] || { echo "ERROR: missing $ELF_REL — run bench_abba.sh once (it builds the guest)." >&2; exit 1; }
-[ -f "$INPUT_REL" ] || { echo "ERROR: missing $INPUT_REL — run bench_abba.sh once (it builds the fixture)." >&2; exit 1; }
+if [ ! -f "$INPUT_REL" ]; then
+ if [ "$WORKLOAD" = "real" ]; then
+ # ~1 MB, gitignored, never in a fresh checkout; fetch rather than abort. This is
+ # a URL + sha256 download, not a build.
+ echo "==> Fetching ethrex real-block fixture (missing)"
+ make ethrex-real-block-fixture
+ else
+ echo "ERROR: missing $INPUT_REL — run bench_abba.sh once (it builds the fixture)." >&2
+ exit 1
+ fi
+fi
+echo "==> Workload: $WORKLOAD ($INPUT_REL${CONT_ARGS:+, continuations epoch 2^$EPOCH_SIZE_LOG2})"
echo "==> Refs"
git fetch origin --quiet || echo "WARNING: 'git fetch origin' failed -- resolving against possibly-stale local refs." >&2
@@ -86,14 +132,16 @@ fi
# --- Record: warmup, then B A B A (interleaved so drift hits both sides) ---
record() { # $1=binary $2=out.data
+ # shellcheck disable=SC2086 # CONT_ARGS is a deliberate multi-word flag list (empty when synthetic)
perf record -F 599 -o "$WORK/$2" -- \
- "$WORK/$1" prove "$ELF_REL" --private-input "$INPUT_REL" -o "$PROOF" --time \
+ "$WORK/$1" prove "$ELF_REL" --private-input "$INPUT_REL" $CONT_ARGS -o "$PROOF" --time \
>"$WORK/$2.log" 2>&1
rm -f "$PROOF"
grep -o 'Proving time: [0-9.]*' "$WORK/$2.log" || true
}
echo "==> Warmup (B, not recorded)"
-"$WORK/cli_B" prove "$ELF_REL" --private-input "$INPUT_REL" -o "$PROOF" --time >/dev/null 2>&1
+# shellcheck disable=SC2086
+"$WORK/cli_B" prove "$ELF_REL" --private-input "$INPUT_REL" $CONT_ARGS -o "$PROOF" --time >/dev/null 2>&1
rm -f "$PROOF"
echo "==> Recording B (main), run 1"; record cli_B B1.data
echo "==> Recording A (PR), run 1"; record cli_A A1.data
diff --git a/scripts/render_bench_comment.js b/scripts/render_bench_comment.js
new file mode 100644
index 000000000..eee39f231
--- /dev/null
+++ b/scripts/render_bench_comment.js
@@ -0,0 +1,99 @@
+#!/usr/bin/env node
+//
+// render_bench_comment.js — renders benchmark-pr.yml's PR-comment script offline.
+//
+// WHY: that comment is ~200 lines of JS embedded in YAML, and the only other way to
+// see its output is to push and run a real benchmark on the shared bench server
+// (~15-20 min of occupancy). This extracts the `script:` block from the workflow,
+// runs it against synthetic env values, and prints the markdown — so a wording or
+// formatting change is checked in a second.
+//
+// It renders; it does not assert. Read the output. The scenarios below are the ones
+// that have actually broken before: sign handling on the verdict lines, the
+// no-baseline and block-mismatch paths, and the fixture-unavailable footer.
+//
+// USAGE: node scripts/render_bench_comment.js
+//
+// Add a scenario by adding a key to `scenarios`; each is the env the comment step
+// would see. Keep one per branch of the renderer.
+const fs = require('fs');
+const path = require('path');
+const wfPath = path.join(__dirname, '..', '.github', 'workflows', 'benchmark-pr.yml');
+const wf = fs.readFileSync(wfPath, 'utf8');
+const lines = wf.split('\n');
+const start = lines.map(l => l.trim() === 'script: |').lastIndexOf(true);
+if (start < 0) throw new Error('script block not found');
+const indent = lines[start].match(/^\s*/)[0].length + 2;
+const body = [];
+for (let i = start + 1; i < lines.length; i++) {
+ const l = lines[i];
+ if (l.trim() === '') { body.push(''); continue; }
+ if (l.match(/^\s*/)[0].length < indent) break;
+ body.push(l.slice(indent));
+}
+const src = body.join('\n');
+
+const captured = [];
+const github = { rest: { issues: {
+ listComments: async () => ({ data: [] }),
+ createComment: async (a) => captured.push(a.body),
+ updateComment: async (a) => captured.push(a.body),
+}}};
+const context = { repo: { owner: 'o', repo: 'r' }, issue: { number: 1 } };
+
+const REAL = {
+ COMMIT_SHA: 'c4e42b2900', REAL_EPOCH_LOG2: '22', BASELINE_SRC: 'cached',
+ PR_REAL_TIME: '281.400', PR_REAL_PEAK: '32980', PR_REAL_EPOCHS: '13',
+ PR_REAL_INPUT: 'ethrex_mainnet_25368371.bin',
+ REAL_RUNS: '3', REAL_TIME_SPREAD: '1.9', REAL_ALL_TIMES: '279.1/281.4/284.4',
+};
+const scenarios = {
+ 'A: normal /bench — real block vs cached baseline': {
+ ...REAL, BASE_REAL_TIME: '288.900', BASE_REAL_PEAK: '32975',
+ REAL_TIME_DIFF: '-7.500', REAL_TIME_PCT: '-2.6', REAL_PEAK_DIFF: '5', REAL_PEAK_PCT: '0.0',
+ },
+ 'B: clear improvement': {
+ ...REAL, BASE_REAL_TIME: '340.000', BASE_REAL_PEAK: '32975',
+ REAL_TIME_DIFF: '-58.600', REAL_TIME_PCT: '-17.2', REAL_PEAK_DIFF: '5', REAL_PEAK_PCT: '0.0',
+ },
+ 'C: clear regression': {
+ ...REAL, BASE_REAL_TIME: '240.000', BASE_REAL_PEAK: '32975',
+ REAL_TIME_DIFF: '+41.400', REAL_TIME_PCT: '17.3', REAL_PEAK_DIFF: '5', REAL_PEAK_PCT: '0.0',
+ },
+ 'D: unresolved middle band (3-10%)': {
+ ...REAL, BASE_REAL_TIME: '300.000', BASE_REAL_PEAK: '32975',
+ REAL_TIME_DIFF: '-18.600', REAL_TIME_PCT: '-6.2', REAL_PEAK_DIFF: '5', REAL_PEAK_PCT: '0.0',
+ },
+ 'E: bad spread': {
+ ...REAL, REAL_TIME_SPREAD: '11.4', REAL_ALL_TIMES: '265.0/281.4/297.1',
+ BASE_REAL_TIME: '288.900', BASE_REAL_PEAK: '32975',
+ REAL_TIME_DIFF: '-7.500', REAL_TIME_PCT: '-2.6', REAL_PEAK_DIFF: '5', REAL_PEAK_PCT: '0.0',
+ },
+ 'F: no baseline yet': { ...REAL, BASELINE_SRC: 'built from main' },
+ 'G: baseline measured a different block': {
+ ...REAL, BASE_REAL_TIME: '288.900', BASE_REAL_PEAK: '32975',
+ REAL_MISMATCH: 'ethrex_hoodi_1265656.bin',
+ },
+ 'H: fixture unavailable': { COMMIT_SHA: 'c4e42b2900', REAL_EPOCH_LOG2: '22', BASELINE_SRC: 'cached' },
+ 'I: with growth sweep (push-style)': {
+ ...REAL, BASE_REAL_TIME: '288.900', BASE_REAL_PEAK: '32975',
+ REAL_TIME_DIFF: '-7.500', REAL_TIME_PCT: '-2.6', REAL_PEAK_DIFF: '5', REAL_PEAK_PCT: '0.0',
+ PR_GROWTH_HEAPS: '18756/26966/34748/43896/50431',
+ PR_GROWTH_TIMES: '10.685/13.434/18.854/21.094/24.737',
+ PR_GROWTH_SLOPE: '2007', PR_GROWTH_R2: '0.9981',
+ BASE_GROWTH_HEAPS: '18700/26900/34700/43800/50217',
+ BASE_GROWTH_SLOPE: '2000', BASE_GROWTH_R2: '0.9980',
+ GROWTH_SLOPE_DIFF: '7', GROWTH_SLOPE_PCT: '0.4',
+ },
+};
+
+(async () => {
+ for (const [name, env] of Object.entries(scenarios)) {
+ captured.length = 0;
+ process.env = { ...env };
+ const fn = new Function('github', 'context', 'require', `return (async () => { ${src} })()`);
+ await fn(github, context, require);
+ console.log(`\n${'='.repeat(72)}\n### ${name}\n${'='.repeat(72)}`);
+ console.log(captured[0]);
+ }
+})();
diff --git a/tooling/ethrex-block-converter/.gitignore b/tooling/ethrex-block-converter/.gitignore
new file mode 100644
index 000000000..6d50c2d01
--- /dev/null
+++ b/tooling/ethrex-block-converter/.gitignore
@@ -0,0 +1,4 @@
+/target
+# Downloaded ethrex-replay cache JSONs (~1.5 MB each), fetched on demand by
+# `make ethrex-real-block-fixture`.
+/caches/
diff --git a/tooling/ethrex-block-converter/Cargo.lock b/tooling/ethrex-block-converter/Cargo.lock
new file mode 100644
index 000000000..a8268a857
--- /dev/null
+++ b/tooling/ethrex-block-converter/Cargo.lock
@@ -0,0 +1,3344 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "aead"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
+dependencies = [
+ "crypto-common",
+ "generic-array",
+]
+
+[[package]]
+name = "aes"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
+[[package]]
+name = "aes-gcm"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
+dependencies = [
+ "aead",
+ "aes",
+ "cipher",
+ "ctr",
+ "ghash",
+ "subtle",
+]
+
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
+
+[[package]]
+name = "ark-bn254"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d69eab57e8d2663efa5c63135b2af4f396d66424f88954c21104125ab6b3e6bc"
+dependencies = [
+ "ark-ec",
+ "ark-ff",
+ "ark-std",
+]
+
+[[package]]
+name = "ark-ec"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce"
+dependencies = [
+ "ahash",
+ "ark-ff",
+ "ark-poly",
+ "ark-serialize",
+ "ark-std",
+ "educe",
+ "fnv",
+ "hashbrown 0.15.5",
+ "itertools 0.13.0",
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+ "zeroize",
+]
+
+[[package]]
+name = "ark-ff"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70"
+dependencies = [
+ "ark-ff-asm",
+ "ark-ff-macros",
+ "ark-serialize",
+ "ark-std",
+ "arrayvec",
+ "digest",
+ "educe",
+ "itertools 0.13.0",
+ "num-bigint",
+ "num-traits",
+ "paste",
+ "zeroize",
+]
+
+[[package]]
+name = "ark-ff-asm"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60"
+dependencies = [
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "ark-ff-macros"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3"
+dependencies = [
+ "num-bigint",
+ "num-traits",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "ark-poly"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27"
+dependencies = [
+ "ahash",
+ "ark-ff",
+ "ark-serialize",
+ "ark-std",
+ "educe",
+ "fnv",
+ "hashbrown 0.15.5",
+]
+
+[[package]]
+name = "ark-serialize"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7"
+dependencies = [
+ "ark-serialize-derive",
+ "ark-std",
+ "arrayvec",
+ "digest",
+ "num-bigint",
+]
+
+[[package]]
+name = "ark-serialize-derive"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "ark-std"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a"
+dependencies = [
+ "num-traits",
+ "rand 0.8.7",
+]
+
+[[package]]
+name = "arrayvec"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "base16ct"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "base64ct"
+version = "1.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
+
+[[package]]
+name = "bitcoin-consensus-encoding"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "207311705279250ba465076a1bac4b1ac982855fff73fc5f67e22158ac58cdc9"
+dependencies = [
+ "bitcoin-internals",
+ "hex-conservative 1.2.0",
+ "serde",
+]
+
+[[package]]
+name = "bitcoin-internals"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d573f4cf32996a8dce612e4348cece65a241f1882ed594047c9ba348e8869fa5"
+
+[[package]]
+name = "bitcoin-io"
+version = "0.1.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb5de036369d1ac59d3c1819ebc4d850f89466f5401c571a285b6ed564a4cb78"
+dependencies = [
+ "bitcoin-consensus-encoding",
+]
+
+[[package]]
+name = "bitcoin_hashes"
+version = "0.14.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2"
+dependencies = [
+ "bitcoin-io",
+ "hex-conservative 0.2.2",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
+
+[[package]]
+name = "bitvec"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block2"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
+dependencies = [
+ "objc2",
+]
+
+[[package]]
+name = "bls12_381"
+version = "0.8.0"
+source = "git+https://github.com/lambdaclass/bls12_381?branch=expose-affine-constructors#78cad0378b17fc3157b83f514be192bf46edf9a1"
+dependencies = [
+ "digest",
+ "ff",
+ "group",
+ "pairing",
+ "rand_core 0.6.4",
+ "subtle",
+]
+
+[[package]]
+name = "blst"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c20659f9bbee16cbbd2f7393e40ab6309f5a98f76a2eb57a995ec508b72387fe"
+dependencies = [
+ "cc",
+ "glob",
+ "threadpool",
+ "zeroize",
+]
+
+[[package]]
+name = "bs58"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "byte-slice-cast"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d"
+
+[[package]]
+name = "bytecheck"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0caa33a2c0edca0419d15ac723dff03f1956f7978329b1e3b5fdaaaed9d3ca8b"
+dependencies = [
+ "bytecheck_derive",
+ "ptr_meta",
+ "rancor",
+ "simdutf8",
+]
+
+[[package]]
+name = "bytecheck_derive"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "bytemuck"
+version = "1.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "c-kzg"
+version = "2.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38d04308254695569fdb9bfe3bacc1c91837a670d0806605eb82d63748fbd3a6"
+dependencies = [
+ "blst",
+ "cc",
+ "glob",
+ "hex",
+ "libc",
+ "once_cell",
+ "serde",
+]
+
+[[package]]
+name = "cc"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
+
+[[package]]
+name = "chrono"
+version = "0.4.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
+dependencies = [
+ "iana-time-zone",
+ "num-traits",
+ "serde",
+ "windows-link",
+]
+
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
+[[package]]
+name = "concat-kdf"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d72c1252426a83be2092dd5884a5f6e3b8e7180f6891b6263d2c21b92ec8816"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "const-default"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b396d1f76d455557e1218ec8066ae14bba60b4b36ecd55577ba979f5db7ecaa"
+
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
+[[package]]
+name = "const_format"
+version = "0.2.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e"
+dependencies = [
+ "const_format_proc_macros",
+ "konst",
+]
+
+[[package]]
+name = "const_format_proc_macros"
+version = "0.2.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "convert_case"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "critical-section"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
+
+[[package]]
+name = "crossbeam"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
+dependencies = [
+ "crossbeam-channel",
+ "crossbeam-deque",
+ "crossbeam-epoch",
+ "crossbeam-queue",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-queue"
+version = "0.3.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
+
+[[package]]
+name = "crunchy"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+
+[[package]]
+name = "crypto-bigint"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
+dependencies = [
+ "generic-array",
+ "rand_core 0.6.4",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "rand_core 0.6.4",
+ "typenum",
+]
+
+[[package]]
+name = "ctr"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
+name = "ctrlc"
+version = "3.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162"
+dependencies = [
+ "dispatch2",
+ "nix",
+ "windows-sys",
+]
+
+[[package]]
+name = "darling"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
+dependencies = [
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "der"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
+dependencies = [
+ "const-oid",
+ "zeroize",
+]
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "derive_more"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05"
+dependencies = [
+ "derive_more-impl",
+]
+
+[[package]]
+name = "derive_more-impl"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"
+dependencies = [
+ "convert_case",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "unicode-xid",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "const-oid",
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "dispatch2"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
+dependencies = [
+ "bitflags",
+ "block2",
+ "libc",
+ "objc2",
+]
+
+[[package]]
+name = "dyn-clone"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
+
+[[package]]
+name = "ecdsa"
+version = "0.16.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
+dependencies = [
+ "der",
+ "digest",
+ "elliptic-curve",
+ "rfc6979",
+ "signature",
+ "spki",
+]
+
+[[package]]
+name = "educe"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417"
+dependencies = [
+ "enum-ordinalize",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "either"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
+
+[[package]]
+name = "elliptic-curve"
+version = "0.13.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
+dependencies = [
+ "base16ct",
+ "crypto-bigint",
+ "digest",
+ "ff",
+ "generic-array",
+ "group",
+ "pkcs8",
+ "rand_core 0.6.4",
+ "sec1",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "embedded-alloc"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f2de9133f68db0d4627ad69db767726c99ff8585272716708227008d3f1bddd"
+dependencies = [
+ "const-default",
+ "critical-section",
+ "linked_list_allocator",
+ "rlsf",
+]
+
+[[package]]
+name = "embedded-hal"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89"
+
+[[package]]
+name = "enum-ordinalize"
+version = "4.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07f808d588c10e464ea6f7d3eaed500049eff30aaac103460f61828c2d65b3eb"
+dependencies = [
+ "enum-ordinalize-derive",
+]
+
+[[package]]
+name = "enum-ordinalize-derive"
+version = "4.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42e528e2d34ba8a67a1a650b86beae8ef69fc5fdb638016f386b973226590432"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "ethbloom"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c321610643004cf908ec0f5f2aa0d8f1f8e14b540562a2887a1111ff1ecbf7b"
+dependencies = [
+ "crunchy",
+ "fixed-hash",
+ "impl-rlp",
+ "impl-serde",
+ "tiny-keccak",
+]
+
+[[package]]
+name = "ethereum-types"
+version = "0.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ab15ed80916029f878e0267c3a9f92b67df55e79af370bf66199059ae2b4ee3"
+dependencies = [
+ "ethbloom",
+ "fixed-hash",
+ "impl-rlp",
+ "impl-serde",
+ "primitive-types",
+ "uint",
+]
+
+[[package]]
+name = "ethrex-block-converter"
+version = "0.1.0"
+dependencies = [
+ "ethrex-common",
+ "ethrex-config",
+ "ethrex-guest-program",
+ "k256",
+ "lambda-vm-ethrex-crypto",
+ "rkyv",
+ "serde",
+ "serde_json",
+ "sha2",
+]
+
+[[package]]
+name = "ethrex-blockchain"
+version = "13.0.0"
+source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c"
+dependencies = [
+ "bytes",
+ "crossbeam",
+ "ethrex-common",
+ "ethrex-crypto",
+ "ethrex-metrics",
+ "ethrex-rlp",
+ "ethrex-storage",
+ "ethrex-trie",
+ "ethrex-vm",
+ "rayon",
+ "rustc-hash",
+ "thiserror 2.0.19",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "ethrex-common"
+version = "13.0.0"
+source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c"
+dependencies = [
+ "bytes",
+ "crc32fast",
+ "ethereum-types",
+ "ethrex-crypto",
+ "ethrex-rlp",
+ "ethrex-trie",
+ "hex",
+ "hex-literal",
+ "hex-simd",
+ "indexmap 2.14.0",
+ "lazy_static",
+ "libc",
+ "lru",
+ "once_cell",
+ "rkyv",
+ "rustc-hash",
+ "serde",
+ "serde_json",
+ "sha2",
+ "thiserror 2.0.19",
+ "tracing",
+]
+
+[[package]]
+name = "ethrex-config"
+version = "13.0.0"
+source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c"
+dependencies = [
+ "ethrex-common",
+ "ethrex-p2p",
+ "hex",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "ethrex-crypto"
+version = "13.0.0"
+source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c"
+dependencies = [
+ "ark-bn254",
+ "ark-ec",
+ "ark-ff",
+ "bls12_381",
+ "c-kzg",
+ "ethereum-types",
+ "ff",
+ "hex-literal",
+ "k256",
+ "malachite",
+ "num-bigint",
+ "p256",
+ "ripemd",
+ "sha2",
+ "thiserror 2.0.19",
+ "tiny-keccak",
+]
+
+[[package]]
+name = "ethrex-guest-program"
+version = "13.0.0"
+source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c"
+dependencies = [
+ "bytes",
+ "ethereum-types",
+ "ethrex-common",
+ "ethrex-crypto",
+ "ethrex-l2-common",
+ "ethrex-rlp",
+ "ethrex-vm",
+ "hex",
+ "k256",
+ "lambda-vm-syscalls",
+ "rkyv",
+ "serde",
+ "serde_with",
+ "thiserror 2.0.19",
+]
+
+[[package]]
+name = "ethrex-l2-common"
+version = "13.0.0"
+source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c"
+dependencies = [
+ "bytes",
+ "ethereum-types",
+ "ethrex-common",
+ "ethrex-crypto",
+ "k256",
+ "lambdaworks-crypto",
+ "rkyv",
+ "secp256k1",
+ "serde",
+ "serde_with",
+ "thiserror 2.0.19",
+ "tracing",
+]
+
+[[package]]
+name = "ethrex-levm"
+version = "13.0.0"
+source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c"
+dependencies = [
+ "bytes",
+ "derive_more",
+ "ethrex-common",
+ "ethrex-crypto",
+ "ethrex-rlp",
+ "malachite",
+ "rustc-hash",
+ "serde",
+ "strum",
+ "thiserror 2.0.19",
+]
+
+[[package]]
+name = "ethrex-metrics"
+version = "13.0.0"
+source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c"
+dependencies = [
+ "ethrex-common",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.19",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "ethrex-p2p"
+version = "13.0.0"
+source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c"
+dependencies = [
+ "aes",
+ "aes-gcm",
+ "bytes",
+ "concat-kdf",
+ "crossbeam",
+ "ctr",
+ "ethereum-types",
+ "ethrex-blockchain",
+ "ethrex-common",
+ "ethrex-crypto",
+ "ethrex-rlp",
+ "ethrex-storage",
+ "ethrex-trie",
+ "futures",
+ "hex",
+ "hkdf",
+ "hmac",
+ "indexmap 2.14.0",
+ "lazy_static",
+ "lru",
+ "prometheus",
+ "rand 0.8.7",
+ "rayon",
+ "rustc-hash",
+ "secp256k1",
+ "serde",
+ "sha2",
+ "snap",
+ "spawned-concurrency",
+ "spawned-rt",
+ "thiserror 2.0.19",
+ "tokio",
+ "tokio-stream",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "ethrex-rlp"
+version = "13.0.0"
+source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c"
+dependencies = [
+ "bytes",
+ "ethereum-types",
+ "thiserror 2.0.19",
+]
+
+[[package]]
+name = "ethrex-storage"
+version = "13.0.0"
+source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c"
+dependencies = [
+ "anyhow",
+ "bytes",
+ "ethrex-common",
+ "ethrex-crypto",
+ "ethrex-rlp",
+ "ethrex-trie",
+ "fastbloom",
+ "lru",
+ "rayon",
+ "rustc-hash",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.19",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "ethrex-trie"
+version = "13.0.0"
+source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c"
+dependencies = [
+ "anyhow",
+ "bytes",
+ "crossbeam",
+ "ethereum-types",
+ "ethrex-crypto",
+ "ethrex-rlp",
+ "lazy_static",
+ "rayon",
+ "rkyv",
+ "rustc-hash",
+ "serde",
+ "thiserror 2.0.19",
+]
+
+[[package]]
+name = "ethrex-vm"
+version = "13.0.0"
+source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c"
+dependencies = [
+ "bytes",
+ "derive_more",
+ "dyn-clone",
+ "ethrex-common",
+ "ethrex-crypto",
+ "ethrex-levm",
+ "ethrex-rlp",
+ "rustc-hash",
+ "serde",
+ "thiserror 2.0.19",
+ "tracing",
+]
+
+[[package]]
+name = "fastbloom"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7f34442dbe69c60fe8eaf58a8cafff81a1f278816d8ab4db255b3bef4ac3c4"
+dependencies = [
+ "getrandom 0.3.4",
+ "libm",
+ "rand 0.9.5",
+ "siphasher",
+]
+
+[[package]]
+name = "ff"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
+dependencies = [
+ "bitvec",
+ "rand_core 0.6.4",
+ "subtle",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "fixed-hash"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534"
+dependencies = [
+ "byteorder",
+ "rand 0.8.7",
+ "rustc-hex",
+ "static_assertions",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
+[[package]]
+name = "funty"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
+
+[[package]]
+name = "futures"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307"
+
+[[package]]
+name = "futures-task"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109"
+
+[[package]]
+name = "futures-util"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+ "zeroize",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+]
+
+[[package]]
+name = "ghash"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
+dependencies = [
+ "opaque-debug",
+ "polyval",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
+
+[[package]]
+name = "group"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
+dependencies = [
+ "ff",
+ "rand_core 0.6.4",
+ "subtle",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "allocator-api2",
+ "foldhash 0.1.5",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash 0.2.0",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hex-conservative"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f"
+dependencies = [
+ "arrayvec",
+]
+
+[[package]]
+name = "hex-conservative"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35431185f361ccf3ffc58254628af5f1f5d5f28531da2e02e5d6c82bbc282a10"
+dependencies = [
+ "arrayvec",
+]
+
+[[package]]
+name = "hex-literal"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46"
+
+[[package]]
+name = "hex-simd"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f7685beb53fc20efc2605f32f5d51e9ba18b8ef237961d1760169d2290d3bee"
+dependencies = [
+ "outref",
+ "vsimd",
+]
+
+[[package]]
+name = "hkdf"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
+dependencies = [
+ "hmac",
+]
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "impl-codec"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d40b9d5e17727407e55028eafc22b2dc68781786e6d7eb8a21103f5058e3a14"
+dependencies = [
+ "parity-scale-codec",
+]
+
+[[package]]
+name = "impl-rlp"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54ed8ad1f3877f7e775b8cbf30ed1bd3209a95401817f19a0eb4402d13f8cf90"
+dependencies = [
+ "rlp",
+]
+
+[[package]]
+name = "impl-serde"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a143eada6a1ec4aefa5049037a26a6d597bfd64f8c026d07b77133e02b7dd0b"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "impl-trait-for-tuples"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+ "serde",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.17.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "inout"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "itertools"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "js-sys"
+version = "0.3.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "k256"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b"
+dependencies = [
+ "cfg-if",
+ "ecdsa",
+ "elliptic-curve",
+ "once_cell",
+ "sha2",
+ "signature",
+]
+
+[[package]]
+name = "keccak"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
+dependencies = [
+ "cpufeatures",
+]
+
+[[package]]
+name = "konst"
+version = "0.2.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb"
+dependencies = [
+ "konst_macro_rules",
+]
+
+[[package]]
+name = "konst_macro_rules"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37"
+
+[[package]]
+name = "lambda-vm-ethrex-crypto"
+version = "0.1.0"
+dependencies = [
+ "ethrex-crypto",
+ "k256",
+ "lambda-vm-syscalls",
+]
+
+[[package]]
+name = "lambda-vm-syscalls"
+version = "0.1.0"
+dependencies = [
+ "embedded-alloc",
+ "getrandom 0.2.17",
+ "getrandom 0.3.4",
+ "lazy_static",
+ "rand 0.9.5",
+ "riscv",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "lambdaworks-crypto"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58b1a1c1102a5a7fbbda117b79fb3a01e033459c738a3c1642269603484fd1c1"
+dependencies = [
+ "lambdaworks-math",
+ "rand 0.8.7",
+ "rand_chacha 0.3.1",
+ "serde",
+ "sha2",
+ "sha3",
+]
+
+[[package]]
+name = "lambdaworks-math"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "018a95aa873eb49896a858dee0d925c33f3978d073c64b08dd4f2c9b35a017c6"
+dependencies = [
+ "getrandom 0.2.17",
+ "num-bigint",
+ "num-traits",
+ "rand 0.8.7",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.189"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
+
+[[package]]
+name = "libm"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
+
+[[package]]
+name = "linked_list_allocator"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b23ac50abb8261cb38c6e2a7192d3302e0836dac1628f6a93b82b4fad185897"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
+
+[[package]]
+name = "lru"
+version = "0.16.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39"
+dependencies = [
+ "hashbrown 0.16.1",
+]
+
+[[package]]
+name = "malachite"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec410515e231332b14cd986a475d1c3323bcfa4c7efc038bfa1d5b410b1c57e4"
+dependencies = [
+ "malachite-base",
+ "malachite-nz",
+ "malachite-q",
+]
+
+[[package]]
+name = "malachite-base"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c738d3789301e957a8f7519318fcbb1b92bb95863b28f6938ae5a05be6259f34"
+dependencies = [
+ "hashbrown 0.15.5",
+ "itertools 0.14.0",
+ "libm",
+ "ryu",
+]
+
+[[package]]
+name = "malachite-nz"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1707c9a1fa36ce21749b35972bfad17bbf34cf5a7c96897c0491da321e387d3b"
+dependencies = [
+ "itertools 0.14.0",
+ "libm",
+ "malachite-base",
+ "wide",
+]
+
+[[package]]
+name = "malachite-q"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d764801aa4e96bbb69b389dcd03b50075345131cd63ca2e380bca71cc37a3675"
+dependencies = [
+ "itertools 0.14.0",
+ "malachite-base",
+ "malachite-nz",
+]
+
+[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "mio"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys",
+]
+
+[[package]]
+name = "munge"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c"
+dependencies = [
+ "munge_macro",
+]
+
+[[package]]
+name = "munge_macro"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "nix"
+version = "0.31.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "objc2"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
+dependencies = [
+ "objc2-encode",
+]
+
+[[package]]
+name = "objc2-encode"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "opaque-debug"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
+
+[[package]]
+name = "outref"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
+
+[[package]]
+name = "p256"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
+dependencies = [
+ "ecdsa",
+ "elliptic-curve",
+ "primeorder",
+ "sha2",
+]
+
+[[package]]
+name = "pairing"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f"
+dependencies = [
+ "group",
+]
+
+[[package]]
+name = "parity-scale-codec"
+version = "3.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa"
+dependencies = [
+ "arrayvec",
+ "bitvec",
+ "byte-slice-cast",
+ "const_format",
+ "impl-trait-for-tuples",
+ "parity-scale-codec-derive",
+ "rustversion",
+ "serde",
+]
+
+[[package]]
+name = "parity-scale-codec-derive"
+version = "3.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
+
+[[package]]
+name = "polyval"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "opaque-debug",
+ "universal-hash",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "primeorder"
+version = "0.13.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
+dependencies = [
+ "elliptic-curve",
+]
+
+[[package]]
+name = "primitive-types"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d15600a7d856470b7d278b3fe0e311fe28c2526348549f8ef2ff7db3299c87f5"
+dependencies = [
+ "fixed-hash",
+ "impl-codec",
+ "impl-rlp",
+ "impl-serde",
+ "uint",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
+dependencies = [
+ "toml_edit",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "prometheus"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ca5326d8d0b950a9acd87e6a3f94745394f62e4dae1b1ee22b2bc0c394af43a"
+dependencies = [
+ "cfg-if",
+ "fnv",
+ "lazy_static",
+ "memchr",
+ "parking_lot",
+ "protobuf",
+ "thiserror 2.0.19",
+]
+
+[[package]]
+name = "protobuf"
+version = "3.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4"
+dependencies = [
+ "once_cell",
+ "protobuf-support",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "protobuf-support"
+version = "3.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6"
+dependencies = [
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "ptr_meta"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79"
+dependencies = [
+ "ptr_meta_derive",
+]
+
+[[package]]
+name = "ptr_meta_derive"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "radium"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
+
+[[package]]
+name = "rancor"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daff8b7b3ccf5f7ba270b3e7a0a4d4c701c5797e38dec27c7e2c3dbb830fed1c"
+dependencies = [
+ "ptr_meta",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.17",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+dependencies = [
+ "getrandom 0.3.4",
+]
+
+[[package]]
+name = "rayon"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "ref-cast"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d"
+dependencies = [
+ "ref-cast-impl",
+]
+
+[[package]]
+name = "ref-cast-impl"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
+[[package]]
+name = "rend"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "663ba70707f96e871406fe10d68128412e619b06d1d47cb91c3a4c6501176240"
+dependencies = [
+ "bytecheck",
+]
+
+[[package]]
+name = "rfc6979"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
+dependencies = [
+ "hmac",
+ "subtle",
+]
+
+[[package]]
+name = "ripemd"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "riscv"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b05cfa3f7b30c84536a9025150d44d26b8e1cc20ddf436448d74cd9591eefb25"
+dependencies = [
+ "critical-section",
+ "embedded-hal",
+ "paste",
+ "riscv-macros",
+ "riscv-pac",
+]
+
+[[package]]
+name = "riscv-macros"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d323d13972c1b104aa036bc692cd08b822c8bbf23d79a27c526095856499799"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "riscv-pac"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8188909339ccc0c68cfb5a04648313f09621e8b87dc03095454f1a11f6c5d436"
+
+[[package]]
+name = "rkyv"
+version = "0.8.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73389e0c99e664f919275ab5b5b0471391fe9a8de61e1dff9b1eaf56a90f16e3"
+dependencies = [
+ "bytecheck",
+ "bytes",
+ "hashbrown 0.17.1",
+ "indexmap 2.14.0",
+ "munge",
+ "ptr_meta",
+ "rancor",
+ "rend",
+ "rkyv_derive",
+ "tinyvec",
+ "uuid",
+]
+
+[[package]]
+name = "rkyv_derive"
+version = "0.8.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d2ed0b54125315fb36bd021e82d314d1c126548f871634b483f46b31d13cac6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "rlp"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa24e92bb2a83198bb76d661a71df9f7076b8c420b8696e4d3d97d50d94479e3"
+dependencies = [
+ "bytes",
+ "rustc-hex",
+]
+
+[[package]]
+name = "rlsf"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07393724337be2ee43a9d86164df4505746874a3fa65913374bc6d6a92314362"
+dependencies = [
+ "cfg-if",
+ "const-default",
+ "libc",
+ "rustversion",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
+
+[[package]]
+name = "rustc-hex"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6"
+
+[[package]]
+name = "rustversion"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "safe_arch"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "schemars"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "sec1"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
+dependencies = [
+ "base16ct",
+ "der",
+ "generic-array",
+ "pkcs8",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "secp256k1"
+version = "0.30.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252"
+dependencies = [
+ "bitcoin_hashes",
+ "rand 0.8.7",
+ "secp256k1-sys",
+]
+
+[[package]]
+name = "secp256k1-sys"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.151"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_with"
+version = "3.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c"
+dependencies = [
+ "base64",
+ "bs58",
+ "chrono",
+ "hex",
+ "indexmap 1.9.3",
+ "indexmap 2.14.0",
+ "schemars 0.9.0",
+ "schemars 1.2.2",
+ "serde_core",
+ "serde_json",
+ "serde_with_macros",
+ "time",
+]
+
+[[package]]
+name = "serde_with_macros"
+version = "3.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sha3"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874"
+dependencies = [
+ "digest",
+ "keccak",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc",
+]
+
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "digest",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "simdutf8"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+
+[[package]]
+name = "siphasher"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
+
+[[package]]
+name = "snap"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "199905e6153d6405f9728fe44daace35f8f837bbf830bb6e85fbd5828709a886"
+
+[[package]]
+name = "socket2"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "spawned-concurrency"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc21166874e8cd7584ea795c223303160461f0bb1b571bc23e92ca2abb7c5149"
+dependencies = [
+ "futures",
+ "pin-project-lite",
+ "spawned-macros",
+ "spawned-rt",
+ "thiserror 2.0.19",
+ "tracing",
+]
+
+[[package]]
+name = "spawned-macros"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d64742b41741dfebd5b5ba4dbc4cbc5cc91f4a2cf8107191007d64295682973"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "spawned-rt"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e9e270e6606a118708120671f2d171316762fa832cab73699c714c23aaafe6eb"
+dependencies = [
+ "ctrlc",
+ "tokio",
+ "tokio-stream",
+ "tokio-util",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "strum"
+version = "0.27.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
+dependencies = [
+ "strum_macros",
+]
+
+[[package]]
+name = "strum_macros"
+version = "0.27.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "3.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tap"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9"
+dependencies = [
+ "thiserror-impl 2.0.19",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "threadpool"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
+dependencies = [
+ "num_cpus",
+]
+
+[[package]]
+name = "time"
+version = "0.3.54"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244"
+dependencies = [
+ "deranged",
+ "num-conv",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
+
+[[package]]
+name = "time-macros"
+version = "0.2.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tiny-keccak"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
+dependencies = [
+ "crunchy",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "libc",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "1.1.1+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.25.13+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b"
+dependencies = [
+ "indexmap 2.14.0",
+ "toml_datetime",
+ "toml_parser",
+ "winnow",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.1.3+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
+dependencies = [
+ "winnow",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex-automata",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "typenum"
+version = "1.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+
+[[package]]
+name = "uint"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "909988d098b2f738727b161a106cfc7cab00c539c2687a8836f8e565976fb53e"
+dependencies = [
+ "byteorder",
+ "crunchy",
+ "hex",
+ "static_assertions",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "universal-hash"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
+dependencies = [
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "uuid"
+version = "1.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "vsimd"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.4+wasi-0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wide"
+version = "0.7.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03"
+dependencies = [
+ "bytemuck",
+ "safe_arch",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "winnow"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "wyz"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
+dependencies = [
+ "tap",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
+dependencies = [
+ "zeroize_derive",
+]
+
+[[package]]
+name = "zeroize_derive"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
diff --git a/tooling/ethrex-block-converter/Cargo.toml b/tooling/ethrex-block-converter/Cargo.toml
new file mode 100644
index 000000000..b1796fa60
--- /dev/null
+++ b/tooling/ethrex-block-converter/Cargo.toml
@@ -0,0 +1,60 @@
+[package]
+name = "ethrex-block-converter"
+version = "0.1.0"
+edition = "2024"
+
+# Detached workspace: keeps the heavy ethrex host deps out of the main build.
+[workspace]
+
+[dependencies]
+# Pinned to the SAME ethrex rev as the guest (open LambdaVM-backend PR branch)
+# so the generated ProgramInput rkyv layout matches what the guest deserializes.
+ethrex-common = { git = "https://github.com/lambdaclass/ethrex.git", rev = "156cb8d6a3974f411d71622eecd1b249ee37ff1c", package = "ethrex-common", default-features = false }
+ethrex-config = { git = "https://github.com/lambdaclass/ethrex.git", rev = "156cb8d6a3974f411d71622eecd1b249ee37ff1c", package = "ethrex-config", default-features = false }
+# Matches the guest's own declaration (executor/programs/rust/ethrex/Cargo.toml).
+#
+# CAVEAT: this line alone does NOT reproduce the guest's precompile surface. The
+# `ethrex-config` dep above pulls `ethrex-p2p`, whose `default = ["c-kzg"]`
+# propagates to `ethrex-crypto/c-kzg` — and `default-features = false` cannot
+# switch it off, because ethrex's own workspace declares `ethrex-p2p` with its
+# defaults on. So this graph links a working c-kzg (and malachite modexp, and
+# `ark-ff/asm` BN254) that the guest does not have; verify with
+# `cargo tree -e features -i ethrex-crypto`. Closing that gap means dropping
+# `ethrex-config` and sourcing `ChainConfig` another way — see the README.
+ethrex-guest-program = { git = "https://github.com/lambdaclass/ethrex.git", rev = "156cb8d6a3974f411d71622eecd1b249ee37ff1c", package = "ethrex-guest-program", default-features = false, features = ["lambdavm"] }
+
+# Exact pin: the fixture writer and the guest/executor readers must agree on the
+# rkyv layout. Keep this in sync with the guest
+# (executor/programs/rust/ethrex/Cargo.toml) and the other two detached ethrex
+# tool workspaces (tooling/ethrex-fixtures, tooling/ethrex-tests). The `executor`
+# crate itself declares no rkyv — the guest is what deserializes ProgramInput.
+rkyv = { version = "=0.8.16", features = ["std", "unaligned"] }
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+
+# Not used by this crate's code directly — declared to pin k256's feature set.
+# `lambda-vm-ethrex-crypto` (a dev-dependency) is otherwise the only thing asking
+# for `expose-field`, and under resolver 3 dev-dependency features are not unified
+# into builds that exclude dev-dependencies: `cargo run` (the
+# `make ethrex-real-block-fixture` step) and the `cargo test` that follows it in
+# CI would resolve two different k256 feature sets, recompiling k256 and every
+# ethrex crate above it in the same job. Declaring the features here puts them in
+# both resolutions. Keep in sync with crypto/ethrex-crypto's k256 line; verify:
+# cargo tree -e features,no-dev -i k256 # must list expose-field
+# cargo tree -e features -i k256 # must match the above
+k256 = { version = "=0.13.4", default-features = false, features = ["arithmetic", "expose-field"] }
+
+[dev-dependencies]
+# The `Crypto` impl the guest injects. Off riscv64 its keccak and ECSM paths both
+# fall back to software, so this exercises the guest's *fallback* crypto semantics
+# — not the accelerators themselves. (Its k256 feature needs are mirrored in
+# [dependencies] above — see the comment there.)
+lambda-vm-ethrex-crypto = { path = "../../crypto/ethrex-crypto" }
+# No feature delta against the rest of the graph (sha2/std is already on), so this
+# one can stay a dev-dependency without forcing the rebuild described above.
+sha2 = "0.10"
+
+# `ethrex-guest-program`'s `lambdavm` feature pins `lambda-vm-syscalls` to an
+# older commit; override it with our working-tree copy, as the guest does.
+[patch."https://github.com/yetanotherco/lambda_vm.git"]
+lambda-vm-syscalls = { path = "../../syscalls" }
diff --git a/tooling/ethrex-block-converter/README.md b/tooling/ethrex-block-converter/README.md
new file mode 100644
index 000000000..78dc4fd2e
--- /dev/null
+++ b/tooling/ethrex-block-converter/README.md
@@ -0,0 +1,504 @@
+# ethrex-block-converter
+
+Converts a **real Ethereum block** into a serialized `ProgramInput` `.bin` for
+the lambda-vm ethrex guest, reading an [`ethrex-replay`][replay] cache JSON.
+
+This complements `tooling/ethrex-fixtures`, which builds *synthetic* blocks of N
+plain ETH transfers. Those are cheap and deterministic but not representative:
+they execute no contract code, touch a genesis state trie only a couple of
+levels deep, and carry no bytecode in the witness. This tool produces the
+opposite — a block that actually looks like Ethereum.
+
+Figures below are for the **current default** real block, mainnet 25368371; a
+repoint replaces them (see [Adopting a different block](#adopting-a-different-block)).
+All are measured.
+
+| | `ethrex_bench_20.bin` (synthetic) | `ethrex_mainnet_25368371.bin` (real, default) |
+|---|---|---|
+| gas used | 420,000 | **2,428,684** |
+| transactions | 20 (all plain transfers) | 29 (real mix) |
+| serialized size | 32,766 B | 1,110,156 B |
+| cycles | 8,734,622 | **50,781,557** |
+| keccak / ecsm calls | 411 / 80 | **10,478** / 116 |
+| keccaks per ecrecover | 5.1 | **90** |
+
+Note the real block uses ~5.8x the gas and costs ~5.8x the cycles here, and that it
+inverts the crypto mix: the synthetic block is ecrecover-bound, the real one keccak-
+and trie-bound. That inversion is the point — a prover change can move the two
+numbers in opposite directions. (The gas and cycle ratios agreeing is a coincidence
+of this pair, not a rule: cycles/gas is 20.9 here and ranges ~21–38 across the
+candidate blocks.)
+
+**Pin the ELF whenever you quote a cycle count.** Counts above were measured on this
+branch at merge `fdb92f67` (main @ `9ccdaf2`), guest built with **clang 21.1.8**.
+Two things move them:
+- **Guest optimisation.** #861 gave the guest thin LTO; this block read 74,819,518
+ on a mid-July pre-LTO ELF, so anything quoting ~74.8M or ~65.6M is **superseded**.
+- **clang major version**, by ~2%. The guest embeds C (secp256k1-sys) and the
+ Makefile pins target flags but not the compiler, so `cc` picks up whatever `clang`
+ is on PATH. The RTX 5090 box (clang-18) measured **50,713,534** for this block on
+ main @ `9ccdaf2` — 0.13% below the number above, same commit, different compiler.
+
+Why this block specifically: it was the **only** block in a 90-day Dune sweep that
+matched the shape constraints in the 1.6–2.6M gas band — exactly 2 heavy
+transactions, no single whale transaction dominating, and a sane plain-transfer
+share. Its composition is 11.94 tx/Mgas, 44.3% of gas in heavy transactions, 22.5%
+in the top transaction, p50 transaction gas 41,297. A block that is merely *large*
+is easy to find; one that is structurally typical is not.
+
+The synthetic column is `ethrex_bench_20.bin` as the benchmark scripts actually
+generate it — `ethrex-fixtures 20 … distinct`, i.e. 20 distinct genesis-funded
+senders to 20 distinct recipients (`scripts/bench_verify.sh`,
+`scripts/bench_recursion_scaling.sh`). The same block in `same` mode (one sender,
+one recipient) serializes to 16,811 B.
+
+Block 25368371 is **verified to run on the guest's precompile surface** (see
+[Validation](#validation)); it needs no accelerator we don't have. Any
+replacement block must clear the same check — that is what makes it usable, not
+just realistic.
+
+## Getting the fixture
+
+The fixture is **fetched, not built**:
+
+```bash
+make ethrex-real-block-fixture
+```
+
+That downloads the finished `.bin` from `ETHREX_REAL_BLOCK_FIXTURE_URL` and
+verifies `ETHREX_REAL_BLOCK_FIXTURE_SHA256` before moving it into place —
+the same contract as `prepare-sysroot`. The file is gitignored (~1 MB; see
+`executor/.gitignore`), and a corrupt or interrupted download is discarded rather
+than left looking valid.
+
+The digest of whatever is already on disk is re-checked on every invocation, not
+only when the file is missing — so a stale copy left over from a re-upload under the
+same block number, a corrupted file, or a hand-placed one is all caught and
+re-fetched. That check is the reason these are phony targets rather than file rules.
+
+Artifacts live in the **[`bench-fixtures-v1`][release]** release on
+`yetanotherco/lambda_vm`, fetched unauthenticated:
+
+| asset | sha256 | read by |
+|---|---|---|
+| `ethrex_mainnet_25368371.bin` | `61eba49b…` | every benchmark (**current default**) |
+| `cache_mainnet_25368371.json` | `7aa88a5f…` | `regen-real-block-fixture` |
+| `ethrex_mainnet_25453112.bin` | `0298663d…` | alternate candidate |
+| `cache_mainnet_25453112.json` | `20ffbbc1…` | alternate candidate |
+
+Each block has two assets: the fixture and the **cache** it was converted from
+(`make ethrex-real-block-cache`, ~2 MB, same verify-then-move contract). Only
+`regen-real-block-fixture` reads the cache. Note it is *not* the cache the
+converter's own tests use — see [Validation](#validation).
+
+This crate is not on the fixture's path at all. Fetching a verified binary takes the
+converter, the ~335-package ethrex host dependency tree and an ethrex-replay `rev`
+pin off the critical path of everyone who just wants to run a benchmark. It also
+decouples the benchmark block from what upstream hosts: ethrex-replay publishes a
+cache for Hoodi and nothing else, so any mainnet block is unreachable by the
+convert-locally route — producing its cache takes ~4 minutes and ~700 calls against
+an archive RPC — and trivial by this one.
+
+[release]: https://github.com/yetanotherco/lambda_vm/releases/tag/bench-fixtures-v1
+
+## Regenerating the fixture (ethrex rev bumps)
+
+Needed roughly twice a year, when the guest's ethrex `rev` moves and the rkyv
+layout changes with it:
+
+```bash
+make regen-real-block-fixture # fetches that block's cache, rebuilds the .bin
+sha256sum "$(make -s print-real-block-fixture)"
+```
+
+Then upload the result and update `ETHREX_REAL_BLOCK_FIXTURE_SHA256` and its URL.
+
+Directly, against any cache file:
+
+```bash
+cd tooling/ethrex-block-converter
+cargo run --release --
+```
+
+Output is deterministic for a given cache file:
+
+```text
+wrote ../../executor/tests/ethrex_mainnet_25368371.bin (1110156 bytes): 1 block(s) \
+ from mainnet starting at #25368371, 29 transaction(s), 2428684 gas
+```
+
+Verified: regenerating from the hosted cache reproduces `61eba49b…` byte for byte,
+which is what proves the hosted `.bin` and the hosted cache describe the same block.
+
+The converter's `conversion_is_reproducible` test enforces the same property, but
+against its own pinned block rather than this one — see [Validation](#validation).
+
+## What benchmarks with it
+
+Costs below are for the **current default block** (mainnet 25368371) and move with
+it — see [Measured cost of candidate blocks](#measured-cost-of-candidate-blocks).
+
+| Where | How to run it | Cost (current default) |
+|---|---|---|
+| `benchmark-pr.yml` | **`/bench`** on a PR — the only workload it proves; also push to main and `workflow_dispatch` | 3 runs, ~4–5 min each (first run at epoch 2^22 confirms) |
+| `scripts/bench_verify.sh` | `WORKLOAD=real scripts/bench_verify.sh [` | ~4.5–4.8 min per side, then cached |
+| `scripts/perf_diff.sh` | `WORKLOAD=real scripts/perf_diff.sh ][` | 5 recordings, so ~25 min |
+| `benchmark-gpu.yml` | `/bench-gpu` on a PR — **the default there** | 59.87 s/prove on an RTX 5090 (see below) |
+| `scripts/bench_abba.sh` | `WORKLOAD=real scripts/bench_abba.sh ][` | ~4 h at 20 pairs — **manual only** |
+
+None of them hardcode the fixture path or a block number — they read the path from
+`make -s print-real-block-fixture` and run `make ethrex-real-block-fixture` when
+the `.bin` is absent.
+
+**`/bench-abba` is deliberately NOT wired to the real block.** The script supports
+it (`WORKLOAD=real`, which is what the GPU workflow drives), but the CPU ABBA
+workflow still defaults to the synthetic fixture: 20 pairs × 2 proves × ~4.5–4.8 min
+is **~3 hours** on the one shared bench runner, which every other bench queues behind.
+Run it by hand when a paired test on a real workload is worth that; the option is
+here rather than a footgun on a comment trigger.
+
+**Both bench flows prove this block.** `/bench` runs it sampled on the shared
+runner, against the cached baseline main publishes; `/bench-gpu [N]` runs it as
+N A/B/B/A pairs on a rented box, comparing PR vs main on the same machine —
+absolute GPU times are host-CPU-dependent, so only same-box deltas are
+meaningful.
+
+**GPU baseline (measured), and why the GPU epoch is 2^22.** On an RTX 5090 (32,607 MiB)
+against main @ `9ccdaf2`, same fixture and CLI, one prove per setting:
+
+| epoch | wall | VRAM | epochs | proof |
+|---|---|---|---|---|
+| 2^21 | 70.52 s | 19,193 MiB (58.9%) | 25 | 1.65 GB |
+| **2^22** | **59.87 s** | 23,193 MiB (71.1%) | 13 | 1.12 GB |
+| 2^23 | OOM after 9.7 s | 32,079 MiB (98.4%) — needs ~44 GiB | — | — |
+
+**VRAM is the binding constraint**, so 2^22 is simply the largest setting that fits a
+32 GiB card — and it is ~15% faster than 2^21 (equivalently, 2^21 is ~18% slower) with
+28.9% headroom to spare. 2^23 is out of reach for every card below 48 GiB, not just
+this one. `benchmark-gpu.yml` defaults the real-block path to 2^22 for this reason;
+raw traces are in `~/workspace/lambda_vm_bench_cache/gpu_epoch_calib_2026-07-31/`
+(`PROVENANCE.txt`).
+
+That default is **GPU-path only**. `bench_abba.sh` still defaults to 2^20 for the CPU
+`/bench-abba`, and the CLI's own `DEFAULT_CONTINUATION_EPOCH_SIZE_LOG2` is still 20:
+2^22 needs ~28 GiB of *host* memory on a CPU build, which would break laptops. VRAM
+binds on one path and host RAM on the other, so they are not the same question and the
+GPU number must not be copied across. **The CPU-server epoch recommendation is pending
+calibration.**
+
+The CPU bench runner is roughly **4.5–4.8x** the GPU wall time for the same block.
+
+Do not derive one from the other in general: the CPU rate (5.31–5.62 s/Mcycle) does not
+transfer to the GPU, and the RTX 5090 sweep found the prover CPU-bound at the serial
+producer above epoch 2^21, so GPU time lands closer to CPU time than a naive
+device-throughput estimate suggests.
+
+**Continuations are mandatory, not a tuning choice.** Peak heap on a monolithic
+prove grows ~4.9 GB per million cycles on this workload family (measured on the
+bench server: `10,728 MB + 2,007 MB/transfer`, R² = 0.998 across 4→20 transfers),
+so this block would need **~240 GB** monolithically — and a heavier candidate far
+more. `--continuations` makes peak heap a function of the epoch size instead of the
+trace length, so the same block fits in **~32 GiB** at epoch 2^22, the setting both the
+CPU bench runner and the GPU path now use — picked by host RAM on one and by VRAM on
+the other. See [Choosing the epoch size](#choosing-the-epoch-size) for the full curve
+and the other tiers. The
+bundle on disk is ~1.9 GB; note that a heavier block pushes it past 2 GiB, which
+needs rkyv `pointer_width_64` to serialize.
+
+**`/bench` proves this block and nothing else** — 3 sampled runs against the cached
+3-run baseline main publishes on every push. `/bench N` changes the sample count
+(clamped to 5).
+
+The synthetic N-transfer screen that used to run alongside it was **removed**. Two
+reasons, recorded here because "add a cheap screen back" is an easy suggestion to
+make twice:
+
+1. Its only unique coverage was the **monolithic** prove path, which is vestigial —
+ reportedly slower than a single-epoch continuation. Spending runner time to cover
+ a path we intend to delete is not a trade worth making.
+2. Its crypto mix is one **no real block has**: 8.83 ECSM per Mcycle, against a real
+ block's ~1.3–1.6. Screening against it tunes the prover for a worst case that
+ cannot occur.
+
+The synthetic fixtures themselves are not gone — `/bench-growth` still sweeps them for
+a heap-vs-block-size slope, which needs a family of blocks and so cannot come from one
+real one. `/bench-abba` and `/bench-verify` are untouched.
+
+**The cost is a shared resource.** One runner carries every `/bench`, `/bench-abba`
+and `/bench-verify` in the repo, and a `/bench` now occupies it for **~15–20 min**, on
+every comment and every push to main. `BENCH_RUNS_REAL` in `benchmark-pr.yml` is the
+dial. Its per-run time on that runner is still unmeasured — the epoch calibration ran
+on a different box — so the first run is what tells you whether 3 was the right
+number; above ~6 min a run, turn it down.
+
+Cycle counts here (8.73M synthetic, 50.78M real) are from merge `fdb92f67` (main @
+`9ccdaf2`, clang 21). They move with guest optimisation and ~2% with the clang major,
+so pin the ELF whenever you quote one, or it will look like a regression the next time
+someone measures.
+
+## Where validation runs
+
+The checks themselves are described under [Validation](#validation) below; this is
+where each one executes.
+
+`.github/workflows/ethrex-block-converter.yml` runs them on changes to this crate,
+`tooling/ethrex-tests`, or the `Makefile` — **not** on every PR. The fixture is a
+benchmark input, read by no product code; it has to be right when it changes, not on
+every commit, and running it per-PR put a network fetch and a cold build of ~335
+packages in the required gate.
+
+`no_kzg_backend_linked` is the exception and stays in the required gate
+(`pr_main.yaml`): it is a pure unit test costing microseconds, and it is the property
+the usability screen depends on, so it should fail on the PR that breaks it rather
+than on some later, unrelated one.
+
+## Prerequisites (for regeneration only)
+Rust (stable) and network access on first run (cargo fetches the pinned ethrex
+crates; `make` downloads the cache). **No RV64 target or sysroot needed** — this
+is a host tool.
+
+## Getting a cache for a different block (regeneration only)
+
+The cache format is `ethrex-replay`'s, so use that tool to produce one — it
+handles the RPC fetching, multiple client backends, and the `eth_getProof`
+fallback, none of which is worth reimplementing here:
+
+```bash
+# In a checkout of https://github.com/lambdaclass/ethrex-replay
+ethrex-replay cache --rpc-url
+```
+
+`debug_executionWitness` requires a **reth or ethrex** node; public providers
+(Alchemy, Infura) do not serve it. `ethrex-replay` also supports `eth_getProof`
+for geth/nethermind.
+
+Only **mainnet, Hoodi and Sepolia** caches are accepted. ethrex-replay writes
+`network: "LocalDevnet"` for any other chain, and that resolves to a test chain
+(chain_id 9, every fork active from timestamp 0) — so converting it would replay
+the block under invented rules while still passing every check here, since the
+witness is only ever validated against whichever config we chose. The converter
+refuses instead; `unmappable_network_is_rejected` pins that.
+
+## Adopting a different block
+
+The benchmark block and this crate's test block are **independent** — the fetch is
+what decouples them — so a repoint touches two files and neither is this crate's
+source.
+
+**1. The Makefile — the only place a block number appears.** Five lines:
+
+```make
+ETHREX_REAL_BLOCK_NETWORK :=
+ETHREX_REAL_BLOCK :=
+ETHREX_REAL_BLOCK_FIXTURE_URL :=
+ETHREX_REAL_BLOCK_FIXTURE_SHA256 :=
+ETHREX_REAL_BLOCK_CACHE_URL / _SHA256 :=
+```
+
+**2. `REAL_BLOCK_FIXTURE` in `tooling/ethrex-tests`**, which points the usability
+screen at the block actually being benchmarked. That is the whole of it.
+
+**Nothing in this crate moves.** Its test constants stay pinned to Hoodi 1265656
+across every repoint — what they exercise is the conversion, not the workload, and
+Hoodi's is the one cache ethrex-replay publishes, so pinning there costs no hosting
+and cannot drift.
+
+Everything else derives from the Makefile — the fixture name, and through
+`make -s print-real-block-fixture` the benchmark scripts and `benchmark-pr.yml`. No
+workflow, script or env var names a block. Nothing in `executor/.gitignore` needs
+touching either: it already ignores every accepted network's fixture name, so a
+repointed ~1 MB fixture cannot become committable by accident.
+
+### Measured cost of candidate blocks
+
+Cost is a property of the block, so it changes with the repoint. All figures are
+measured, never derived from gas — **cycles per gas is not constant** (20.9 for the
+current default), so ranking candidates by gas mispredicts cost.
+
+**Current default — main-vintage (merge `fdb92f67`, main @ `9ccdaf2`):**
+
+| block | gas | cycles | GPU prove (RTX 5090) | CPU prove | proof | fixture |
+|---|---|---|---|---|---|---|
+| **mainnet 25368371** | 2.43M | **50,781,557** (clang 21)]
50,713,534 (clang 18) | **59.87 s** @ epoch 2^22 | ~4.5–4.8x the GPU wall | ~1.9 GB | 1,110,156 B, `61eba49b…` |
+
+Epoch 2^22 is the GPU recommendation: VRAM binds, 2^22 leaves 28.9% headroom on a
+32 GiB card and 2^23 does not fit one at all. See
+[Choosing the epoch size](#choosing-the-epoch-size) for the CPU tiers.
+
+**Alternates — PRE-LTO vintage, superseded, re-measure before quoting.** These were
+taken on a mid-July guest ELF, before #861 gave the guest thin LTO; the same build
+change took the current default from 74,819,518 to ~50.7M, so expect these to fall by
+a comparable factor. Kept because they are the selection evidence, not because the
+numbers are current:
+
+| block | gas | cycles (pre-LTO) | fixture |
+|---|---|---|---|
+| mainnet 25453112 | 4.24M | 125,932,956 | 2,019,747 B, `0298663d…` |
+| hoodi 1265656 | 4.40M | 168,319,360 | 1,021,207 B, `1f7d4c4c…` |
+
+All three clear the usability screen. Add a row rather than editing the wiring, and
+say which ELF a number came from.
+
+Two things these numbers show that a gas-based estimate would have got wrong, and both
+survive the vintage change because they are same-ELF comparisons. **Gas does not order
+cost:** 25453112 carries *more* gas than hoodi 1265656 yet costs ~25% fewer cycles.
+And **fixture size does not track cost** either: the current default is the cheapest
+block and the middle-sized fixture.
+
+The default is the cheapest of the three, which matters because the CPU workload sits
+on a single shared bench runner. It was also the only block in a 90-day Dune sweep
+matching the shape constraints (2 heavy transactions, no whale, sane transfer share)
+in the 1.6–2.6M gas band — so it is cheap *and* structurally typical, not cheap
+because it is degenerate.
+
+### Choosing the epoch size
+
+`--epoch-size-log2` trades memory for speed, and **the right value is a property of the
+machine, not of the block**. Three tiers, all measured:
+
+| where | epoch | why |
+|---|---|---|
+| GPU, 32 GiB card | **2^22** | VRAM-bound — 2^23 does not fit |
+| CPU bench runner (≥64 GiB) | **2^22** | host-RAM-bound — 2^23's 60 GiB does not fit safely |
+| CPU server, 128 GiB class | **2^23** | the knee; 2^24 fits but is not worth it |
+| laptops (CLI default) | **2^20** | unchanged, so a plain `cli prove` still works |
+
+CPU sweep, 2026-07-31, on a 124 GiB / 32-core box, real block, **branch vintage**
+(53,757,588 cycles on that box's clang-21 pre-LTO ELF):
+
+| epoch | epochs | wall | peak RSS | proof |
+|---|---|---|---|---|
+| 2^20 | 52 | 616.90 s | 14.56 GiB | 2.83 GB |
+| 2^21 | 26 | 464.26 s | 18.43 GiB | 1.72 GB |
+| 2^22 | 13 | 397.88 s | 32.21 GiB | 1.15 GB |
+| **2^23** | 7 | **356.47 s** | **60.01 GiB** | 0.90 GB |
+| 2^24 | 4 | ~334 s | ~97–105 GiB *(provisional)* | — |
+
+**There is a real knee.** Speed gained per doubling shrinks — 24.7%, 14.3%, 10.4%,
+~6% — while memory roughly doubles at each step near the top. 2^23 uses 48% of a
+124 GiB box (~52% headroom); 2^24 buys only ~6% more speed for ~15% headroom, so it
+**fits but is not recommended on a shared box**, where one co-tenant turns a tight fit
+into an OOM. The 2^24 RSS figure is provisional pending the calibration agent's formal
+report.
+
+A main-vintage anchor also ran on the same box: 2^23 = 344.12 s / 58.87 GiB, i.e. ~3%
+faster than branch vintage at the same memory — as expected, since #861 cut cycles and
+peak RSS is set by the epoch size rather than the trace length.
+
+**Do not read absolute seconds off this table for another machine.** The calibration box
+runs ~8.6 s/Mcycle against the bench runner's 5.31–5.62, so the *ratios* between epochs
+transfer and the wall times do not. `/bench`'s real-block arm now runs at 2^22 (it was
+2^21); the first run after that change establishes the runner's real number.
+
+### Verifying a repoint
+
+Run **both**, in this order:
+
+```bash
+make ethrex-real-block-fixture # fetch + verify the new .bin
+make test-ethrex # the block is USABLE on the guest
+```
+
+`make test-ethrex` is the one that matters here, and the converter's tests cannot
+replace it — they run against a different block. A new block is only
+usable if it needs no accelerator the guest lacks, and this crate cannot tell you
+that — its graph links a working c-kzg, so a block calling point evaluation (0x0a)
+passes here and fails in the guest. `test_ethrex_real_block_native` in
+`tooling/ethrex-tests` is the screen. See [Validation](#validation).
+
+Benchmark comparability does not survive the swap, and that is intentional rather
+than a wrinkle to work around: `benchmark-pr.yml` records which block it measured
+and refuses to diff a PR against a baseline that measured a different one, so the
+first run after a repoint reports one-sided numbers until main republishes.
+
+## Why the JSON and not ethrex-replay's own `.bin`
+
+`ethrex-replay` can already emit a rkyv `ProgramInput`, but it tracks ethrex
+`main`, where the type has diverged from the rev our guest pins
+(`156cb8d6…`): `main` has an extra `fee_configs` field, moved the type from
+`l1::` to `input::`, and uses rkyv 0.8.10 against our exact `=0.8.16`. Its
+binary would not deserialize in our guest.
+
+The cache JSON carries only `blocks` + `witness` + `network` as plain serde, so
+it survives that drift. This tool re-reads it with **our** pinned ethrex types
+and re-serializes with **our** rkyv, which is what keeps the output layout
+correct by construction. When the guest's ethrex `rev` is bumped, bump it here
+too (and in `tooling/ethrex-fixtures` and the guest) and regenerate.
+
+A previous real-block fixture (`ethrex_hoodi.bin`) was lost exactly to this kind
+of drift — it predated the `Crypto` trait and stopped deserializing. Reading the
+version-tolerant JSON instead of a pinned binary is the mitigation.
+
+## Validation
+
+Six checks, ordered so the argument builds: the host-side parity test first, then
+what it does *not* cover, then the checks in `tooling/ethrex-tests` that close the
+gap. Five run on the host and need no RV64 toolchain, and each executes in
+milliseconds.
+
+What costs time is a cold build of the ethrex host dependency tree — ~335
+packages, including the `blst`, `c-kzg` and `secp256k1-sys` C builds, `malachite`
+and `ark-ff/asm` — not the tests. That build is why these checks live in their own
+path-filtered workflow rather than the PR gate (see [Where validation
+runs](#where-validation-runs)); `ethrex-block-converter.yml` caches this workspace's
+`target/` under its own key, so only cold runs pay it.
+
+These run against this crate's own pinned block (Hoodi 1265656), **not** the
+benchmark block — they test the conversion, which any real block exercises equally,
+and Hoodi's is the one cache ethrex-replay publishes. Only
+`test_ethrex_real_block_native` follows the benchmark block.
+
+**`cargo test` here — `real_block_executes_under_guest_crypto`.** Executes the
+block through `LambdaVmEcsmCrypto`, the `Crypto` impl the guest injects, so it is
+exercised via the guest's own trait dispatch. Stateless re-execution ends in a
+post-state-root check, so any divergence from consensus fails here.
+
+**It does not screen KZG.** Declaring `ethrex-guest-program` with
+`default-features = false, features = ["lambdavm"]` is necessary but not
+sufficient: the `ethrex-config` dependency (used only for
+`Network::get_genesis()`) pulls `ethrex-p2p`, whose `default = ["c-kzg"]`
+propagates down to `ethrex-crypto/c-kzg` — and `default-features = false` cannot
+switch it off, because ethrex's own workspace declares `ethrex-p2p` with defaults
+on. Verify with `cargo tree -e features -i ethrex-crypto`. So point evaluation
+(0x0a) resolves to a working c-kzg here and to nothing in the guest.
+
+Scope of the gap: in `ethrex-crypto`, KZG is the **only** precompile whose
+*availability* is feature-gated. The other two gates swap between working
+implementations — `secp256k1` picks libsecp256k1 over k256, `std` picks malachite
+over num-bigint for modexp — so they change which code runs, not whether a block
+can execute. Dropping `ethrex-config` would therefore be a CI-time improvement
+(it also sheds `ethrex-p2p`, `ethrex-blockchain`, `ethrex-storage` and the c-kzg
+and secp256k1 C builds), not a correctness fix.
+
+**`cargo test` here — `unmappable_network_is_rejected`.** Refuses a cache whose
+`network` cannot be mapped to real chain rules rather than converting it under
+substituted ones — see [above](#getting-a-cache-for-a-different-block) for why
+that matters.
+
+**`cargo test` here — `conversion_is_reproducible`.** Pins the block's stats and
+the fixture's **sha256**. A length assert would not do: `ChainConfig` is
+fixed-size, so a substituted chain config yields a byte-length-identical fixture,
+and rkyv's `big_endian` feature would byte-swap in place — neither changes the
+byte count. This is what catches an ethrex rev bump that moves the rkyv layout
+instead of silently producing a fixture the guest can't read.
+
+**`tooling/ethrex-tests` — `no_kzg_backend_linked`.** Asserts that crate links no
+KZG backend. That was incidental to its dependency graph, and it is the property
+the next check relies on, so it is pinned here rather than assumed.
+
+**`tooling/ethrex-tests` — `test_ethrex_real_block_native`.** The one check that
+follows the BENCHMARK block. Checks the serialized `.bin` itself deserializes and
+executes. Since that crate links no KZG
+backend, this is also **what screens point evaluation (0x0a)**: a block reaching
+it diverges from consensus and fails here.
+
+**`tooling/ethrex-tests` — `test_ethrex_real_block_vm`** (`#[ignore]`, excluded
+from PR CI). The block through the guest ELF, comparing the VM's committed
+output against the native reference. Needs the RV64 toolchain and its runtime is
+unmeasured; run it on a build server, not a laptop:
+
+```bash
+cd tooling/ethrex-tests && cargo test --release test_ethrex_real_block_vm -- --ignored
+```
+
+[replay]: https://github.com/lambdaclass/ethrex-replay
diff --git a/tooling/ethrex-block-converter/src/main.rs b/tooling/ethrex-block-converter/src/main.rs
new file mode 100644
index 000000000..f96b03f24
--- /dev/null
+++ b/tooling/ethrex-block-converter/src/main.rs
@@ -0,0 +1,223 @@
+//! Convert a real Ethereum block into the rkyv-serialized `ProgramInput` the
+//! lambda-vm ethrex guest consumes, from an `ethrex-replay` cache JSON.
+//!
+//! Usage:
+//! cargo run --release --
+
+use ethrex_common::types::Block;
+use ethrex_common::types::block_execution_witness::RpcExecutionWitness;
+use ethrex_config::networks::Network;
+use ethrex_guest_program::l1::ProgramInput;
+use serde::Deserialize;
+
+/// The subset of `ethrex-replay`'s on-disk cache that a `ProgramInput` needs.
+///
+/// Deliberately deserialized with *our* pinned ethrex types rather than by
+/// depending on `ethrex-replay`: it tracks ethrex `main`, where `ProgramInput`
+/// has diverged (extra `fee_configs` field, different module path, rkyv 0.8.10
+/// vs our `=0.8.16`), so its own `.bin` output would not deserialize in our
+/// guest. This JSON is the version-tolerant interface between the two.
+///
+/// Extra fields in the file (L2 blob data, custom `chain_config`) are ignored.
+#[derive(Deserialize)]
+struct Cache {
+ blocks: Vec,
+ witness: RpcExecutionWitness,
+ network: Network,
+}
+
+/// Summary of the converted block, for the CLI's one-line report.
+struct BlockSummary {
+ network: String,
+ first_block_number: u64,
+ blocks: usize,
+ transactions: usize,
+ gas_used: u64,
+}
+
+fn program_input_from_cache(
+ cache_path: &str,
+) -> Result<(ProgramInput, BlockSummary), Box> {
+ let cache: Cache =
+ serde_json::from_reader(std::io::BufReader::new(std::fs::File::open(cache_path)?))?;
+
+ let Some(first_block) = cache.blocks.first() else {
+ return Err("cache contains no blocks".into());
+ };
+ let summary = BlockSummary {
+ network: cache.network.to_string(),
+ first_block_number: first_block.header.number,
+ blocks: cache.blocks.len(),
+ transactions: cache.blocks.iter().map(|b| b.body.transactions.len()).sum(),
+ gas_used: cache.blocks.iter().map(|b| b.header.gas_used).sum(),
+ };
+
+ // The chain rules come from `network`, and ethrex-replay maps every chain it
+ // doesn't recognise (anything but mainnet / Hoodi / Sepolia) onto
+ // `LocalDevnet` — which resolves to a test chain: chain_id 9, every fork
+ // active from timestamp 0. Converting under that would execute the block
+ // against invented rules while still satisfying every check downstream (the
+ // witness is replayed against whatever config we picked, and host and guest
+ // read the same one), so refuse rather than guess.
+ if !matches!(cache.network, Network::PublicNetwork(_)) {
+ return Err(format!(
+ "unsupported network `{}`: its chain rules would be guessed, not read \
+ (ethrex-replay writes LocalDevnet for any chain it does not recognise)",
+ cache.network
+ )
+ .into());
+ }
+
+ // `into_execution_witness` rebuilds the trie structures from the flat node
+ // list and needs the parent header, which the cache carries inside `witness`.
+ let chain_config = cache.network.get_genesis()?.config;
+ let witness = cache
+ .witness
+ .into_execution_witness(chain_config, summary.first_block_number)?;
+
+ Ok((ProgramInput::new(cache.blocks, witness), summary))
+}
+
+fn usage_and_exit(program: &str) -> ! {
+ eprintln!("usage: {program} ");
+ std::process::exit(2);
+}
+
+fn main() -> Result<(), Box> {
+ let mut args = std::env::args();
+ let program = args
+ .next()
+ .unwrap_or_else(|| "ethrex-block-converter".into());
+ let (Some(cache_path), Some(out_path)) = (args.next(), args.next()) else {
+ usage_and_exit(&program);
+ };
+ if args.next().is_some() {
+ usage_and_exit(&program);
+ }
+
+ let (program_input, summary) = program_input_from_cache(&cache_path)?;
+ let bytes = rkyv::to_bytes::(&program_input)?;
+ std::fs::write(&out_path, &bytes)?;
+
+ println!(
+ "wrote {out_path} ({} bytes): {} block(s) from {} starting at #{}, \
+ {} transaction(s), {} gas",
+ bytes.len(),
+ summary.blocks,
+ summary.network,
+ summary.first_block_number,
+ summary.transactions,
+ summary.gas_used,
+ );
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use ethrex_config::networks::HOODI_CHAIN_ID;
+
+ const CACHE: &str = "caches/cache_hoodi_1265656.json";
+
+ /// This crate is pinned to Hoodi 1265656 on purpose, independently of whichever
+ /// block the benchmarks currently prove: it is the one cache ethrex-replay hosts
+ /// upstream, so keeping the converter's test input there costs us no hosting and
+ /// cannot drift. What is under test here is the CONVERSION, not the benchmark
+ /// workload — see tooling/ethrex-block-converter/README.md.
+ ///
+ /// `caches/` is gitignored and fetched on demand, so every test here fails on
+ /// a clean checkout until the cache is downloaded. Say so instead of surfacing
+ /// a bare `No such file or directory` from `unwrap()`.
+ const CACHE_MISSING: &str = "caches/cache_hoodi_1265656.json is missing — run \
+ `make ethrex-real-block-converter-cache` from the repo root first";
+
+ /// Executes the converted block with `LambdaVmEcsmCrypto`, the `Crypto` impl
+ /// the guest injects, so the block is exercised through the same trait
+ /// dispatch the guest uses. Stateless re-execution ends in a post-state-root
+ /// check, so any divergence from consensus fails here — on the host, with no
+ /// RV64 toolchain and no proving run.
+ ///
+ /// It does NOT screen KZG: this crate's graph links `c-kzg` (via
+ /// `ethrex-config` → `ethrex-p2p`, see Cargo.toml), so point evaluation
+ /// (0x0a) resolves to a working implementation here while the guest has none.
+ /// A block calling 0x0a would pass this test.
+ ///
+ /// `test_ethrex_real_block_native` in `tooling/ethrex-tests` is what covers
+ /// 0x0a, and `no_kzg_backend_linked` there keeps it covered. That split is
+ /// sufficient rather than a workaround: KZG is the only precompile in
+ /// `ethrex-crypto` whose *availability* is feature-gated — the other gates
+ /// swap between two working implementations.
+ #[test]
+ fn real_block_executes_under_guest_crypto() {
+ use ethrex_guest_program::l1::execution_program;
+ use lambda_vm_ethrex_crypto::LambdaVmEcsmCrypto;
+ use std::sync::Arc;
+
+ let (program_input, _) = program_input_from_cache(CACHE).expect(CACHE_MISSING);
+ execution_program(program_input, Arc::new(LambdaVmEcsmCrypto)).unwrap();
+ }
+
+ /// A cache whose `network` we can't map to real chain rules must be refused,
+ /// not converted under substituted ones. Only the `network` field is changed
+ /// here, and the result is byte-length-identical to the real fixture — which
+ /// is exactly why the other tests cannot catch this on their own.
+ #[test]
+ fn unmappable_network_is_rejected() {
+ let mut cache: serde_json::Value =
+ serde_json::from_str(&std::fs::read_to_string(CACHE).expect(CACHE_MISSING)).unwrap();
+ cache["network"] = serde_json::json!("LocalDevnet");
+
+ let path = std::env::temp_dir().join(format!(
+ "ethrex_real_block_localdevnet_{}.json",
+ std::process::id()
+ ));
+ std::fs::write(&path, serde_json::to_vec(&cache).unwrap()).unwrap();
+ let result = program_input_from_cache(path.to_str().unwrap());
+ std::fs::remove_file(&path).ok();
+ let Err(err) = result else {
+ panic!("LocalDevnet resolves to chain_id 9 with all forks at 0; must not convert");
+ };
+
+ assert!(
+ err.to_string().contains("unsupported network"),
+ "wrong rejection reason: {err}"
+ );
+ }
+
+ /// The serialized form is what the guest actually reads, so pin it: a
+ /// change here means the rkyv layout moved and every consumer of the
+ /// fixture (and its README checksum) needs regenerating.
+ #[test]
+ fn conversion_is_reproducible() {
+ use sha2::Digest;
+
+ let (program_input, summary) = program_input_from_cache(CACHE).expect(CACHE_MISSING);
+ let bytes = rkyv::to_bytes::(&program_input).unwrap();
+
+ assert_eq!(summary.first_block_number, 1_265_656);
+ assert_eq!(summary.transactions, 11);
+ assert_eq!(summary.gas_used, 4_402_947);
+
+ // Asserted separately from the digest below, not covered by it. The digest
+ // is exactly what a legitimate ethrex rev bump forces someone to rewrite
+ // (the layout moves, this goes red, a fresh digest gets pasted in) — and at
+ // that moment it stops covering the substituted-chain-config case it was
+ // chosen for. This assert survives that churn.
+ assert_eq!(
+ program_input.execution_witness.chain_config.chain_id, HOODI_CHAIN_ID,
+ "chain config is not Hoodi's — the block would replay under other rules",
+ );
+
+ // Digest, not length: a layout change can preserve the byte count exactly
+ // (`ChainConfig` is fixed-size, and rkyv's `big_endian` feature would only
+ // byte-swap in place), so `len()` cannot pin the archived form.
+ let digest: String = sha2::Sha256::digest(&bytes)
+ .iter()
+ .map(|b| format!("{b:02x}"))
+ .collect();
+ assert_eq!(
+ digest, "1f7d4c4cdf9bd52472d9ebafdb4038f57a88c3c92d65c96fd86d7e323db87142",
+ "fixture bytes changed — regenerate it and update the README checksum",
+ );
+ }
+}
diff --git a/tooling/ethrex-tests/Cargo.lock b/tooling/ethrex-tests/Cargo.lock
index 26f991c7a..250e2411f 100644
--- a/tooling/ethrex-tests/Cargo.lock
+++ b/tooling/ethrex-tests/Cargo.lock
@@ -610,6 +610,7 @@ version = "0.1.0"
dependencies = [
"k256",
"num-bigint",
+ "num-integer",
"num-traits",
]
diff --git a/tooling/ethrex-tests/tests/ethrex.rs b/tooling/ethrex-tests/tests/ethrex.rs
index c87ccceba..eaa811bf4 100644
--- a/tooling/ethrex-tests/tests/ethrex.rs
+++ b/tooling/ethrex-tests/tests/ethrex.rs
@@ -40,8 +40,8 @@ const ELF_PATH: &str = "../../executor/program_artifacts/rust/ethrex.elf";
const FIXTURES_DIR: &str = "../../executor/tests";
/// Larger-block smoke test: a synthetic ethrex block with 10 ETH transfers.
-/// (Replaces the old `ethrex_hoodi.bin` real-block fixture, which was in the
-/// pre-Crypto-trait ethrex format and no longer deserializes.)
+/// (The old `ethrex_hoodi.bin` real-block fixture predated the `Crypto` trait
+/// and no longer deserializes; the current real-block fixture is separate.)
#[ignore = "heavier synthetic block (10 txs); run in the dedicated --ignored CI step"]
#[test]
fn test_ethrex() {
@@ -86,3 +86,85 @@ fn test_ethrex_empty_block() {
let output = execution_program(input, Arc::new(NativeCrypto)).unwrap();
run_program_and_check_public_output(ELF_PATH, output.encode(), inputs);
}
+
+const REAL_BLOCK_FIXTURE: &str = "ethrex_mainnet_25368371.bin";
+
+/// Host-only acceptance gate for the real-block fixture produced by
+/// `tooling/ethrex-block-converter` (`make ethrex-real-block-fixture`): the block is
+/// re-executed statelessly against its own witness, so a successful run means
+/// the recovered tries, codes and headers reproduce the header's post-state
+/// root. Needs no guest ELF, which is what keeps it runnable where the RV64
+/// toolchain isn't available.
+///
+/// Checks the *serialized artifact* specifically — that the committed rkyv
+/// bytes deserialize and execute — which is why it reads the `.bin` rather
+/// than converting the cache itself.
+///
+/// It is also, in practice, the check that the block needs no KZG: this crate's
+/// dependency graph links no KZG backend, so a block calling point evaluation
+/// (0x0a) diverges from consensus here and fails. That property is incidental to
+/// the dep graph rather than declared, so `no_kzg_backend_linked` below pins it.
+/// (`tooling/ethrex-block-converter`'s parity test does NOT cover 0x0a — it links
+/// `c-kzg` transitively via `ethrex-config`.)
+#[test]
+fn test_ethrex_real_block_native() {
+ use ethrex_guest_program::crypto::NativeCrypto;
+ use ethrex_guest_program::l1::{ProgramInput, execution_program};
+ use rkyv::rancor::Error;
+ use std::sync::Arc;
+ let inputs = std::fs::read(format!("{FIXTURES_DIR}/{REAL_BLOCK_FIXTURE}")).unwrap();
+ let input = rkyv::from_bytes::(&inputs).unwrap();
+ execution_program(input, Arc::new(NativeCrypto)).unwrap();
+}
+
+/// Pins the property the test above leans on: this crate's dependency graph must
+/// link no KZG backend, so a block calling point evaluation (0x0a) diverges from
+/// consensus and fails rather than passing on a surface the guest doesn't have.
+/// If a future dependency pulls `c-kzg` or `kzg-rs` in, this goes red instead of
+/// the screen silently disappearing.
+/// Detection is by error *text*, deliberately: zero input fails under a linked
+/// backend too (invalid G1 encoding), and both paths surface as
+/// `CryptoError::Other`, so the variant can't tell them apart. The string comes
+/// from `ethrex-crypto`'s `KzgError::Unimplemented`; if upstream rewords it this
+/// test goes red, which is the safe direction.
+///
+/// Worth knowing why this can regress: `ethrex-crypto`'s own default feature set
+/// is `["std", "kzg-rs", "secp256k1"]`, so any future dependency pulling it in
+/// with defaults on restores a backend and silently removes the screen.
+#[test]
+fn no_kzg_backend_linked() {
+ use ethrex_guest_program::crypto::{Crypto, NativeCrypto};
+ let result = NativeCrypto.verify_kzg_proof(&[0u8; 32], &[0u8; 32], &[0u8; 48], &[0u8; 48]);
+ let message = match result {
+ Ok(()) => "verify_kzg_proof accepted zero input".to_string(),
+ Err(err) => format!("{err:?}"),
+ };
+ assert!(
+ message.contains("One of features c-kzg, openvm-kzg or kzg-rs should be active"),
+ "a KZG backend is linked into ethrex-tests, so test_ethrex_real_block_native no \
+ longer screens precompile 0x0a: {message}"
+ );
+}
+
+/// The same real block through the guest ELF, checking the VM's committed
+/// output matches the native reference. Split from the native gate above
+/// because this one needs the ethrex ELF and is far heavier than the synthetic
+/// fixtures (a ~1 MB witness, real contract execution).
+///
+/// Deliberately excluded from the PR CI step, which otherwise runs everything
+/// via `--include-ignored`: the cycle cost of a real block in the VM has not
+/// been measured yet, so it is opt-in until we know what it does to job time.
+/// Run it explicitly with:
+/// cd tooling/ethrex-tests && cargo test --release test_ethrex_real_block_vm -- --ignored
+#[ignore = "real block through the VM; unmeasured runtime, run explicitly on a build server"]
+#[test]
+fn test_ethrex_real_block_vm() {
+ use ethrex_guest_program::crypto::NativeCrypto;
+ use ethrex_guest_program::l1::{ProgramInput, execution_program};
+ use rkyv::rancor::Error;
+ use std::sync::Arc;
+ let inputs = std::fs::read(format!("{FIXTURES_DIR}/{REAL_BLOCK_FIXTURE}")).unwrap();
+ let input = rkyv::from_bytes::(&inputs).unwrap();
+ let output = execution_program(input, Arc::new(NativeCrypto)).unwrap();
+ run_program_and_check_public_output(ELF_PATH, output.encode(), inputs);
+}