Skip to content

feat: add standalone zkvm guest target - #2

Open
ivokub wants to merge 23 commits into
masterfrom
feat/standalone-zkvm-guest
Open

feat: add standalone zkvm guest target#2
ivokub wants to merge 23 commits into
masterfrom
feat/standalone-zkvm-guest

Conversation

@ivokub

@ivokub ivokub commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

This PR adds the EVM precompiles and makes the library compilable to R5 standalone target.


Note

High Risk
Touches KZG trusted-setup loading, pairing correctness, and new cryptographic entry points for bare-metal builds; BN254 fix changes EVM pairing behavior for mixed infinity inputs.

Overview
Adds a freestanding RISC-V64 (rv64im) build path for zkVM guests: Nim --os:standalone gates file I/O and stdio, a clang shim plus stub headers/standalone_stdio.c satisfy the runtime, and make_lib_riscv64_freestanding produces libconstantine.riscv64.a with embedded KZG verification-only (CTT_EMBEDDED_KZG + CTT_KZG_VERIFICATION_ONLY), no threads, and an archive symbol check.

Embedded KZG can load the full ceremony from a compile-time .dat blob (ctt_eth_kzg_context_new_embedded) or, in verification-only mode, only embed [τ]G2 and strip prover/PeerDAS APIs via fullKzgContext / fullKzgApi macros and slimmer C headers.

zkVM secp256k1 adds raw eth_zkvm_secp256k1_ecrecover / eth_zkvm_secp256k1_verify (digest-in, strict scalar checks, new cttEVM_MalformedSignature) plus verifyFromDigest for ECDSA.

BN254 pairing (EIP-197) fixes a regression where seeing an infinity pair short-circuited the whole check to success; infinity pairs are now skipped in the product like the spec expects, with dedicated tests/vectors.

CI gains embedded-KZG test batches, BN254 pairing and zkVM secp256k1 suites, and .gitattributes LF enforcement for the embedded setup file.

Reviewed by Cursor Bugbot for commit ba2b28f. Bugbot is set up for automated code reviews on this repo. Configure here.

Roman and others added 17 commits September 4, 2026 13:45
defined(standalone) shims for platforms with no OS primitives:
- sysrand: deterministic zero-fill stub (no OS CSPRNG; the zkVM circuit
  guarantees integrity, so side-channel blinding is unnecessary)
- threadpool: route barriers/futexes/topology/threads to *_standalone
  single-hart implementations
Single-hart zkVM guest implementations:
- panicoverride: trap loop instead of OS abort
- barriers_standalone: N=1 barrier releases immediately
- futexes_standalone: spin-wait degenerates to a fence with one worker
- threads_standalone: createThread traps (spawn loop is empty at n=1)
- topology_standalone: report one core / one available thread
Signed-off-by: Roman <4833306+Filter94@users.noreply.github.com>
Signed-off-by: Roman <4833306+Filter94@users.noreply.github.com>
Signed-off-by: Roman <4833306+Filter94@users.noreply.github.com>
Signed-off-by: Roman <4833306+Filter94@users.noreply.github.com>
…on function name to zkvm as well

Signed-off-by: Roman <4833306+Filter94@users.noreply.github.com>
Signed-off-by: Roman <4833306+Filter94@users.noreply.github.com>
…grams

Signed-off-by: Roman <4833306+Filter94@users.noreply.github.com>
Signed-off-by: Roman <4833306+Filter94@users.noreply.github.com>
Signed-off-by: Roman <4833306+Filter94@users.noreply.github.com>
Signed-off-by: Roman <4833306+Filter94@users.noreply.github.com>
Signed-off-by: Roman <4833306+Filter94@users.noreply.github.com>
@ivokub

ivokub commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

Review Comments

1. bindings/panicoverride.nim: Trap on freestanding panic instead of infinite spin loop

In a bare-metal zkVM guest, while true: discard causes any assertion failure or out-of-memory error to spin until the prover hits its maximum cycle budget or times out, resulting in an opaque cycle-exhaustion error rather than an immediate diagnostic failure.

Calling __builtin_trap() (matching standalone_stdio.c:exit) emits a trap instruction (unimp / ebreak), halting the guest immediately and cleanly.

-proc panic(s: string) {.noreturn, compilerproc,
-    codegenDecl: "static $# $#$#".} =
-  rawoutput(s)
-  while true:
-    discard
+proc c_builtin_trap() {.importc: "__builtin_trap", nodecl.}
+
+proc panic(s: string) {.noreturn, compilerproc,
+    codegenDecl: "static $# $#$#".} =
+  rawoutput(s)
+  c_builtin_trap()

2. constantine.nimble: Support configurable output paths and remove redundant chmod +x in make_lib_riscv64_freestanding

When Constantine is consumed as a Zig package dependency (~/.cache/zig/p/...), the package directory is immutable and read-only. In make_lib_riscv64_freestanding:

  1. chmod +x constantine/platforms/clang-rv64-standalone.sh fails on read-only filesystems. The script is already committed with executable mode 100755 in git, so this chmod can be dropped.
  2. Hardcoded writes to nimcache/libconstantine_riscv64_freestanding and lib/libconstantine.riscv64.a write directly into the dependency tree.

Allowing environment variable overrides (e.g. CTT_OUTDIR and CTT_NIMCACHE) enables embedders to redirect build outputs into their own build cache directories:

   let wrapper = "constantine/platforms/clang-rv64-standalone.sh"
-  let nimcache = "nimcache/libconstantine_riscv64_freestanding"
-  exec "chmod +x " & wrapper
+  let outdir = if existsEnv"CTT_OUTDIR": getEnv"CTT_OUTDIR" else: "lib"
+  let nimcache = if existsEnv"CTT_NIMCACHE": getEnv"CTT_NIMCACHE" else: "nimcache/libconstantine_riscv64_freestanding"
   exec "rm -rf " & nimcache
+  exec "mkdir -p " & outdir
   let nim = if existsEnv"NIM": getEnv"NIM" else: "nim"
   exec nim & " c " &
        releaseBuildOptions(bmStaticLib) &
        " --cc:clang " &
        " --cpu:riscv64 --os:standalone -d:noSignalHandler -d:CTT_EMBEDDED_KZG -d:CTT_KZG_VERIFICATION_ONLY " &
        " --clang.exe:" & wrapper & " --clang.linkerexe:" & wrapper &
        " --threads:off " &
        " --noMain --app:staticlib " &
        " --nimMainPrefix:ctt_init_ " &
-       " --out:libconstantine.riscv64.a --outdir:lib " &
+       " --out:libconstantine.riscv64.a --outdir:" & outdir & " " &
        " --nimcache:" & nimcache & " " &
        " bindings/lib_constantine_riscv64_freestanding.nim"
   exec wrapper & " -c constantine/platforms/standalone_stdio.c" &
        " -o " & nimcache & "/standalone_stdio.riscv64.o"
   let ar = if existsEnv"LLVM_AR": getEnv"LLVM_AR"
            elif fileExists"/opt/homebrew/opt/llvm/bin/llvm-ar": "/opt/homebrew/opt/llvm/bin/llvm-ar"
            elif fileExists"/usr/local/opt/llvm/bin/llvm-ar": "/usr/local/opt/llvm/bin/llvm-ar"
            else: "llvm-ar"
-  exec "rm -f lib/libconstantine.riscv64.a"
-  exec ar & " rcs lib/libconstantine.riscv64.a" &
+  let archive = outdir / "libconstantine.riscv64.a"
+  exec "rm -f " & archive
+  exec ar & " rcs " & archive &
        " " & nimcache & "/*.o"

3. constantine/platforms/clang-rv64-standalone.sh: Guard Homebrew LLVM lookup by OS in clang driver wrapper

clang-rv64-standalone.sh runs for every single C compilation unit spawned by Nim. Because brew --prefix llvm 2>/dev/null executes unconditionally when CLANG is unset, running on Linux spawns a failing brew subprocess for every object file.

Guarding the lookup with [ "$(uname -s)" = "Darwin" ] eliminates this overhead on Linux while preserving the Homebrew fallback on macOS:

 if [ -z "${CLANG:-}" ]; then
-  BREW_LLVM=$(brew --prefix llvm 2>/dev/null || true)
-  CLANG=${BREW_LLVM:+$BREW_LLVM/bin/clang}
+  if [ "$(uname -s)" = "Darwin" ]; then
+    BREW_LLVM=$(brew --prefix llvm 2>/dev/null || true)
+    CLANG=${BREW_LLVM:+$BREW_LLVM/bin/clang}
+  fi
   CLANG=${CLANG:-clang}
 fi

@ivokub ivokub changed the title feat: add standaolne zkvm guest target feat: add standalone zkvm guest target Sep 9, 2026
Signed-off-by: Ivo Kubjas <ivo.kubjas@consensys.net>
Signed-off-by: Ivo Kubjas <ivo.kubjas@consensys.net>
@ivokub

ivokub commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

Second review round. The guest build and archive checks pass locally and the freestanding plug looks minimal and correct — no findings there.

One request: eth_zkvm_secp256k1_verify / eth_zkvm_secp256k1_ecrecover have no behavioral tests in this repo (only the symbol check in check_riscv64_freestanding_archive.sh). Since these are the ABI the downstream accelerator contract consumes directly, please add a small test covering:

  • sign → ecrecover → verify roundtrip
  • negative cases: recid ∉ {0,1}, r or s = 0 / ≥ n, off-curve pubkey, x with no on-curve point (recovery yields neutral → cttEVM_MalformedSignature)

The canonical-scalar checks are load-bearing here because Fr.fromBig silently reduces mod n, so they deserve a regression pin. tests/t_ethereum_evm_bn254_pairing.nim is a decent template.

@Filter94

Copy link
Copy Markdown
Collaborator

Second review round. The guest build and archive checks pass locally and the freestanding plug looks minimal and correct — no findings there.

One request: eth_zkvm_secp256k1_verify / eth_zkvm_secp256k1_ecrecover have no behavioral tests in this repo (only the symbol check in check_riscv64_freestanding_archive.sh). Since these are the ABI the downstream accelerator contract consumes directly, please add a small test covering:

  • sign → ecrecover → verify roundtrip
  • negative cases: recid ∉ {0,1}, r or s = 0 / ≥ n, off-curve pubkey, x with no on-curve point (recovery yields neutral → cttEVM_MalformedSignature)

The canonical-scalar checks are load-bearing here because Fr.fromBig silently reduces mod n, so they deserve a regression pin. tests/t_ethereum_evm_bn254_pairing.nim is a decent template.

Thanks for the comments! I addressed them

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.

2 participants