ITADN
0xAndoroid/jolt-wasm-prover
README.md

Jolt WASM Prover

In-browser zero-knowledge proving and verification using Jolt. Compiles the Jolt zkVM prover and verifier to WebAssembly with multithreading support via SharedArrayBuffer and wasm-bindgen-rayon.

Programs

Four guest programs are included:

ProgramDescriptionGuest crate
SHA-256Hash arbitrary inputguests/sha2
ECDSASecp256k1 signature verificationguests/secp256k1
Keccak ChainIterated Keccak-256 hashingguests/sha3-chain
SHA-256 ChainIterated SHA-256 (tunable trace length, for scale benchmarks)guests/sha2-chain

Prerequisites

  • Rust nightly (managed via rust-toolchain.toml)
  • wasm-pack: curl https://drager.github.io/wasm-pack/installer/init.sh -sSf | bash
  • Node.js (for the dev server)

Quick Start

1. Generate preprocessing data

Preprocessing generates the Dory SRS, compiles guest ELFs, and serializes prover/verifier preprocessing into frontend/public/.

cargo run --release --features native --bin generate-preprocessing

This produces per-program files in frontend/public/:

  • {name}_prover.bin — prover preprocessing (Dory SRS + shared preprocessing)
  • {name}_verifier.bin — verifier preprocessing (Dory verifier setup + shared preprocessing)
  • {name}.elf — compiled guest RISC-V ELF

2. Set up patched WASM dependencies

./setup-wasm-deps.sh

Browser proving currently needs two one-line wasm32 fixes that are not upstream yet — see WASM runtime patches. Native builds (step 1, roundtrip test) work without this step.

3. Build WASM

CARGO_UNSTABLE_BUILD_STD="panic_abort,std" wasm-pack build --release --target web

Outputs the WASM package to pkg/.

4. Run (dev)

cd frontend && npm install && npm run dev
# Open http://localhost:8080

Or build and serve with the production server:

cd frontend && npm run build
node server.mjs
# Open http://localhost:8080

Both set Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers required for SharedArrayBuffer.

Architecture

src/lib.rs          WASM entry point — WasmProver, WasmVerifier, init_inlines
src/wasm_tracing.rs Chrome Trace Format profiling (Perfetto-compatible)
preprocessing/      Native binaries for generating preprocessing data
guests/             RISC-V guest programs (compiled to ELF by jolt-sdk)
frontend/           Vite + React + TypeScript + Tailwind frontend
frontend/public/    Preprocessing artifacts + worker.js
server.mjs          Production server with COOP/COEP headers

WASM API

// Initialize
await init();
await initThreadPool(navigator.hardwareConcurrency);
init_tracing();  // optional: enables Perfetto-compatible tracing
init_inlines();  // registers optimized inline implementations

// Prove
const prover = new WasmProver(proverPreprocessingBytes, elfBytes);
const result = prover.prove_sha2(inputBytes);
// result.proof, result.program_io, result.proof_size, result.num_cycles

// Verify
const verifier = new WasmVerifier(verifierPreprocessingBytes);
const valid = verifier.verify(result.proof, result.program_io);

Build flags

The .cargo/config.toml configures the WASM build with:

  • Atomics + shared memory — enables wasm-bindgen-rayon multithreading
  • 4 GB max memory — required for prover memory usage
  • build-std — rebuilds std with atomics support (requires nightly)

Roundtrip Testing

Validates that preprocessing serialization is deterministic and cross-platform:

cargo run --release --features native --bin test-roundtrip

Dependency pins

Jolt crates come from a16z/jolt at rev 70a294ad58629af59ab89f646d6d0079b57174cb (branch perf/manycore-scaling). Arkworks comes from a16z/arkworks-algebra branch dev/twist-shout; the committed Cargo.lock pins it to 76bb3a4518928f1ff7f15875f940d614bb9845e6. The [patch.crates-io] block in Cargo.toml redirects the registry ark-* crates (pulled in by dory-pcs) onto the same fork so the whole graph shares one set of arkworks types. Build with the committed lockfile; cargo update can move the arkworks branch resolution.

WASM runtime patches (pending upstream)

The repo builds everywhere from the pinned upstream revs, and native binaries are fully functional. Proving on wasm32 additionally needs two one-line fixes that are not upstream yet, shipped in patches/:

  1. 0001-jolt-coefflut-u64.patchCoeffLut::saturated() in jolt-kernels computes len * len in usize; at the 65536-entry table this is 2^32, which wraps to 0 on 32-bit targets and later panics every rayon worker with an index-out-of-bounds. Native 64-bit is unaffected.
  2. 0002-arkworks-wasm-nested-pool.patchmsm_bigint_wnaf in ark-ec builds a nested rayon::ThreadPoolBuilder per chunk; build() panics on wasm32, where wasm-bindgen-rayon provides exactly one fixed global pool. The fix runs the inner parallel MSM on the global pool.

What breaks without them, empirically (HeadlessChrome 150, M4, at the pinned revs): the default sha2 demo (2^13 trace) hangs — proving never completes and no error surfaces; sha2-chain at 2^16 panics in every rayon worker with index out of bounds: the len is 0 at jolt-kernels/src/optimized/registers_read_write.rs:439 (the CoeffLut fix's exact target). With both patches applied, sha2 proves and sha2-chain at 2^16 proves and verifies in-browser. Native 64-bit builds are unaffected either way (the roundtrip test passes without the patches).

./setup-wasm-deps.sh clones both upstreams at the pinned revs into .wasm-deps/, applies the patches, and rewrites the marked override block in Cargo.toml onto the patched checkouts. ./setup-wasm-deps.sh --revert restores the pinned block.