Skip to content

feat(zkevm): SP1 platform for ZKEVM guests (eth-act zkvm-standards)#2763

Open
tamirhemo wants to merge 47 commits into
mainfrom
tamir/zkevm
Open

feat(zkevm): SP1 platform for ZKEVM guests (eth-act zkvm-standards)#2763
tamirhemo wants to merge 47 commits into
mainfrom
tamir/zkevm

Conversation

@tamirhemo
Copy link
Copy Markdown
Contributor

@tamirhemo tamirhemo commented Apr 29, 2026

Experimental Implementation of the eth-act zkvm-standards

@tamirhemo tamirhemo force-pushed the tamir/zkevm branch 2 times, most recently from 2ffabb1 to 124fd32 Compare April 29, 2026 07:33
@tamirhemo tamirhemo force-pushed the tamir/zkevm branch 2 times, most recently from 62f4869 to f24c16d Compare April 29, 2026 08:41
@tamirhemo tamirhemo changed the title feat(zkevm-sdk): SP1 platform SDK for non-Rust guests (eth-act zkvm-standards C ABI) feat(zkevm): SP1 platform for ZKEVM guests (eth-act zkvm-standards C ABI) Apr 29, 2026
@tamirhemo tamirhemo changed the title feat(zkevm): SP1 platform for ZKEVM guests (eth-act zkvm-standards C ABI) feat(zkevm): SP1 platform for ZKEVM guests (eth-act zkvm-standards ABI) Apr 29, 2026
@tamirhemo tamirhemo changed the title feat(zkevm): SP1 platform for ZKEVM guests (eth-act zkvm-standards ABI) feat(zkevm): SP1 platform for ZKEVM guests (eth-act zkvm-standards) Apr 29, 2026
Base automatically changed from tamir/no_std_zkvm to main April 30, 2026 21:26
tamirhemo and others added 20 commits April 30, 2026 21:35
…zkvm-standards C ABI

# Summary

