Skip to content

feat(l1): add LambdaVM zkVM backend for the L1 prover#6722

Open
ilitteri wants to merge 3 commits into
mainfrom
feat/lambdavm-prover-backend
Open

feat(l1): add LambdaVM zkVM backend for the L1 prover#6722
ilitteri wants to merge 3 commits into
mainfrom
feat/lambdavm-prover-backend

Conversation

@ilitteri
Copy link
Copy Markdown
Collaborator

@ilitteri ilitteri commented May 22, 2026

Motivation

LambdaVM (yetanotherco/lambda_vm) was open-sourced as a verifiable RV64IM zkVM with a transparent STARK-over-Goldilocks proof system, 128-bit security, and LogUp lookups. Bringing it into ethrex as a first-class zkVM backend closes the loop on a vertically integrated stack and gives the LambdaVM team a real EVM workload to harden against.

A proof-of-concept guest already exists in the lambda_vm repo at executor/programs/rust/ethrex/, but it was written against an older revision of the guest-program API (before the Crypto trait was extracted) and won't compile against current ethrex main. This PR brings the integration into ethrex itself, alongside the existing SP1, RISC0, OpenVM, and Zisk backends.

Description

Wires LambdaVM into the existing zkVM backend framework, scoped to L1 local proving (mirrors the current Zisk stance — no L2 prover wiring, no on-chain verifier).

Guest side

  • crates/guest-program/bin/lambdavm/ — detached workspace, custom RV64IM target spec (riscv64im-lambda-vm-elf), pinned nightly-2026-02-01 toolchain with rust-src, thin main.rs mirroring the Zisk wrapper.
  • crates/guest-program/src/crypto/lambdavm.rsLambdaVmCrypto overrides keccak256 via LambdaVM's keccak_permute syscall and ECDSA secp256k1 via the shared pure-Rust k256 helpers. Every other Crypto method inherits the trait default (ark-bn254, bls12_381, malachite, p256, kzg-rs) — same shape as the OpenVM adapter.
  • build.rs builds the guest ELF under the lambdavm-build-elf feature. Sysroot path resolved from LAMBDA_VM_SYSROOT_DIR (default /opt/lambda-vm-sysroot); inner toolchain pinned via RUSTC=$(rustc_path("nightly-2026-02-01")) with CARGO / RUSTUP_TOOLCHAIN / RUSTFLAGS / CARGO_ENCODED_RUSTFLAGS cleared so the outer cargo's env doesn't force the inner build onto stable.

Host side

  • crates/prover/src/backend/lambdavm.rsLambdaVmBackend shells out to LambdaVM's cli binary for execute, prove, verify. Inputs serialized as [4-byte LE length][rkyv bytes] matching LambdaVM's PRIVATE_INPUT_START memory layout. prove_timed parses Proving time: <float>s from cli prove --time stdout, falls back to wall-clock if missing.
  • BackendType::LambdaVM registered, FromStr arm added, dispatch site in start_prover wired.

CI / release plumbing

  • .github/actions/install-lambdavm/ — composite action: installs nightly-2026-02-01 + rust-src, downloads the sysroot tarball (cached), builds the cli binary from the pinned LambdaVM commit (cached), puts it on $PATH.
  • .github/workflows/pr-main_l2_prover.yamllambdavm added to the lint_zk matrix, conditional install step added.
  • Root Makefilebin/lambdavm/Cargo.toml added to update-cargo-lock and check-cargo-lock (metadata-only check, toolchain-free pattern matching ZisK).
  • docs/developers/release-process.mdbin/lambdavm/Cargo.toml in the version-bump list, bin/lambdavm/Cargo.lock in the expected-changes list.
  • crates/guest-program/Makefilelambdavm / l2-lambdavm build targets, help text, clean rule extended.

Docs

  • docs/zkvm-integrations.md — LambdaVM section with status, key features, integration details, limitations, build/run instructions.
  • crates/guest-program/README.md — "Integrating a New zkVM" guide expanded from 6 to 11 steps, covering the missing host backend, CI install action, root-Makefile cargo-lock plumbing, release-process doc, and zkvm-integrations.md updates that future zkVM vendors will also need.

Source pinning

LambdaVM is pinned by commit hash (8af4e92e6e75638746fe9924f6bac715ffac9cb6) from both crates/guest-program/Cargo.toml (the lambda-vm-syscalls git dep) and .github/actions/install-lambdavm/action.yml (the cli checkout target). Bumping the version is a single-commit operation touching exactly those two locations; inline comments cross-reference them.

Limitations

Only keccak_permute is accelerated today. ECDSA, BN254, KZG, BLS12-381, sha256, and modexp run via pure-Rust crates — real EVM block proving will be substantially slower than SP1/RISC0 until LambdaVM lands more precompiles. Acknowledged in docs/zkvm-integrations.md.

How to Test

Local build verified end-to-end on macOS arm64:

# One-time sysroot setup (also handled by .github/actions/install-lambdavm in CI)
mkdir -p $HOME/.lambda-vm-sysroot
curl -sL https://lambda.alignedlayer.com/lambda-vm-sysroot-rv64im.tar.gz \
  | tar -xz -C $HOME/.lambda-vm-sysroot --strip-components=1

# Build guest ELF (`brew install llvm` for RISC-V clang)
LAMBDA_VM_SYSROOT_DIR=$HOME/.lambda-vm-sysroot \
  PATH=/opt/homebrew/opt/llvm/bin:$PATH \
  cargo check -p ethrex-guest-program --features lambdavm-build-elf

# Verify ELF type
file crates/guest-program/bin/lambdavm/out/riscv64im-lambda-vm-elf
# => ELF 64-bit LSB executable, UCB RISC-V, soft-float ABI, lp64,
#    statically linked, not stripped

# Build host backend (embeds the ELF via include_bytes!)
cargo check -p ethrex-prover --features lambdavm

Regression: cargo check -p ethrex-prover --features sp1 / --features risc0 still pass; default builds pull no new deps. cargo clippy -p ethrex-prover --features lambdavm and -p ethrex-guest-program --features lambdavm are warning-free on the LambdaVM files.

The new lambdavm entry in the lint_zk matrix will exercise the install action, the guest ELF build, and clippy on every PR.

Checklist

  • Updated STORE_SCHEMA_VERSION (crates/storage/lib.rs) if the PR includes breaking changes to the Store requiring a re-sync.
    • Not applicable — no storage schema changes; the LambdaVM backend is purely on the prover side.

Follow-ups (not blocking this PR)

  • Run LambdaVmBackend::execute / prove / verify against a real L1 block on a developer machine with the LambdaVM cli installed.
  • Capture baseline cycles / proving-time numbers and add them to docs/zkvm-integrations.md.
  • Coordinate with the LambdaVM team to either retire the executor/programs/rust/ethrex/ PoC in their repo or update it to point at this in-ethrex guest.

ilitteri added 3 commits May 22, 2026 19:27
SP1, RISC0, OpenVM, and Zisk backends. LambdaVM (yetanotherco/lambda_vm)
is a verifiable RV64IM zkVM with a transparent STARK-over-Goldilocks proof
system; bringing it into ethrex closes the loop on a vertically integrated
stack and gives the LambdaVM team a real EVM workload to harden against.

Scope: L1 only, local execute/prove/verify via LambdaVM's `cli` binary —
no L2 wiring and no on-chain verifier (mirrors the current Zisk stance).

Guest binary at `crates/guest-program/bin/lambdavm/` (detached workspace,
custom RV64IM target spec, nightly-2026-02-01 toolchain). `LambdaVmCrypto`
overrides `keccak256` via LambdaVM's `keccak_permute` syscall and ECDSA
secp256k1 via the shared pure-Rust k256 helpers — every other Crypto
method inherits the trait default (ark-bn254, bls12_381, malachite, p256,
kzg-rs), matching the OpenVM adapter pattern.

Host backend at `crates/prover/src/backend/lambdavm.rs` shells out to the
LambdaVM `cli` for execute/prove/verify and serializes inputs to a
length-prefixed file matching LambdaVM's `PRIVATE_INPUT_START` layout.
`BackendType::LambdaVM` is gated on the new `lambdavm` Cargo feature.

Source is pinned via commit hash on `yetanotherco/lambda_vm`
(`8af4e92e6e75638746fe9924f6bac715ffac9cb6`) and referenced from both
`crates/guest-program/Cargo.toml` and the new
`.github/actions/install-lambdavm/` composite action. Bumping LambdaVM is
a single-commit operation touching exactly those two locations.

The new `lambdavm` entry in the `lint_zk` matrix in
`.github/workflows/pr-main_l2_prover.yaml` will exercise the install
action and guest ELF build on every PR. Local validations: cargo check
clean with and without the feature on both crates, clippy clean on all
new files, no regressions on `--features sp1` / `--features risc0`.

Real EVM block proving will be substantially slower than SP1/RISC0 until
LambdaVM lands more accelerators — only keccak is accelerated today, so
ECDSA, BN254, KZG, BLS12-381, sha256, and modexp run via pure-Rust crates.
This is acknowledged in `docs/zkvm-integrations.md` and the guest-program
README.
full zkVM integration playbook in the guest-program README.

The initial LambdaVM commit added the backend but missed the surrounding
plumbing that every zkVM needs once it ships a per-backend Cargo.lock:

- Root `Makefile`: `update-cargo-lock` now bumps
  `crates/guest-program/bin/lambdavm/Cargo.toml`, and `check-cargo-lock`
  validates the lockfile via `cargo metadata --locked` (toolchain-free,
  same pattern as ZisK). Without this, CI's lockfile diff check on the
  prover lint job would fail on every PR that touches root deps.
- `docs/developers/release-process.md`: lists `bin/lambdavm/Cargo.toml`
  in the version-bump set and `bin/lambdavm/Cargo.lock` in the
  expected-changes set so the next release cut won't drift.
- `crates/guest-program/Makefile`: adds `lambdavm` / `l2-lambdavm` build
  targets, extends the `.PHONY` list, updates the help text, and adds
  `bin/lambdavm/out` to `clean`.
- `crates/guest-program/README.md`: expands the "Integrating a New zkVM"
  guide to cover the steps the original guide missed — the host backend
  in `crates/prover/src/backend/`, the dispatch site in `prover.rs`, the
  CI install action and prover-lint matrix entry, the root-Makefile
  cargo-lock plumbing, the release-process doc update, and the
  zkvm-integrations.md landing page. Future zkVM integrations can follow
  the same checklist instead of re-deriving it.
actually uses the pinned nightly toolchain.

When `build.rs` runs as part of an outer `cargo check -p ethrex-guest-program
--features lambdavm-build-elf`, that outer cargo sets `RUSTC`, `CARGO`, and
`RUSTUP_TOOLCHAIN` env vars in the child environment. Those override the
`+nightly-2026-02-01` toolchain selector on the spawned `cargo build`, so the
inner build ends up using the outer (stable) toolchain — which then rejects
the `-Z build-std`, `-Z build-std-features`, and `-Z json-target-spec` flags
the LambdaVM target requires.

Same fix the Zisk build script already uses: set `RUSTC` explicitly to the
nightly toolchain's rustc binary (via `rustc_path("nightly-2026-02-01")`),
and clear `CARGO` / `RUSTUP_TOOLCHAIN` so the rustup proxy picks up the
right toolchain on its own. The `rustc_path` helper is now visible to both
`zisk-build-elf` and `lambdavm-build-elf`.

Verified locally on macOS arm64 with `LAMBDA_VM_SYSROOT_DIR=$HOME/.lambda-vm-sysroot`
and Homebrew LLVM on `$PATH`. Produces a 2.8MB RISC-V 64-bit ELF executable
(soft-float, lp64 ABI, statically linked, UCB RISC-V) at
`crates/guest-program/bin/lambdavm/out/riscv64im-lambda-vm-elf`. The prover
crate then sees and embeds it via `ZKVM_LAMBDAVM_PROGRAM_ELF`.

This closes task 8.1 partially: the guest ELF builds end-to-end. Running
`LambdaVmBackend::execute` against an actual block still needs the LambdaVM
`cli` binary installed; the install action covers that for CI.
@ilitteri ilitteri requested a review from a team as a code owner May 22, 2026 22:41
Copilot AI review requested due to automatic review settings May 22, 2026 22:41
@github-actions
Copy link
Copy Markdown

⚠️ Known Issues — intentionally skipped tests

Source: docs/known_issues.md

Known Issues

Tests intentionally excluded from CI. Source of truth for the Known
Issues
section the L1 workflow appends to each ef-tests job summary
and posts as a sticky PR comment.

EF Tests — Stateless coverage narrowed to EIP-8025 optional-proofs

make -C tooling/ef_tests/blockchain test calls test-stateless-zkevm
instead of test-stateless. The zkevm@v0.3.3 fixtures are filled against
bal@v5.6.1, out of sync with current bal spec; the broad target trips ~549
fixtures. Re-broaden once the zkevm bundle is regenerated.

Why and resolution path

PR #6527 broadened
test-stateless to extract the entire for_amsterdam/ tree from the
zkevm bundle and run all of it under --features stateless; combined with
this branch's bal-devnet-7 semantics that scope produces ~549
GasUsedMismatch / ReceiptsRootMismatch /
BlockAccessListHashMismatch failures.

test-stateless-zkevm filters cargo to the eip8025_optional_proofs
suite, which still validates the stateless harness without the bal-version
mismatch.

Re-broaden by switching test: back to test-stateless in
tooling/ef_tests/blockchain/Makefile once the zkevm bundle is regenerated
against the current bal spec.

@github-actions
Copy link
Copy Markdown

🤖 Kimi Code Review

Overall Assessment: This is a well-structured integration of LambdaVM as a new zkVM backend. The code follows the established patterns for other zkVMs (SP1, RISC0, Zisk, OpenVM) and includes comprehensive documentation. However, there are two critical issues that need addressing before merge: a race condition in the host backend and a supply chain security gap in the CI installer.


1. Race Condition in Prover Backend (Critical)

File: crates/prover/src/backend/lambdavm.rs
Lines: 12–14, 95, 116, 161, 177

The backend uses fixed file paths for input, proof, and ELF files:

const INPUT_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/lambdavm_input.bin");
const PROOF_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/lambdavm_proof.bin");
const ELF_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/zkvm-lambdavm-program");

If multiple LambdaVmBackend instances run concurrently (e.g., in a multi-threaded prover service), they will overwrite each other's input/output files, leading to corrupted proofs or verification failures.

Recommendation: Use unique temporary files per invocation. Pattern used in write_elf_file (lines 59–60) should be extended to inputs and proofs:

let input_path = format!("{}/lambdavm_input.{}.bin", std::env::temp_dir().display(), std::process::id());

2. Supply Chain Risk: No Checksum Verification (Security)

File: .github/actions/install-lambdavm/action.yml
Lines: 31–41

The action downloads a sysroot tarball from an external domain without checksum verification:

retry curl -L "${SYSROOT_URL}" -o "${SYSROOT_TARBALL}"

Recommendation: Add a SHA256 checksum check after download (and before extraction) to prevent supply chain attacks if the CDN is compromised:

echo "expected_sha256  lambda-vm-sysroot-rv64im.tar.gz" | sha256sum -c -

3. Future Rust Toolchain Date (Verify)

File: .github/actions/install-lambdavm/action.yml (line 15)
File: crates/guest-program/bin/lambdavm/rust-toolchain.toml (line 2)

The toolchain is pinned to nightly-2026-02-01. If this PR is being reviewed before February 2026, this date is in the future and the toolchain may not exist or may change before that date.

Recommendation: Verify this is the correct, intended date. If LambdaVM requires a specific future nightly, add a comment explaining why this specific date is required.


4. Error Handling in Guest Deserialization

File: crates/guest-program/bin/lambdavm/src/main.rs
Line: 16

let input = { rkyv::from_bytes::<ProgramInput, Error>(&input).unwrap() };

While panicking in a zkVM guest is acceptable (it produces a failed proof), consider using expect with a descriptive message to aid debugging:

.expect("failed to deserialize ProgramInput")

5. Missing #[cfg] Guard in build.rs

File: crates/guest-program/build.rs
Lines: 225–300

The build_lambdavm_program function is conditionally compiled, but the #[cfg] attribute on the shared rustc_path helper function (lines 295+) was changed to include lambdavm-build-elf. Verify this doesn't break Zisk builds if the function signatures differ, though the current implementation appears safe.


6. Documentation Nit

File: crates/guest-program/README.md
Line: 168

The documentation states ECDSA runs via "pure-Rust crates (slow inside the zkVM until more precompiles land)". This is accurate based on the LambdaVmCrypto implementation routing through k256, but note that the comment in crates/guest-program/src/crypto/lambdavm.rs (lines 12–16) mentions forward compatibility with [patch.crates-io]. Ensure the README mentions that users can accelerate ECDSA by patching k256 in bin/lambdavm/Cargo.toml if LambdaVM releases patches.


Positive Notes

  1. Atomic ELF writes: The write_elf_file method (lines 50–72) correctly uses the .tmp + rename pattern to prevent corruption during concurrent writes.
  2. Consistent versioning: The commit hash 8af4e92e6e75638746fe9924f6bac715ffac9cb6 is consistently referenced in both Cargo.toml and the GitHub Action.
  3. Keccak implementation: The sponge construction in keccak256_via_lambdavm (lines 50–88) correctly implements rate 1088, capacity 512, and multi-rate padding (0x01...0x80) with proper little-endian lane ordering.
  4. Feature flags: Clean separation between lambdavm (runtime) and lambdavm-build-elf (compile-time) prevents unnecessary rebuilds in CI.

Summary: Address the race condition (Item 1) and add checksum verification (Item 2) before merging. The rest are minor suggestions or verification items.


Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt

@github-actions
Copy link
Copy Markdown

Lines of code report

Total lines added: 353
Total lines removed: 0
Total lines changed: 353

Detailed view
+------------------------------------------------------+-------+------+
| File                                                 | Lines | Diff |
+------------------------------------------------------+-------+------+
| ethrex/crates/guest-program/bin/lambdavm/src/main.rs | 21    | +21  |
+------------------------------------------------------+-------+------+
| ethrex/crates/guest-program/build.rs                 | 256   | +63  |
+------------------------------------------------------+-------+------+
| ethrex/crates/guest-program/src/crypto/lambdavm.rs   | 62    | +62  |
+------------------------------------------------------+-------+------+
| ethrex/crates/guest-program/src/crypto/mod.rs        | 13    | +2   |
+------------------------------------------------------+-------+------+
| ethrex/crates/guest-program/src/lib.rs               | 57    | +5   |
+------------------------------------------------------+-------+------+
| ethrex/crates/prover/src/backend/lambdavm.rs         | 184   | +184 |
+------------------------------------------------------+-------+------+
| ethrex/crates/prover/src/backend/mod.rs              | 104   | +8   |
+------------------------------------------------------+-------+------+
| ethrex/crates/prover/src/prover.rs                   | 257   | +8   |
+------------------------------------------------------+-------+------+

@github-actions
Copy link
Copy Markdown

🤖 Codex Code Review

Findings

  1. crates/prover/src/backend/lambdavm.rs:150, crates/prover/src/backend/lambdavm.rs:221, crates/prover/src/prover.rs:142, crates/prover/src/prover.rs:286: BackendType::LambdaVM is now selectable in the generic prover loop, but the backend still cannot participate in that loop. request_new_input() calls self.backend.prover_type(), which is unimplemented!(), so the prover will panic on its first poll. Even if that gets patched, to_proof_bytes() is still NotImplemented, so proofs still cannot be submitted. Either keep LambdaVM out of BackendType/start_prover() until the protocol supports it, or implement the full ProverType/submission path first.

  2. crates/prover/Cargo.toml:64, crates/guest-program/src/lib.rs:58, crates/prover/src/backend/lambdavm.rs:38, docs/zkvm-integrations.md:118: the plain lambdavm feature does not build/embed the guest ELF; ZKVM_LAMBDAVM_PROGRAM_ELF is &[] unless lambdavm-build-elf is enabled. In that state write_elf_file() will happily write a zero-byte zkvm-lambdavm-program, and cli execute/prove will fail at runtime. The docs currently tell users to run with --features lambdavm, which is not sufficient on a clean checkout. This should either hard-error on an empty embedded ELF or wire the intended consumer feature to lambdavm-build-elf.

  3. crates/prover/src/backend/lambdavm.rs:91, crates/prover/src/backend/lambdavm.rs:178, docs/zkvm-integrations.md:111: ProofFormat is accepted by the API but ignored by prove_core(). A caller asking for Groth16 gets the same LambdaVM-native proof as Compressed, even though the docs explicitly say an L1-cheap wrapper proof is not available yet. That is a contract violation in a proof API; unsupported formats should return NotImplemented instead of silently producing the wrong artifact.

  4. .github/actions/install-lambdavm/action.yml:34: the CI action downloads and extracts the LambdaVM sysroot tarball without any checksum/signature verification. Since this sysroot directly affects guest compilation and therefore proof semantics, this is a supply-chain risk. I’d strongly prefer a pinned digest check before extraction.

I didn’t spot an obvious correctness bug in the guest-side Keccak sponge itself, but this path is consensus-critical enough that it should have known-answer tests before merge.

I couldn’t run cargo check in this environment because rustup tries to write under a read-only temp dir in the sandbox.


Automated review by OpenAI Codex · gpt-5.4 · custom prompt

@github-actions
Copy link
Copy Markdown

🤖 Claude Code Review

Now I have enough context for a thorough review. Let me write it up.


Code Review: PR #6722 — feat(l1): add LambdaVM zkVM backend for the L1 prover

