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:
| Program | Description | Guest crate |
|---|---|---|
| SHA-256 | Hash arbitrary input | guests/sha2 |
| ECDSA | Secp256k1 signature verification | guests/secp256k1 |
| Keccak Chain | Iterated Keccak-256 hashing | guests/sha3-chain |
| SHA-256 Chain | Iterated 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-rayonmultithreading - 4 GB max memory — required for prover memory usage
build-std— rebuildsstdwith 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/:
0001-jolt-coefflut-u64.patch—CoeffLut::saturated()injolt-kernelscomputeslen * leninusize; 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.0002-arkworks-wasm-nested-pool.patch—msm_bigint_wnafinark-ecbuilds a nestedrayon::ThreadPoolBuilderper chunk;build()panics onwasm32, 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.