Scaffolding for a platform SDK that lets non-Rust guests (C / TinyGo /
Zig) target SP1 against the
[eth-act/zkvm-standards](https://github.com/eth-act/zkvm-standards) C
ABI. **Scaffolding only** — every precompile body is a stub returning
`ZKVM_EFAIL`; humans will fill those in by calling SP1's patched no-std
crypto crates (`sha2`, `sha3`, `crypto-bigint`, …).

# Layout

```
zkevm-sdk/
├── Makefile                # produces sdk/{libzkevm.a, zkvm.ld, headers}
├── zkvm.ld                 # linker script (ENTRY(_start) -> sp1-zkvm)
├── include/                # vendored zkvm_accelerators.h
├── libzkevm/               # rlib (member of the SP1 root workspace)
├── libzkevm-cabi/          # staticlib facade (own workspace, panic=abort)
└── examples/
    ├── hello-c/            # C smoke test (clang + ld.lld)
    └── hello-rust/         # Rust smoke test (own workspace)
```

# Why three workspaces

- `libzkevm` lives in the **SP1 root workspace** so it can depend on
  `sp1-zkvm` directly (now no_std, see base PR) and reuse the patched
  crypto crates when implementing precompile bodies.
- `libzkevm-cabi` is in its **own workspace** because a `#![no_std]`
  staticlib requires `panic = "abort"`, which cargo only supports as a
  workspace-level setting.
- `examples/hello-rust` is in its **own workspace** for the same reason.

# What's wired

- `extern "C"` symbols match the eth-act header byte-for-byte
- `zkvm_halt` / `exit` / `_exit` / `abort` -> `sp1_zkvm::syscalls::syscall_halt`
  (commits public-values digest before HALT for free)
- `write_output` -> `sp1_zkvm::syscalls::syscall_write` against
  `FD_PUBLIC_VALUES` (feeds the public-values hasher for free)
- `read_input` -> `read_vec_raw` (one-shot drain; first chunk only —
  contract is that the host pushes the entire input as one chunk)
- `_start` comes from `sp1-zkvm`; no separate `crt0.o` needed

# What's stubbed

Every function in `libzkevm/src/precompile/*` is a stub that returns
`ZKVM_EFAIL` after issuing a placeholder `0xDEAD_*` ecall. The suggested
SP1 mapping per function is in `libzkevm/src/precompile/mod.rs`.

# Verification

- `cargo check --workspace` clean (the SP1 root)
- `cargo check` clean for `libzkevm-cabi` and `examples/hello-rust`
- `cargo fmt --all -- --check` clean
- End-to-end `make example && sp1 prove` is a follow-up

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two more `examples/` to round out the demo set beyond hello-{c,rust}'s
IO round-trip:

  * `fibonacci/` — Rust guest that reads `u32 n`, computes
    `fib(n) % 7919`, and writes the result. Same shape as SP1's stock
    `examples/fibonacci/program/` so cycle counts are roughly comparable
    (~18.7k cycles for n=1000). Demonstrates that "normal" arithmetic
    code runs cleanly through libzkevm's C ABI.

  * `panic/` — Rust guest that reads a 1-byte flag; if non-zero, panics.
    Demonstrates the failed-termination path (panic_handler from the
    succinct toolchain's std stub routes to syscall_halt(1)). The script
    runs both the success path (flag=0) and the panic path (flag=1) and
    reports what SP1 surfaces in each case.

Wiring:
  * Both `*-script` crates added to the SP1 root workspace.
  * Both guests are stand-alone workspaces (panic = "abort"), excluded
    from root.
  * `make example-fibonacci-{execute,prove}` and
    `make example-panic-execute` Makefile targets.
  * README updated with smoke-test transcripts.

Verified end-to-end on this box (CPU prover):
  * fibonacci execute: 18726 cycles, fib(1000) % 7919 = 5965
  * fibonacci prove:   core proof generated + verified (~15.5s)
  * panic flag=0:      4678 cycles, "no panic" output
  * panic flag=1:      8663 cycles, executor returns Ok with empty
                       public values (panic happened pre-write_output)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First non-stub precompile body in libzkevm. The general pattern: most
accelerator implementations sit on top of one or more SP1 syscalls plus
some software bookkeeping. SP1's KECCAK_PERMUTE precompile only does
the inner keccak-f[1600] permutation; the sponge construction (absorb /
pad / squeeze) stays in libzkevm.

# What

`libzkevm/src/precompile/hash.rs::zkvm_keccak256`:
  * Allocates a 25-lane (200-byte) sponge state, addressable as both
    bytes and `[u64; 25]` for the syscall.
  * Absorbs full 136-byte rate-sized chunks via XOR + permutation.
  * Pads the final chunk with `0x01 ... 0x80` (Keccak padding, NOT
    SHA-3 which uses `0x06`).
  * Calls `sp1_zkvm::syscalls::syscall_keccak_permute` for each
    permutation step (dispatches to KECCAK_PERMUTE precompile,
    `t0 = 0x00_01_01_09`).
  * Squeezes the first 32 bytes as the digest.

# Why not the patched tiny-keccak crate

Tried `tiny-keccak = { git = "...sp1-patches/tiny-keccak", tag = "patch-2.0.2-sp1-6.0.0" }`
first. The patched tag was cut against SP1 6.0 and pulls in
`p3-field 0.3.1-succinct` transitively; SP1 6.1 is on
`p3-field 0.3.3-succinct`, and the resolver can't reconcile the two
within a single workspace. Driving the sponge by hand on top of the
syscall avoids the version-pin drift entirely and is small enough
(~30 lines).

# New example: examples/keccak/

  * `program/`: reads bytes via libzkevm IO, calls `zkvm_keccak256`,
    writes the 32-byte digest.
  * `script/execute.rs`: tries five input shapes (empty, < rate,
    = rate, > rate, arbitrary), asserts each matches a host-side
    reference computed with stock `tiny_keccak::Keccak::v256()`.
  * `script/prove.rs`: generates + verifies a core proof.

# Verified end-to-end

  * `make example-keccak-execute` — all 5 shapes match host reference.
    Permutation counts are correct (1 for input < rate, 2 once you
    cross the rate boundary because of the final padding chunk).
  * `make example-keccak-prove` — 17.3s on CPU; "generate Keccak
    trace" log lines confirm the precompile circuit is actually being
    constrained inside the proof.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# What

  * libzkevm now depends on `tiny-keccak` from
    `sp1-patches/tiny-keccak#patch-2.0.2-sp1-6.2.0-pre`, a freshly cut
    pre-release tag of the SP1-patched fork. Its `keccakf` is replaced
    with `sp1_lib::syscall_keccak_permute(...)` at `target_os = "zkvm"`;
    falls back to the stock software impl on host.
  * `precompile::hash::zkvm_keccak256` is now a thin wrapper around
    `tiny_keccak::Keccak::v256()` — drops the hand-rolled sponge
    bookkeeping (~30 lines).

# Why a new patch tag

The existing `patch-2.0.2-sp1-6.0.0` tag pulls `sp1-lib = "6.0.0"`,
which transitively pins `p3-field 0.3.1-succinct`. SP1 main is on
`0.3.3-succinct`, so cargo can't reconcile the two when the patched
tiny-keccak is consumed from a current-SP1 workspace. The new
`-6.2.0-pre` tag repoints `sp1-lib` to a git ref of the
`tamir/no_std_zkvm` branch (the pre-merge home of the no_std refactor
in #2762), so all SP1 crates resolve to a single source
tree.

When 6.2.0 ships we'll cut `patch-2.0.2-sp1-6.2.0` against
`sp1-lib = "6.2.0"` from crates.io and bump the tag here.

# Verified

`cargo run -p keccak-script --bin keccak-execute` — all 5 input shapes
match the host-computed reference. `keccak_permute_calls` matches the
hand-rolled version (1 for input < rate, 2 once you cross rate).
Cycle counts are slightly higher (~3% — tiny-keccak's general-purpose
sponge has a bit more bookkeeping than my targeted version, fine
trade for not maintaining a custom sponge).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# What

  * libzkevm now depends on `sha2` from
    `sp1-patches/RustCrypto-hashes#patch-sha2-0.10.9-sp1-6.2.0-pre`
    (default-features = false). The patched `compress256` calls
    `syscall_sha256_extend` + `syscall_sha256_compress` at
    `target_os = "zkvm"`; on host it's the stock RustCrypto impl.
  * `precompile::hash::zkvm_sha256` is now a thin wrapper around
    `sha2::Sha256::new().update(...).finalize()`.

# Why a new patch tag

`RustCrypto-hashes` doesn't have a `sp1-lib` dependency (it declares
the syscall extern symbols directly), so the new tag is just an alias
of `patch-sha2-0.10.9-sp1-6.0.0`. No content change — just a version-
named alias for symmetry with the other sp1-patches repos and to keep
all SP1 6.2.0 patches lined up under the same `-sp1-6.2.0-pre` suffix.

# New example: examples/sha256/

Mirror of `examples/keccak/`. Pushes 5 input shapes (empty, 11, 64,
200, 43 bytes) and asserts each digest matches a host-side reference
computed with stock `sha2::Sha256`. Verified — all 5 match.

`SHA_COMPRESS` syscall counts behave as expected (1 for input that
fits in one padded block; 2 once you cross 64 bytes; 4 for 200 bytes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirror SP1's `examples/Cargo.toml` pattern: every example (program +
script) is a member of a single examples workspace. Shared host deps
(`sp1-sdk`, `sp1-build`, `tokio`, `tracing`, `sha2`, `tiny-keccak`)
move to `[workspace.dependencies]`; per-crate `Cargo.toml`s shrink to
`{ workspace = true }` references.

# Changes

  * **New**: `zkevm/examples/Cargo.toml` — workspace root with shared
    deps and members for all 11 example crates (5 program + 6 script;
    `hello-c/program/` is C source, not a Cargo crate).
  * **Programs**: drop standalone `[workspace]` blocks and the
    `panic = "abort"` profiles. `sp1-build` already sets
    `-C panic=abort` via rustflags at compile time, so per-program
    profile overrides were redundant. Matches how SP1's own
    `examples/fibonacci/program/Cargo.toml` (and friends) are set up.
  * **Scripts**: switch their deps to `{ workspace = true }` against
    the new examples workspace.
  * **SP1 root `Cargo.toml`**: drop the per-script `members` and per-
    program `exclude` entries; replace with a single
    `exclude = ["zkevm/examples"]`.
  * **Makefile**: example targets `cd examples && cargo run -p ...`
    instead of running from the SP1 root.

# Why

Single Cargo.lock for all examples. Adding a new example = one
new line in `members` + a tiny per-crate Cargo.toml. Scripts can use
each other's helpers without going through a workspace boundary. And
it's the convention SP1 already uses for its own `examples/`.

# Verified

`cargo run -p {sha256,keccak,fibonacci}-script --bin *-execute` from
`zkevm/examples/` — all three work; cycle counts and digests
unchanged from before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…zkevm-c-build helper

# What

  * **`zkevm/examples/c-build/`** — a tiny library crate that
    encapsulates the per-example C build step (sp1-build for
    libzkevm-cabi + clang + ld.lld). Each C script's `build.rs` is
    now ~7 lines:
        let elf = zkevm_c_build::build_c_example(example_dir);
        println!("cargo:rustc-env=GUEST_ELF={}", elf.display());

  * **`fibonacci-c/`** — `int main(void)` that reads u32 n, computes
    fib(n) % 7919, writes the u32 result. 18758 cycles for n=1000
    (matches the Rust version's 18795 within ~0.2%).

  * **`panic-c/`** — calls `abort()` on non-zero input flag.
    libzkevm's `abort()` is `zkvm_halt(1)`, so this exercises the
    failed-termination path from C identically to the Rust panic
    example. flag=0: 4704 cycles. flag=1: 4640 cycles, executor
    returns Ok with empty public values + halted-with-non-zero exit
    code.

  * **`keccak-c/`** — calls `zkvm_keccak256` from C against the same
    5 input shapes as `keccak/`. All digests match host-computed
    keccak256. 5339 cycles for a 43-byte input (Rust version: 5563);
    1 KECCAK_PERMUTE syscall.

  * Refactored existing `hello-c/script/build.rs` to use the new
    helper (drops ~95 lines of inline shell-out logic).

# Why one helper crate, not duplicating 4 build.rs files

The C build pipeline (sp1-build for cabi -> clang -> ld.lld with
ld.lld lookup fallback to ~/.sp1/toolchains/) is ~95 lines and is
identical across all four C examples. Extracting it gives:
  * O(1) maintenance (any tweak to clang flags, lld lookup, or
    target-dir layout lives in one place);
  * Each C example's `script/Cargo.toml` and `build.rs` shrink to
    near-trivial templates that a future C example author can copy
    verbatim.

# Verified

  * `cargo run -p {fibonacci-c,panic-c,keccak-c}-script --bin *-execute`
    from `zkevm/examples/` — all three pass; cycle counts within a
    few percent of the Rust counterparts (the difference is mostly
    sp1-zkvm's __start init overhead being amortized differently).
  * `cargo run -p hello-c-script --bin hello-c-execute` — still works
    after the build.rs refactor.
  * `cargo fmt --all -- --check` clean across both workspaces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# What

Three small additions to `sp1-build` for crates wiring C code into SP1:

  * `pub const CLANG_FLAGS: &[&str]` — clang command-line flags for
    SP1's `riscv64im-succinct-zkvm-elf` target (`--target=...`,
    `-march=rv64im`, `-mabi=lp64`, `-ffreestanding`, `-fno-builtin`,
    `-fno-stack-protector`, `-nostdlibinc`).

  * `pub fn find_lld() -> Option<PathBuf>` — locate `ld.lld`,
    preferring `PATH` and falling back to the bundled copy in any
    installed SP1 toolchain
    (`~/.sp1/toolchains/*/lib/rustlib/.../bin/gcc-ld/ld.lld`).

  * `pub fn build_program_staticlib(path: &str) -> PathBuf` — build a
    `crate-type = ["staticlib"]` crate via `build_program` and return
    the path to the resulting `.a`. Existing `build_program` is bin-
    oriented and surfaces ELFs via `SP1_ELF_*` env vars; staticlibs
    follow a fixed convention under the helper target subdir, so
    this wrapper just runs the build and assembles the path from
    cargo metadata.

# Why

`zkevm/examples/c-build/` (the build helper for our C example
scripts) had ~95 lines of inline shell-out logic for exactly these
three things. Pulling them into `sp1-build` is:

  * The natural home: the clang flag set is determined by the SP1
    target spec; the lld lookup needs to know about
    `~/.sp1/toolchains/`; the staticlib path depends on
    sp1-build's own target-dir convention. All sp1-build knowledge.
  * Low-cost: ~80 net lines added to sp1-build, all additive (no
    public API breakage).
  * Reusable: any future C consumer of SP1 (FFI in a Rust guest, a
    minimal pure-C guest, …) gets the same plumbing.

`zkevm/examples/c-build/src/lib.rs` is now a thin orchestrator
(~50 lines), down from ~130, calling the three new helpers.

# Verified

`cargo run -p keccak-c-script --bin keccak-c-execute` — all 5 input
shapes still match host-computed keccak256.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…elease workflow

Three deliverables targeting non-Rust SP1 zkEVM users:

# 1. `make sdk-archive` produces `zkevm-sdk-vX.Y.Z.tar.gz`

  * Stages `libzkevm.a + zkvm.ld + include/zkvm_accelerators.h + README.md`
    under a versioned root dir, tars+gzips. ~432K compressed.
  * `SDK_VERSION` is overrideable on the command line; defaults to
    `0.1.0-pre`.
  * `templates/sdk-archive-README.md` is the README packaged inside the
    tarball; it explains link line, tooling requirements, and
    termination semantics.

# 2. `zkevm/templates/c-program/`

A minimal C guest scaffold that consumers can `cp -r` to start a new
project. Three files:

  * `Makefile` — clang + ld.lld pipeline; `SDK_DIR ?= ../zkevm-sdk-X.Y.Z`.
  * `main.c` — `int main(void)` skeleton with a commented-out menu of
    common patterns (echo input, hash via `zkvm_keccak256`, signal
    failure via non-zero return).
  * `README.md` — copy/build/prove walkthrough.

# 3. `.github/workflows/zkevm-sdk-release.yml`

Triggers on push to a `zkevm-sdk-v*` tag (or manual `workflow_dispatch`
for testing). Single linux job:
  * Setup CI via the existing `./.github/actions/setup`.
  * Install the succinct toolchain via `cargo run -p sp1-cli -- prove install-toolchain`.
  * `make -C zkevm sdk-archive SDK_VERSION=$VERSION`.
  * Upload as a workflow artifact (always) and create a GitHub Release
    with the tarball attached (only on tag push).

# Plumbing change: `zkevm/build-sdk/` helper bin

The Makefile's `make sdk` rule used to do `cd libzkevm-cabi && cargo
build --release --target $(TARGET)` with `RUSTUP_TOOLCHAIN=succinct`.
That fell back to nightly cargo and complained about a missing
`rust-src` component on the succinct toolchain. The reliable way to
invoke the succinct toolchain on a staticlib crate is through
`sp1_build::build_program_staticlib` (which sets RUSTC, target dir,
and rustflags directly).

Wrapping that in a tiny `cargo run`-able binary at `zkevm/build-sdk/`
lets the Makefile drive the whole pipeline with one `cargo run` (no
toolchain-env juggling). Verified end-to-end:
  * `rm -rf zkevm/sdk zkevm/libzkevm-cabi/target && make -C zkevm sdk`
    succeeds from a clean state.
  * `make sdk-archive` produces a 432 KB tarball with the expected
    layout (`tar tzf` confirms `libzkevm.a + zkvm.ld + include/ + README`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per review: the workflow should just produce the tarball. Cutting an
actual GitHub Release will be folded into the main `release.yml` (as a
job analogous to `sp1-gpu-server-release`) when an SP1 release is next
cut.

Changes:
  * Renamed `.github/workflows/zkevm-sdk-release.yml` ->
    `zkevm-sdk-build.yml`.
  * Removed the tag-push trigger and the `gh release create` step.
  * Kept `workflow_dispatch` (Actions UI / manual trigger). The
    workflow takes a `version` input and uploads
    `zkevm-sdk-vX.Y.Z.tar.gz` as a workflow artifact (30-day retention).
  * Inline doc comment describes the integration plan.

When an SP1 release is cut, copy the build steps into a new job in
`release.yml` and change the upload step to:

    gh release upload ${{ needs.prepare.outputs.tag_name }} \
        zkevm/zkevm-sdk-${VERSION}.tar.gz

(matching the `sp1-gpu-server-release` pattern). The standalone
build workflow can then stay (as a sanity check) or be removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirror the pattern already in `hello-{c,rust}/script/src/execute.rs`:

  let client = ProverClient::builder().light().build().await;

instead of `ProverClient::from_env().await`. The light prover is
faster to spin up and doesn't bring up the full CPU prover plumbing
that's only needed when actually generating proofs. Applies to:

  * fibonacci, panic, keccak, sha256
  * fibonacci-c, panic-c, keccak-c

Prove scripts (where we do generate + verify a real STARK proof) keep
`ProverClient::from_env()` so users can still pick the prover via
`SP1_PROVER` (cpu / cuda / mock).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…les workspace

Lets `SP1_PROVER=cuda cargo run -p <example>-script --bin *-prove` work
out of the box (much faster proof generation than the default CPU
prover). Affects every example script via the workspace.dependencies
override.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lets `SP1_PROVER=network cargo run -p <example>-script --bin *-prove`
work as well (with `NETWORK_PRIVATE_KEY` set). Useful for offloading
proof generation to a remote prover during development.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the existing execute scripts: feeds flag=1 so the guest panics
(Rust) / aborts (C), generates a core proof, and verifies it with the
matching `StatusCode::new(1)`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… halt(1)

Adds two C examples covering additional failed-termination shapes:

- assert-c — uses standard `<assert.h>`. A failed `assert(...)` expands to
  a call to glibc-shape `__assert_fail`, which libzkevm now provides as a
  shim that routes to `zkvm_halt(1)`. A minimal freestanding `<assert.h>`
  lives in `zkevm/include/` since the C build is `-nostdlibinc`.
- exit-code-c — `int main(void) { return 1; }`. The SP1 entrypoint already
  forwards `main`'s i32 return value to `syscall_halt`, so this is the
  no-libc-call path to a non-zero exit code.

Both examples mirror panic-c (execute + prove binaries, verify with
`StatusCode::new(1)`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
RIPEMD-160 has no SP1 syscall and isn't on the L1 STF hot path, so the
implementation is the stock RustCrypto `ripemd` crate (no patch). Output
is the 20-byte digest with 12 trailing zero bytes per the
`zkvm_accelerators.h` layout.

The new ripemd-c example mirrors keccak-c: feeds a private input, the
guest computes RIPEMD-160 via `zkvm_ripemd160`, the host driver checks
the output against host-computed RIPEMD-160 across several lengths and
also runs a prove-and-verify path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BLAKE2f compression function (Ethereum precompile 0x09 / EIP-152). SP1
has no BLAKE2 syscall, so the F function is vendored inline in pure
software per RFC 7693 §3.2 — IV constants, SIGMA permutation, the G
mixing function with rotations 32/24/16/63, and the standard 8-call
column-then-diagonal round.

The blake2f-c example feeds the four EIP-152 F-function test vectors
(rounds = 1 and 12, both f=0 and f=1) and checks the post-compression
state against the expected hex. Vector 4 (12 rounds, f=1) coincides
with the final state of `BLAKE2b("abc")`, which we cross-check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ECDSA secp256k1 verify via the patched `k256` crate. At
`target_os = "zkvm"` the inner scalar multiplication and decompression
route through SP1's `SECP256K1_ADD`, `SECP256K1_DOUBLE`, and
`SECP256K1_DECOMPRESS` precompiles; on host it falls back to the
software pure-Rust path.

The patch is pulled in via the new `patch-k256-13.4-sp1-6.2.0-pre`
tag, which mirrors the existing `tiny-keccak`/`sha2` 6.2.0-pre
convention by repointing `sp1-lib` at the `tamir/no_std_zkvm` branch
on succinctlabs/sp1 instead of the crates.io `6.0.0` release.

Pubkey layout per `zkvm_accelerators.h` is the raw 64-byte uncompressed
`x || y` (no SEC1 `0x04` tag); we prepend the tag byte before handing
it to `VerifyingKey::from_sec1_bytes`.

The secp256k1-c example signs a SHA-256 prehash with a freshly-generated
key, sends `(msg_hash || sig || x||y)` to the guest, and asserts the
guest accepts the valid signature and rejects a tampered variant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tamirhemo and others added 11 commits April 30, 2026 21:35
…ample

Wires the patched `substrate-bn` crate (`-sp1-6.2.0-pre-substrate-bn`
tag, repointing `sp1-lib` at the no_std branch) into libzkevm. At
`target_os = "zkvm"` BN254 curve and Fp/Fp2 ops route through SP1's
`BN254_*` precompile syscalls; on host the same code falls back to the
software pure-Rust path.

Layout per `zkvm_accelerators.h` and EIP-196 / EIP-197:
  - G1: 64 BE bytes (x || y); (0,0) = point at infinity.
  - G2: 128 BE bytes (x.a1 || x.a0 || y.a1 || y.a0); (0,0,0,0) = infinity.
  - Scalar: 32 BE bytes, reduced mod the BN254 group order.

bn254-c cross-checks `g1_add` and `g1_mul` outputs against host-side
substrate-bn computations on random scalars, plus the `P + 0 = P`
identity case.

libzkevm now uses `extern crate alloc` to support the variable-length
pairing input.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…12-c

Wires the patched `bls12_381` crate (`-sp1-6.2.0-pre` tag, repointing
`sp1-lib` at the no_std branch) into libzkevm. At `target_os = "zkvm"`
G1/G2 arithmetic and the pairing miller-loop route through SP1's
`BLS12381_*` precompile syscalls; on host the same code falls back to
software pure-Rust.

Implements:
  - `zkvm_bls12_g1_add` / `zkvm_bls12_g2_add`
  - `zkvm_bls12_g1_msm` / `zkvm_bls12_g2_msm` (naive accumulate; switch
    to Pippenger if it shows up in profiles)
  - `zkvm_bls12_pairing` via `multi_miller_loop().final_exponentiation()`
    and equality with `Gt::identity()`

`zkvm_bls12_map_fp_to_g1` / `zkvm_bls12_map_fp2_to_g2` remain stubs —
the SSWU map needs the `experimental` feature plus clear-cofactor
logic, and SP1 has no precompile for those operations yet.

bls12-c covers g1_add, g2_add, and pairing (both a cancelling
2-pair input that should verify and a non-trivial single-pair input
that should not).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EIP-198 modular exponentiation. SP1 has no modexp precompile syscall,
so the implementation is the stock `num-bigint-dig` software impl.
Variable-length BE inputs; output is exactly `mod_len` BE bytes,
right-aligned with leading zeros. `mod_len == 0` returns OK with no
write; `modulus == 0` writes `mod_len` zero bytes per EIP-198 (no
division-by-zero error to surface).

The modexp-c example ships an `(base_len:4 BE || exp_len:4 BE ||
mod_len:4 BE || base || exp || modulus)` framing and exercises
representative cases (3^65537 mod p, the standard EIP-198 edge cases:
0^x, x^0, mod 1) cross-checked against host-side `num-bigint::modpow`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ECRECOVER (Ethereum precompile 0x01) via the patched `k256` crate's
`VerifyingKey::recover_from_prehash`. At `target_os = "zkvm"` recovery
is fast-pathed through SP1's `FD_ECRECOVER_HOOK` and verified in-circuit
with `SECP256K1_ADD`/`SECP256K1_DOUBLE`; on host the same code falls
back to the software pure-Rust path.

Output layout matches `zkvm_secp256k1_pubkey`: 64 bytes uncompressed
`x || y` (no SEC1 `0x04` tag). Recovery id is the standard 0..=3 byte;
out-of-range values surface as `ZKVM_EFAIL`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Maps Fp -> G1 / Fp2 -> G2 via the patched `bls12_381` crate's
`MapToCurve` (gated by the `experimental` feature) followed by
`G1Projective::clear_cofactor` / `G2Projective::clear_cofactor`. Per
EIP-2537 the result of the simple-SWU map must be cofactor-cleared so
the output lands in the prime-order subgroup.

Fp2 input layout per `zkvm_accelerators.h`: 96 bytes = c1 (48 BE) ||
c0 (48 BE), matching EIP-2537.

bls12-c is extended with mode 3 (map_fp_to_g1) and mode 4
(map_fp2_to_g2), cross-checked against host-side
`G1Projective::map_to_curve(...).clear_cofactor()`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the original "everything is a stub, here are the suggestions"
table with the actual implementation status. Only `zkvm_kzg_point_eval`
remains a stub — wiring it up needs the Ethereum trusted-setup `[tau]_2`
constant and a no_std KZG verifier, both of which are follow-up work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…mple

Wires `kzg-rs` (succinctlabs) into libzkevm via the new
`v0.2.8-sp1-6.2.0-pre` tag, which repoints kzg-rs's `bls12_381` dep at
`sp1-patches/bls12_381#patch-0.8.0-sp1-6.2.0-pre` so the KZG verifier
shares the same patched curve as the rest of libzkevm. The Ethereum
trusted setup is baked in at build time via `include_bytes!`, so the
guest doesn't need to load it at runtime.

Layout per `zkvm_accelerators.h`: `commitment` and `proof` are 48-byte
compressed G1; `z` and `y` are 32-byte big-endian field elements. On
parse error or pairing-check failure the function returns ZKVM_EOK
with `*verified = false`; only true API misuse (null pointers)
surfaces as ZKVM_EFAIL.

The kzg-c example feeds a known consensus-spec test vector
(verify_kzg_proof_case_correct_proof_02e696ada7d4631d) and asserts the
guest accepts the valid opening and rejects a tampered one.

Also drops the now-unused `ecall::placeholder` module and
`ecall0/2/3/4` helpers — every precompile now routes through patched
crypto crates whose inner syscalls go via `sp1-lib`, so the
hand-rolled ecall escape hatch is dead. The SP1 syscall-number
re-exports remain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… consensus-spec vectors

Adds a host-only `zkevm-fixtures` crate that owns vendored cryptographic
test vectors and parses them on demand. First module is `kzg`, with 12
`verify_kzg_proof_case_*` YAMLs from the consensus-specs suite (4
correct + 4 incorrect + 4 invalid-encoding) baked in via `include_str!`.

kzg-c's execute driver now iterates the bundled vectors and asserts the
guest's `verified` byte matches the spec's `output` field. Cases with
non-canonical input lengths (commitment/proof != 48 bytes etc.) are
skipped — those test wire-format validation that's enforced by the
fixed-width `zkvm_bytes_*` types at the C ABI boundary, before
libzkevm sees the bytes. Of the 12 bundled cases, 10 run on the guest
and 2 are skipped as out-of-scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…6k1-c

Vendors Google's Wycheproof ECDSA secp256k1 P1363 SHA-256 corpus (250
test cases covering signature malleability, modular-inverse traps,
modified r/s, integer overflows, edge-case public keys, etc.) into the
zkevm-fixtures crate.

The secp256k1-c execute driver now compares three verdicts per case:
guest, unpatched-k256 host, and Wycheproof-declared. The hard
assertion is `guest == host k256` — a divergence there is a real
patched-k256 correctness regression. Disagreements between *both*
k256 paths and Wycheproof reflect k256's deliberate design (it
enforces low-s by default, etc.) and are surfaced as informational
logs only.

232 cases run on the guest (those with canonical 64-byte sigs and
65-byte SEC1 pubkeys). 0 patch divergences. 71 k256-vs-Wycheproof
disagreements, identical on host and guest — the patched library
matches its upstream's behavior to the bit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ver-c

Re-uses the bundled Wycheproof ECDSA secp256k1 P1363 SHA-256 corpus to
exercise `zkvm_secp256k1_ecrecover`. For each case that unpatched-k256
verifies on the host, the script grinds the recovery id (0..=3) and
checks that the guest's recovered pubkey equals the expected pubkey.

Cases that unpatched-k256 rejects (bad sigs, malleability, special-hash
edges, etc.) are skipped — recovery from a sig the underlying ECDSA
library would reject doesn't have a well-defined expected output.

94 tested, 0 mismatches: every case where host k256 considers the
signature valid recovers the same pubkey on the guest.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `-sp1-6.2.0-pre` patch tags previously pointed `sp1-lib` at the
`tamir/no_std_zkvm` branch on succinctlabs/sp1 (the pre-merge home of
the no_std refactor). That branch was deleted after #2762 merged the
no_std change into main, so the existing tag commits were unresolvable
for new clones / `cargo update` runs.

Force-updated each affected sp1-patches tag (tiny-keccak, k256, p256,
bn substrate-bn variant, bls12_381) plus succinctlabs/kzg-rs's
`v0.2.8-sp1-6.2.0-pre` to point sp1-lib at `branch = "main"` instead.
This commit is just the lockfile refresh that pulls those new tag
commits into our resolution graph; both Cargo.lock files now reference
`https://github.com/succinctlabs/sp1?branch=main#66f5953e` rather than
the deleted `tamir%2Fno_std_zkvm` branch.

No code changes. Verified with `cargo check --workspace`,
`cargo build -p libzkevm`, and end-to-end runs of kzg-c-execute,
secp256k1-c-execute (Wycheproof), and bls12-c-execute.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 30, 2026

Test Old New Diff
curve25519_dalek_test_decompressed_expected_value 4556798 4556798 0.0000 %
bls12_381_tests_test_bls_add_100 10498916 10498916 0.0000 %
curve25519_dalek_test_ed25519_verify 13328105 13326031 -0.0156 %
bls12_381_tests_test_bls_double_100 6346516 6346516 0.0000 %
bn_test_bn_test_fr_inverse_100 818209 818209 0.0000 %
bn_test_bn_test_g1_mul_zero 45725 45725 0.0000 %
curve25519_dalek_ng_test_zero_msm 125894 125894 0.0000 %
sha_test_sha2_v0_10_6_expected_digest_lte_100_times 1349983 1349885 -0.0073 %
p256_test_recover_rand_lte_100 15966030 15957910 -0.0509 %
sha_test_sha3_expected_digest_lte_100_times 1192171 1192280 0.0091 %
k256_test_recover_high_hash_high_recid 2231807 2044903 -8.3746 %
k256_test_verify_rand_lte_100 11892224 11877188 -0.1264 %
curve25519_dalek_test_zero_msm 83941 83941 0.0000 %
bls12_381_tests_test_inverse_fp_100 1474453 1474453 0.0000 %
bn_test_bn_test_fq_inverse_100 801209 801209 0.0000 %
bn_test_bn_test_fq_sqrt_100 799609 799609 0.0000 %
sha_test_sha2_v0_10_9_expected_digest_lte_100_times 1347984 1351057 0.2280 %
rustcrypto_bigint_test_bigint_mul_mod_special 2084120 2084120 0.0000 %
k256_test_point_ops_edge_cases 33844 33844 0.0000 %
bls12_381_tests_test_inverse_fp2_100 2659337 2659337 0.0000 %
curve25519_dalek_test_decompressed_noncanonical 7785 7785 0.0000 %
curve25519_dalek_ng_test_decompressed_noncanonical 197730 197730 0.0000 %
bn_test_bn_test_fq_partial_ord 184121 184121 0.0000 %
bn_test_bn_test_g1_msm_edge 406190 406190 0.0000 %
curve25519_dalek_test_add_then_multiply 2717066 2868957 5.5903 %
curve25519_dalek_test_zero_mul 72415 72415 0.0000 %
sha_test_sha2_v0_10_8_expected_digest_lte_100_times 1348436 1350548 0.1566 %
keccack_test_expected_digest_lte_100 1716299 1714105 -0.1278 %
bls12_381_tests_test_sqrt_fp_100 874315 962854 10.1267 %
k256_test_recover_pubkey_infinity 101786 101786 0.0000 %
curve25519_dalek_ng_test_zero_mul 108392 108392 0.0000 %
p256_test_recover_pubkey_infinity 102133 102133 0.0000 %
sha_test_sha2_v0_9_9_expected_digest_lte_100_times 1262166 1265549 0.2680 %
p256_test_recover_high_hash_high_recid 5691345 5390188 -5.2915 %
secp256k1_program_test_verify_rand_lte_100 17114114 17144446 0.1772 %
p256_test_verify_rand_lte_100 11846284 11880003 0.2846 %
rustcrypto_bigint_test_bigint_mul_add_residue 1736063 1736063 0.0000 %
k256_test_recover_rand_lte_100 4582059 4568570 -0.2944 %
curve25519_dalek_ng_test_add_then_multiply 3451913 4470375 29.5043 %
bls12_381_tests_test_sqrt_fp2_100 1519933 1750605 15.1765 %
bn_test_bn_test_g1_add_neg 306844 306844 0.0000 %
secp256k1_program_test_verify_v0_30_0_rand_lte_100 17144830 17129835 -0.0875 %
bn_test_bn_test_g1_add_100 985603 985610 0.0007 %
secp256k1_program_test_recover_v0_30_0_rand_lte_100 5509510 5516925 0.1346 %
rust_crypto_rsa_test_pkcs_verify_100 29002138 29007373 0.0181 %
bn_test_bn_test_g1_double_100 726696 726717 0.0029 %
secp256k1_program_test_recover_rand_lte_100 5503993 5515964 0.2175 %
k256_test_schnorr_verify 5754609 5739279 -0.2664 %

tamirhemo and others added 3 commits May 1, 2026 07:18
Out-of-date sections removed or updated:
  * Precompile status: most are now real (patched crypto crates), no
    longer stubs. Added a status table with per-function backing.
  * Examples list: 14 new C examples landed since the last README
    update (assert-c, exit-code-c, ripemd-c, blake2f-c, secp256k1-c,
    secp256r1-c, bn254-c, bls12-c, modexp-c, ecrecover-c, kzg-c,
    fixtures, c-build). Reorganized as a table by category.
  * Layout: include/ now has assert.h alongside zkvm_accelerators.h.
    Added build-sdk/, templates/c-program/, fixtures/.
  * Quick start: surfaced `make sdk-archive` + the c-program template
    + `SP1_PROVER=cuda` tip for prove scripts.
  * Memory map: corrected — `.text` is at STACK_TOP (0x78000000), not
    0x1000. Stack grows down INTO addresses below STACK_TOP.
  * Removed: "What changed in SP1 to make this possible" section
    (lives in commit history). Stale cycle-count transcripts. The
    inventory of "open TODOs" that are now done.
  * Tightened: ~275 -> ~155 lines.

Kept: ABI notes, termination semantics, three-workspaces
explanation, references.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kevaundray
Copy link
Copy Markdown

cc @marcinbugaj for visibility

tamirhemo and others added 5 commits May 4, 2026 21:22
Adds `sp1_lib::invalid_hint!` (and the underlying
`sp1_lib::halt_invalid_hint`) so patched crypto crates can halt the
zkVM with exit code 3 — distinct from a regular Rust panic (exit 1) —
when a prover-supplied hint fails verification. Without this, a
malicious prover could forge a "panicked program" proof by feeding
wrong hint data; halting on a separate exit code lets the verifier
distinguish the two.

* `sp1-lib`:
  - Add `halt_invalid_hint() -> !` (calls `syscall_halt(3)`).
  - Add `invalid_hint!()` macro: panic-shaped format args, writes the
    diagnostic to stderr (FD 2) via `syscall_write`, then halts with
    exit 3.
  - `read_vec()` and `read::<T>()` now treat empty-stream / corrupt
    bincode as invalid-hint conditions and route through the same
    halt(3) path with the original "Was the correct data written
    into SP1Stdin?" diagnostic preserved.

* `sp1-zkvm`: `#[no_mangle]` on `syscall_halt` so the patches' extern
  declaration in `sp1-lib` resolves at link time. (All other syscalls
  in the entrypoint already had it; this was the lone omission.)

* `sp1-core-executor`: `StatusCode` gains `INVALID_HINT = 3` and
  accepts it in `is_accepted_code`, so verifiers can prove a halt-3
  outcome.

* All seven patches that use hints now call `sp1_lib::invalid_hint!`
  on hint-validation failures with contextual messages:
  curve25519-dalek (+ ng), bls12_381, substrate-bn, k256, p256,
  RustCrypto-RSA. New tags pushed: `patch-X-sp1-6.2.0` across
  `sp1-patches/{curve25519-dalek,curve25519-dalek-ng,bls12_381,bn,
  elliptic-curves(k256+p256),RustCrypto-RSA,tiny-keccak,
  rust-secp256k1(0.29.1+0.30.0),RustCrypto-hashes(4 sha2 + sha3),
  RustCrypto-bigint}`.

* patch-testing: bump all tag references to `-sp1-6.2.0`. Add
  `patch-testing/invalid-hint` smoke-test crate (4 tests, all
  passing) covering the macro's halt(3) path with and without a
  message, the no-op trigger=0 case, and the empty-input read_vec
  halt path.

* examples: `examples/invalid-hint/` — guest reads a trigger byte,
  fires `invalid_hint!` when non-zero, then the host script
  asserts `report.exit_code == 3` and verifies StatusCode round-
  trips through `StatusCode::new(3) == Some(INVALID_HINT)`.

Followup (not in this PR): the minimal executor's `write` path
hardcodes hint hook dispatch in `crates/core/executor/src/minimal/
write.rs` and ignores the user-provided `HookRegistry`. Routing
through the registry would let `with_hook(FD_X, malicious_hook)`
exercise per-patch failure paths (e.g. "supply a wrong Fp inverse,
expect halt(3)"); leaving as a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Repoints all `-sp1-6.2.0-pre` tags (which pinned `sp1-lib` at the
`tamir/no_std_zkvm` git branch) to the proper crates.io-pinned
`patch-X-sp1-6.2.0` tags that just shipped. The `bn` substrate-bn
variant moves to `patch-0.6.0-sp1-6.2.0-substrate-bn`; `kzg-rs`
moves to `v0.2.8-sp1-6.2.0`.

Adds `examples/invalid-hint-c/`: a C guest demonstrating
`zkvm_invalid_hint()` (the C ABI entry point added in
`libzkevm/src/halt.rs`) which halts with exit code 3
(`StatusCode::INVALID_HINT`). Runs both the success path
(flag=0 → exit 0) and the invalid-hint path (flag=1 → exit 3) so
the example doubles as a smoke test for the new V6 exit code.

`[patch.crates-io] sp1-lib = path` overrides added to the root
workspace, `zkevm/examples`, and `zkevm/libzkevm-cabi` so the build
resolves while `sp1-lib 6.2.0` is in flight to crates.io. These
overrides can be removed once the publish lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tamirhemo tamirhemo marked this pull request as ready for review May 11, 2026 20:44
claude and others added 7 commits May 12, 2026 02:27
Make zkevm-sdk-build.yml a reusable workflow (workflow_call alongside
workflow_dispatch) and invoke it from release.yml as a fire-and-forget
job, mirroring the docker-publish pattern.

The standalone manual trigger still produces a workflow artifact;
when called from release.yml, the tarball is uploaded directly to the
release tag created by the prepare job.

https://claude.ai/code/session_01TC7Ei62ZbDirHRgv11bRGX
The Static Library and Linker Script standard (eth-act zkvm-standards
PR #28) requires the vendor linker script to export `_heap_start` and
`_heap_end` so that application-provided allocators can size the heap
without knowing vendor-internal symbol names.

Rename the existing `__heap_start` to `_heap_start` and add `_heap_end`
(set to `__input_start`, i.e. one past the last byte usable by the heap
before SP1's reserved hint-input region begins). Nothing in libzkevm.a
or sp1-zkvm references `__heap_start`; the rlib's embedded allocator
uses `_end`, which is unchanged.

All other PR #28 requirements (ENTRY(_start), stack/gp init, BSS
zeroing by the ELF loader, IO/hasher init, int main(void) contract,
exit-code forwarding) are already satisfied by the existing libzkevm
+ sp1-zkvm scaffolding.
Now that `sp1-lib 6.2.0` is on crates.io, drop the `[patch.crates-io]`
overrides that pinned `sp1-lib` to in-tree path-deps in the root
workspace, in `libzkevm-cabi`, and in the examples workspace. The
sp1-patches/* crates' transitive `sp1-lib = "6.2.0"` requirements now
resolve from crates.io directly.

C-side example coverage parity. Four `zkvm_*` functions were
implemented but not exercised by any C example:
  * `zkvm_sha256` — new `sha256-c` example mirroring `keccak-c`.
  * `zkvm_bn254_pairing` — new mode 2 in `bn254-c` (empty pairing,
    bilinearity `e(P,Q)·e(-P,Q) = 1`, single-pair `e(P,Q) ≠ 1`).
  * `zkvm_bls12_g1_msm` / `_g2_msm` — new modes 5/6 in `bls12-c`,
    with host-side `bls12_381` sum as the cross-check.

EIP golden vectors. The fixtures crate previously vendored KZG and
Wycheproof ECDSA suites only; this commit adds:
  * `eip198/modexp.json` — 8 hand-crafted modexp cases (zero modulus,
    `mod_len`-padding, Fermat on the secp256k1 prime, …).
  * `eip196/g1_{add,mul}.json` — known-answer pairs derived from
    `py_ecc.bn128` for the BN254 generator and small multiples.
  * `eip197/pairing.json` — empty/bilinear/single-pair cases.
  * `eip2537/{g1_add,g2_add,g1_msm,g2_msm,pairing}.json` — same
    treatment for BLS12-381, generated from `py_ecc.bls12_381`.
Each example execute script iterates the corresponding fixture set
and asserts a byte-for-byte match against the guest output.

Doc cleanup. Stale "scaffolding / stubs return ZKVM_EFAIL"
language has been removed from `libzkevm/src/lib.rs`,
`libzkevm/src/precompile/mod.rs`, and `libzkevm/README.md`; the
top-level `Limitations / future work` section now reflects the
remaining real items (software-only `ripemd160`/`modexp`/`blake2f`
and the experimental `bls12_381` map-to-curve feature).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants