feat(zkevm): SP1 platform for ZKEVM guests (eth-act zkvm-standards)#2763
Open
tamirhemo wants to merge 47 commits into
Open
feat(zkevm): SP1 platform for ZKEVM guests (eth-act zkvm-standards)#2763tamirhemo wants to merge 47 commits into
tamirhemo wants to merge 47 commits into
Conversation
6b2d6be to
343037f
Compare
2ffabb1 to
124fd32
Compare
343037f to
ad7f10d
Compare
62f4869 to
f24c16d
Compare
…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>
…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>
Contributor
|
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>
|
cc @marcinbugaj for visibility |
# Conflicts: # Cargo.lock
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>
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Experimental Implementation of the eth-act zkvm-standards