Overall, the integration follows the established Zisk pattern cleanly and the Keccak sponge implementation looks correct. Several issues are worth addressing before merge.


Supply chain / Security

Sysroot download has no integrity verification.

In .github/actions/install-lambdavm/action.yml (line 44):

retry curl -L "${SYSROOT_URL}" -o "${SYSROOT_TARBALL}"
sudo tar -xzf "${SYSROOT_TARBALL}" ...

There is no sha256sum (or equivalent) check after download. A compromised lambda.alignedlayer.com or a CDN cache poisoning attack could serve a malicious sysroot that ships a backdoored compiler/linker and silently corrupts every guest ELF built in CI. The LambdaVM git clone is safe (commit hash is content-addressed), but the sysroot tarball is not.

Recommendation: Add a hardcoded expected hash and verify it before unpacking:

echo "<expected-sha256>  ${SYSROOT_TARBALL}" | sha256sum -c

Correctness

prover_type() panics unconditionally (crates/prover/src/backend/lambdavm.rs, line ~153):

fn prover_type(&self) -> ProverType {
    unimplemented!("LambdaVM is not yet enabled as a backend for the L2")
}

Any L2 infrastructure code that queries prover_type() before checking which backend is active will panic in production. The trait presumably can't return a Result here, but the existing pattern for other unfinished backends should be checked — at minimum the panic message should make clear this is intentional. If other backends use the same unimplemented!() for L2 stubs, this is consistent and can stay; otherwise it needs guarding.


Error messages contain empty stderr (crates/prover/src/backend/lambdavm.rs, e.g. lines ~85, ~117, ~132):

Stdio::inherit() is set for stderr on every Command, which routes the child's stderr directly to the terminal. When .output() is used, output.stderr is therefore always an empty Vec. All three error paths do:

format!("LambdaVM execution failed: {}", String::from_utf8_lossy(&output.stderr))

…and will always emit "LambdaVM execution failed: " with nothing after. The stderr content is visible in the terminal but not in the returned BackendError. This matches the Zisk backend's approach, but the format string is still misleading. Consider:

"LambdaVM execution failed (see stderr above)"

Keccak implementation (crates/guest-program/src/crypto/lambdavm.rs)

The sponge logic is correct: padding with 0x01 / 0x80 (original Keccak, not SHA-3's 0x06), rate = 136 bytes, squeeze of the first 4 lanes in little-endian — all match the Ethereum keccak256 specification.

Two style issues worth noting:

  • Unnecessary saturating arithmetic (lines ~56, ~62, ~76, ~78): Operations like i.saturating_mul(8), offset.saturating_add(RATE), and input.len().saturating_sub(offset) are dead-code defensive guards — offset is always <= input.len(), and i is always 0..4. Plain arithmetic with a debug assert is clearer. The defensive use of saturating_* here obscures the actual invariants.

  • absorb_block is silently tolerant of wrong-sized input (line ~88):

    fn absorb_block(state: &mut [u64; 25], block: &[u8]) {
        for (lane, chunk) in state.iter_mut().zip(block.chunks_exact(8)) {

    If block.len() < 136, zip stops early and some lanes are silently skipped, producing a wrong hash. All call sites pass exactly RATE bytes, so this is safe today, but the missing assertion (debug_assert_eq!(block.len(), RATE)) is a latent bug risk for anyone who reuses this function.


Build script (crates/guest-program/build.rs)

create_dir error silently swallowed (approx. line near the ELF copy):

let _ = std::fs::create_dir("./bin/lambdavm/out");

io::ErrorKind::AlreadyExists should be ignored; other errors (permissions, disk full) should not. Consider:

std::fs::create_dir("./bin/lambdavm/out")
    .or_else(|e| if e.kind() == ErrorKind::AlreadyExists { Ok(()) } else { Err(e) })
    .expect("could not create lambdavm/out dir");

Printf format (line near duration.as_secs_f64()):

println!("LambdaVM guest program built in {:.2?} seconds", duration.as_secs_f64());

{:.2?} applies the Debug format with 2 decimal places to an f64. This works but is unconventional. Use {:.2} (Display format).


CI / Caching

Sysroot cache key is a static string (action.yml, line 33):

key: lambda-vm-sysroot-rv64im-v1

If the sysroot content changes without a new v2 key, CI will continue serving the stale cached version silently. Tie the key to the sysroot's expected checksum (solving the integrity issue above) or to an explicit version in the action inputs.

GITHUB_TOKEN is passed to the install action but is unused inside it (.github/workflows/pr-main_l2_prover.yaml, line ~99):

- name: Install LambdaVM
  if: matrix.backend == 'lambdavm'
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  uses: ./.github/actions/install-lambdavm

The action performs a plain git clone with no authentication, so GITHUB_TOKEN is not needed. Remove the env: block.


Minor / Non-blocking

  • Hardcoded sysroot path in .cargo/config.toml (line 622): CFLAGS_riscv64im_lambda_vm_elf = "--sysroot=/opt/lambda-vm-sysroot" ignores LAMBDA_VM_SYSROOT_DIR. The build.rs sets this env var correctly for host-driven builds, but if a developer builds the guest workspace directly with cargo build and LAMBDA_VM_SYSROOT_DIR points elsewhere, the wrong sysroot is used. A comment documenting this limitation would help.

  • Temporary files never cleaned up: INPUT_PATH and PROOF_PATH persist on disk after proving. These contain serialized ProgramInput, which may include sensitive witness data. Same caveat applies to the Zisk backend. Worth a follow-up issue to clean up after proving/verification.

  • LambdaVmBackend::new() is redundant given #[derive(Default)]. Minor.


What looks good

  • The write_elf_file .tmp + atomic rename pattern is solid.
  • Version pinning is consistent across Cargo.toml, action.yml, and Cargo.lock.
  • Feature flag structure (lambdavm / lambdavm-build-elf) mirrors Zisk correctly.
  • README and docs/zkvm-integrations.md updates are thorough.
  • serialize_input correctly bounds-checks the 4-byte LE length prefix.
  • parse_proving_time gracefully falls back to wall-clock on parse failure.

Automated review by Claude (Anthropic) · sonnet · custom prompt

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds LambdaVM as an additional zkVM backend within ethrex’s existing prover/guest-program integration pattern, including guest build tooling, host backend glue, CI installation, and documentation updates.

Changes:

  • Introduces a LambdaVM guest workspace + target spec and wires guest ELF embedding/build via lambdavm-build-elf.
  • Adds a LambdaVM host backend (execute/prove/verify) and registers it behind the lambdavm feature flag.
  • Extends CI, release, and developer docs to include the new detached workspace and integration steps.

Reviewed changes

Copilot reviewed 17 out of 23 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
Makefile Tracks LambdaVM detached workspace in lockfile update/check targets.
docs/zkvm-integrations.md Documents LambdaVM status, features, limitations, and usage.
docs/developers/release-process.md Adds LambdaVM detached Cargo.toml/Cargo.lock to release bump expectations.
crates/prover/src/prover.rs Adds BackendType::LambdaVM dispatch arm (feature-gated).
crates/prover/src/backend/mod.rs Registers lambdavm module and BackendType::LambdaVM (feature-gated).
crates/prover/src/backend/lambdavm.rs New LambdaVM backend that shells out to LambdaVM CLI and handles input/proof files.
crates/prover/Cargo.toml Adds lambdavm and lambdavm-build-elf features.
crates/guest-program/src/lib.rs Adds ZKVM_LAMBDAVM_PROGRAM_ELF embedding constant behind feature gates.
crates/guest-program/src/crypto/mod.rs Adds LambdaVM crypto module and includes shared helpers for it.
crates/guest-program/src/crypto/lambdavm.rs New LambdaVmCrypto with keccak-permute syscall + k256 ECDSA recovery.
crates/guest-program/README.md Expands “Integrating a New zkVM” guide and documents LambdaVM guest details/features.
crates/guest-program/Makefile Adds lambdavm / l2-lambdavm build targets and clean rule update.
crates/guest-program/Cargo.toml Adds lambda-vm-syscalls dep + lambdavm/lambdavm-build-elf features.
crates/guest-program/build.rs Adds LambdaVM guest build step requiring sysroot + pinned nightly.
crates/guest-program/bin/lambdavm/src/main.rs New LambdaVM guest entrypoint reading private input + committing encoded output.
crates/guest-program/bin/lambdavm/rust-toolchain.toml Pins guest workspace toolchain to nightly-2026-02-01 + rust-src.
crates/guest-program/bin/lambdavm/riscv64im-lambda-vm-elf.json Adds custom RV64IM target spec for LambdaVM ELF builds.
crates/guest-program/bin/lambdavm/Cargo.toml New detached workspace manifest for LambdaVM guest.
crates/guest-program/bin/lambdavm/Cargo.lock New detached workspace lockfile for LambdaVM guest.
crates/guest-program/bin/lambdavm/.cargo/config.toml Target-specific rustflags and CC/CFLAGS env wiring for sysroot builds.
Cargo.lock Adds transitive deps for lambda-vm-syscalls in the root workspace lock.
.github/workflows/pr-main_l2_prover.yaml Adds lambdavm to backend lint matrix and installs LambdaVM in CI.
.github/actions/install-lambdavm/action.yml New composite action to install nightly, sysroot, and build/cache LambdaVM CLI.
Comments suppressed due to low confidence (1)

.github/workflows/pr-main_l2_prover.yaml:97

  • CI currently runs cargo check/clippy with -F "${{ matrix.backend }},ci". For LambdaVM this does not enable lambdavm-build-elf (or build ethrex-guest-program --features lambdavm-build-elf), so the new guest ELF build + sysroot wiring in crates/guest-program/build.rs won’t be exercised in CI.
      - name: Check ${{ matrix.backend }} backend
        run: |
          cargo check -r -p ethrex-prover -F "${{ matrix.backend }},ci"


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +70 to +71
let output = Command::new("cli")
.args(["execute", ELF_PATH, "--private-input", INPUT_PATH])
Comment on lines +72 to +75
.stdin(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.map_err(BackendError::execution)?;
Comment on lines +87 to +94
/// Prove assuming input is already serialized to INPUT_PATH.
///
/// Returns the proof bytes plus the parsed stdout (used by `prove_timed`
/// to extract `Proving time: <float>s`).
fn prove_core(
&self,
_format: ProofFormat,
) -> Result<(LambdaVmProveOutput, String), BackendError> {
Comment on lines +26 to +42
path: /opt/lambda-vm-sysroot
key: lambda-vm-sysroot-rv64im-v1

- name: Prepare LambdaVM sysroot
if: steps.cache-sysroot.outputs.cache-hit != 'true'
shell: bash
env:
SYSROOT_TARBALL: /tmp/lambda-vm-sysroot-rv64im.tar.gz
SYSROOT_URL: https://lambda.alignedlayer.com/lambda-vm-sysroot-rv64im.tar.gz
run: |
source "$GITHUB_WORKSPACE/.github/scripts/retry.sh"

retry curl -L "${SYSROOT_URL}" -o "${SYSROOT_TARBALL}"
sudo mkdir -p /opt/lambda-vm-sysroot
sudo tar -xzf "${SYSROOT_TARBALL}" -C /opt/lambda-vm-sysroot --strip-components=1
rm -f "${SYSROOT_TARBALL}"

Comment on lines +43 to +47
- name: Cache LambdaVM CLI binary
id: cache-cli
uses: actions/cache@v4
with:
path: ~/.cargo/bin/cli
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 22, 2026

Greptile Summary

This PR integrates LambdaVM (yetanotherco/lambda_vm) as a new zkVM backend for the L1 prover, mirroring the existing ZisK integration pattern. It adds a guest ELF workspace (bin/lambdavm/), a LambdaVmCrypto adapter that accelerates Keccak-256 via the keccak_permute syscall, a host backend that shells out to the cli binary, CI plumbing, and documentation.

  • Guest side: custom RV64IM target spec, pinned nightly-2026-02-01 toolchain, hand-rolled Keccak-256 sponge using keccak_permute, ECDSA via shared k256 helpers — all matching the OpenVM/ZisK adapter shape.
  • Host side: LambdaVmBackend follows the ZisK shell-out pattern for execute/prove/verify, with atomic ELF writes and a parse_proving_time helper that extracts self-reported durations from CLI stdout.
  • Limitations noted: only keccak_permute is accelerated; prover_type() is unimplemented!() (L2 not wired, same as ZisK).

Confidence Score: 4/5

The new LambdaVM backend is structurally sound and faithfully mirrors the ZisK integration. The issues found are quality/observability concerns that won't break the happy path.

Three non-blocking issues exist: BackendError messages will contain empty strings when the CLI fails (because Stdio::inherit() on stderr is used alongside output.stderr), parse_proving_time can panic on a NaN or negative f64 from Duration::from_secs_f64, and the cli binary name is very generic and could resolve to a different tool on PATH. None of these affect correct end-to-end proving when the environment is set up as expected; they surface only in error/edge-case paths.

crates/prover/src/backend/lambdavm.rs — the three issues (empty error messages, parse_proving_time panic guard, generic binary name) all concentrate here.

Important Files Changed

Filename Overview
crates/prover/src/backend/lambdavm.rs New LambdaVM host backend that shells out to the cli binary. Three issues: Stdio::inherit() causes empty stderr in all BackendError messages; parse_proving_time can panic on NaN/negative f64 via Duration::from_secs_f64; the generic cli binary name is fragile on PATH.
crates/guest-program/src/crypto/lambdavm.rs New LambdaVmCrypto adapter implementing Keccak-256 via LambdaVM's keccak_permute syscall. Padding (0x01/0x80), rate (136 bytes), lane ordering, and squeeze all match the Keccak-256 spec. ECDSA delegation to shared k256 helpers mirrors the OpenVM pattern correctly.
.github/actions/install-lambdavm/action.yml New composite CI action that installs the nightly toolchain, downloads/caches the sysroot, and builds/caches the cli binary. Caching keys are pinned to the commit hash. Mirrors existing ZisK/SP1 install action patterns well.
crates/guest-program/build.rs Adds build_lambdavm_program that resolves the sysroot path, invokes the nightly cargo build with correct toolchain isolation (clears RUSTC/CARGO/RUSTUP_TOOLCHAIN), and copies the ELF to bin/lambdavm/out/. Pattern matches the ZisK build function.
crates/prover/src/backend/mod.rs Registers LambdaVM variant in BackendType enum and FromStr, with correct cfg(feature = "lambdavm") guards matching every other backend.
crates/guest-program/bin/lambdavm/src/main.rs Thin guest entry point: reads private input via LambdaVM syscall, deserializes with rkyv, runs execution_program with LambdaVmCrypto, and commits the output. Mirrors the ZisK guest wrapper faithfully.

Sequence Diagram

sequenceDiagram
    participant P as ethrex-prover
    participant B as LambdaVmBackend
    participant FS as Filesystem
    participant CLI as cli binary

    P->>B: prove(input, format)
    B->>FS: write_elf_file()
    B->>B: serialize_input - rkyv + 4-byte LE length prefix
    B->>FS: write lambdavm_input.bin
    B->>CLI: cli prove elf -o proof --private-input input --time
    CLI-->>FS: write lambdavm_proof.bin
    CLI-->>B: stdout with Proving time
    B->>FS: read lambdavm_proof.bin
    B-->>P: LambdaVmProveOutput + Duration

    P->>B: verify(proof)
    B->>FS: write_elf_file()
    B->>FS: write lambdavm_proof.bin
    B->>CLI: cli verify proof elf
    CLI-->>B: exit code
    B-->>P: Ok or BackendError
Loading
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
crates/prover/src/backend/lambdavm.rs:69-85
**Empty stderr in error messages**

`execute_core`, `prove_core`, and `verify_core` all set `.stderr(Stdio::inherit())`, which forwards the child's stderr directly to the parent process — so `output.stderr` will always be an empty `Vec`. The `String::from_utf8_lossy(&output.stderr)` in the three error-message `format!` calls will always produce an empty string, rendering `BackendError` messages like `"LambdaVM execution failed: "` with no diagnostic content. Any caller that logs or surfaces the `BackendError` text gets no information about what actually went wrong. The same pattern exists in the ZisK backend; consider fixing both by using `Stdio::piped()` for stderr when diagnostic capture is needed, or removing the `output.stderr` interpolation from the error string.

### Issue 2 of 3
crates/prover/src/backend/lambdavm.rs:238-240
`Duration::from_secs_f64` panics if the parsed `f64` is `NaN`, infinite, or negative. `str::parse::<f64>()` succeeds for `"nan"`, `"inf"`, and `"-1.0"`, so if the CLI ever emits such a value the prover thread will panic instead of falling back to wall-clock time. Adding a finite/non-negative guard keeps the fallback path intact.

```suggestion
            if let Ok(secs) = secs_str.parse::<f64>() {
                if secs.is_finite() && secs >= 0.0 {
                    return Some(Duration::from_secs_f64(secs));
                }
            }
```

### Issue 3 of 3
crates/prover/src/backend/lambdavm.rs:70-75
**Generic `cli` binary name**

`Command::new("cli")` resolves against `$PATH`, and the binary is installed as `~/.cargo/bin/cli`. The name `cli` is extremely common — if any other `cli` binary appears earlier on the user's `$PATH`, the prover will silently invoke the wrong binary and produce confusing failures. Using a more specific binary name (e.g., `lambda-vm-cli`) in both the install action and the `Command::new` calls would eliminate this ambiguity.

Reviews (1): Last reviewed commit: "Fix the LambdaVM guest build script so t..." | Re-trigger Greptile

Comment on lines +69 to +85
fn execute_core(&self) -> Result<(), BackendError> {
let output = Command::new("cli")
.args(["execute", ELF_PATH, "--private-input", INPUT_PATH])
.stdin(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.map_err(BackendError::execution)?;

if !output.status.success() {
return Err(BackendError::execution(format!(
"LambdaVM execution failed: {}",
String::from_utf8_lossy(&output.stderr)
)));
}

Ok(())
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Empty stderr in error messages

execute_core, prove_core, and verify_core all set .stderr(Stdio::inherit()), which forwards the child's stderr directly to the parent process — so output.stderr will always be an empty Vec. The String::from_utf8_lossy(&output.stderr) in the three error-message format! calls will always produce an empty string, rendering BackendError messages like "LambdaVM execution failed: " with no diagnostic content. Any caller that logs or surfaces the BackendError text gets no information about what actually went wrong. The same pattern exists in the ZisK backend; consider fixing both by using Stdio::piped() for stderr when diagnostic capture is needed, or removing the output.stderr interpolation from the error string.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/prover/src/backend/lambdavm.rs
Line: 69-85

Comment:
**Empty stderr in error messages**

`execute_core`, `prove_core`, and `verify_core` all set `.stderr(Stdio::inherit())`, which forwards the child's stderr directly to the parent process — so `output.stderr` will always be an empty `Vec`. The `String::from_utf8_lossy(&output.stderr)` in the three error-message `format!` calls will always produce an empty string, rendering `BackendError` messages like `"LambdaVM execution failed: "` with no diagnostic content. Any caller that logs or surfaces the `BackendError` text gets no information about what actually went wrong. The same pattern exists in the ZisK backend; consider fixing both by using `Stdio::piped()` for stderr when diagnostic capture is needed, or removing the `output.stderr` interpolation from the error string.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +238 to +240
if let Ok(secs) = secs_str.parse::<f64>() {
return Some(Duration::from_secs_f64(secs));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Duration::from_secs_f64 panics if the parsed f64 is NaN, infinite, or negative. str::parse::<f64>() succeeds for "nan", "inf", and "-1.0", so if the CLI ever emits such a value the prover thread will panic instead of falling back to wall-clock time. Adding a finite/non-negative guard keeps the fallback path intact.

Suggested change
if let Ok(secs) = secs_str.parse::<f64>() {
return Some(Duration::from_secs_f64(secs));
}
if let Ok(secs) = secs_str.parse::<f64>() {
if secs.is_finite() && secs >= 0.0 {
return Some(Duration::from_secs_f64(secs));
}
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/prover/src/backend/lambdavm.rs
Line: 238-240

Comment:
`Duration::from_secs_f64` panics if the parsed `f64` is `NaN`, infinite, or negative. `str::parse::<f64>()` succeeds for `"nan"`, `"inf"`, and `"-1.0"`, so if the CLI ever emits such a value the prover thread will panic instead of falling back to wall-clock time. Adding a finite/non-negative guard keeps the fallback path intact.

```suggestion
            if let Ok(secs) = secs_str.parse::<f64>() {
                if secs.is_finite() && secs >= 0.0 {
                    return Some(Duration::from_secs_f64(secs));
                }
            }
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +70 to +75
let output = Command::new("cli")
.args(["execute", ELF_PATH, "--private-input", INPUT_PATH])
.stdin(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.map_err(BackendError::execution)?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Generic cli binary name

Command::new("cli") resolves against $PATH, and the binary is installed as ~/.cargo/bin/cli. The name cli is extremely common — if any other cli binary appears earlier on the user's $PATH, the prover will silently invoke the wrong binary and produce confusing failures. Using a more specific binary name (e.g., lambda-vm-cli) in both the install action and the Command::new calls would eliminate this ambiguity.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/prover/src/backend/lambdavm.rs
Line: 70-75

Comment:
**Generic `cli` binary name**

`Command::new("cli")` resolves against `$PATH`, and the binary is installed as `~/.cargo/bin/cli`. The name `cli` is extremely common — if any other `cli` binary appears earlier on the user's `$PATH`, the prover will silently invoke the wrong binary and produce confusing failures. Using a more specific binary name (e.g., `lambda-vm-cli`) in both the install action and the `Command::new` calls would eliminate this ambiguity.

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

L1 Ethereum client

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants