Skip to content

feat: fully in-browser RS256 proving via multi-threaded WASM#20

Open
moven0831 wants to merge 16 commits intomainfrom
feat/wasm-demo
Open

feat: fully in-browser RS256 proving via multi-threaded WASM#20
moven0831 wants to merge 16 commits intomainfrom
feat/wasm-demo

Conversation

@moven0831
Copy link
Copy Markdown
Collaborator

@moven0831 moven0831 commented Apr 7, 2026

Context

PR #18 implemented a hybrid approach: witness generation in browser, proving on a Node.js server (~7s). This PR explores the alternative: everything runs in the browser via multi-threaded WebAssembly.

What this PR does

ecdsa-spartan2/ — Consolidated Rust crate with cargo feature gating

  • RS256 circuit (RSA-2048, 19 public inputs) with witness-only proving path
  • native feature (default): CLI key generation, witnesscalc-adapter, mobile support
  • No default features: WASM-compatible build (no native deps)
  • Removed ES256/ECDSA code — this branch focuses on RS256 only

openac-sdk/wasm/ — Multi-threaded WASM bindings

  • wasm-bindgen-rayon integration: rayon thread pool backed by Web Workers + SharedArrayBuffer
  • setup_rs256(): generates proving and verifying keys at runtime in the browser
  • load_rs256_pk(): loads serialized proving key into WASM memory for reuse across prove calls
  • prove_rs256() / verify_rs256(): single-circuit RS256 proving and verification
  • Build config: nightly Rust with +atomics,+bulk-memory,+mutable-globals, 4GB max WASM memory

web/ — Vite + TypeScript browser demo

  • 4-step pipeline: Load cert → Generate witness (circom WASM) → Prove (Spartan2) → Verify
  • COOP/COEP headers for SharedArrayBuffer cross-origin isolation
  • scripts/copy-assets.sh for WASM artifact setup (keys optional — generated at runtime)
  • Worker format set to ES modules for wasm-bindgen-rayon compatibility
  • Generated package.json in src/wasm/ for workerHelpers.js import('../../..') resolution

CI

web-build.yaml — End-to-end build verification pipeline with 3 jobs:

  1. compile-circuits — compiles RS256 circom circuit (produces rs256.wasm + witness_calculator.js)
  2. build-wasm — builds Rust WASM bindings with nightly + wasm-bindgen (runs in parallel with step 1)
  3. build-web — copies all WASM artifacts into the web app, runs pnpm build (tsc + vite build)
    • Copies the snippets/ directory from wasm-bindgen output (contains workerHelpers.js needed by wasm-bindgen-rayon for Web Worker threading)
    • Generates a minimal package.json in src/wasm/ for worker module resolution

Triggers on changes to web/, openac-sdk/wasm/, ecdsa-spartan2/, or circom/circuits/.

Key technical decisions

Decision Rationale
Cargo features (native vs no-default) in ecdsa-spartan2 Single crate for both mobile and WASM targets, gated at compile time
Runtime key generation via setup_rs256() Proving keys depend on the circuit structure and should be generated per session, not shared from a static release artifact
load_rs256_pk() split from prove_rs256() PK must be deserialized once, not per-prove call
4GB WASM max memory PK bytes + deserialized ProverKey need headroom
opt-level = 3 over "s" Proving speed matters more than binary size
Nightly Rust + build-std Required for WASM atomics (SharedArrayBuffer threading)
RS256-only (removed ES256/ECDSA) Simplifies WASM demo scope to one circuit
Worker format "es" in Vite config workerHelpers.js uses dynamic import() which requires code-splitting, incompatible with default IIFE format

Screenshot

image

Status

Work in progress. The multi-threaded WASM initialization and proving pipeline is built and compiles, but end-to-end browser proving has not been confirmed yet — the 744MB proving key pushes WASM memory limits. Next step: test in browser after the 4GB max-memory rebuild.

How to run

# 1. Build WASM (nightly required)
cd wallet-unit-poc/openac-sdk/wasm
cargo +nightly build --target wasm32-unknown-unknown --release -Z build-std=panic_abort,std
wasm-bindgen --target web --out-dir pkg target/wasm32-unknown-unknown/release/openac_wasm.wasm

# 2. Copy assets and run (keys generated at runtime in the browser)
cd ../../web
bash scripts/copy-assets.sh
pnpm install && pnpm dev

Related

🤖 Generated with Claude Code

moven0831 and others added 16 commits April 10, 2026 11:57
Separate Rust crate using the 0xVikasRushi/Spartan2 fork (openac-sdk branch)
which compiles to wasm32-unknown-unknown. Supports RS256 circuit with
witness-only proving path for browser use.

Key: Rs256Circuit with 19 public inputs, synthesize_witness_only fallback
when R1CS is unavailable in WASM, CLI for native key generation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add wasm-bindgen-rayon + rayon deps for Web Worker thread pool
- Export init_thread_pool for JS-side thread pool initialization
- Add load_rs256_pk() to deserialize 744MB PK into WASM memory once
- Add prove_rs256() and verify_rs256() for single-circuit RS256 flow
- Configure .cargo/config.toml with atomics, shared-memory, 4GB max memory
- Update wasm-bridge.ts with initWithModule(), loadKeys(), precomputeFromWitness()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Vite + TypeScript browser demo with 4-step pipeline:
1. Load certificate input (test data)
2. Generate witness (circom WASM in browser)
3. Prove RS256 circuit (multi-threaded Spartan2 WASM)
4. Verify proof (Spartan2 WASM)

Uses SharedArrayBuffer + Web Workers for parallel proving.
COOP/COEP headers configured for cross-origin isolation.
scripts/copy-assets.sh handles WASM artifact setup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add circom circuit sources and compilation config for jwt (ES256)
and show circuits, used by the witness calculator in browser.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This PR focuses on RS256 in-browser proving. Remove all ES256/ECDSA
circuit definitions, show/prepare circuit implementations, and related
WASM bindings that are not needed for the RS256 workflow.

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

Switch ecdsa-spartan2 to the WASM-compatible Spartan2 fork (0xVikasRushi/Spartan2
branch openac-sdk) and gate native-only dependencies behind a "native" cargo feature.
This eliminates the duplicate ecdsa-spartan2-jwt crate while supporting both native
and WASM builds from a single crate.

- Add "native" feature (default) gating witnesscalc, x509, rsa, memmap2, etc.
- Add synthesize_witness_only() and with_witness() for WASM proving path
- Add CircuitSize enum and witness-only R1CS fallback
- Update mobile and WASM consumers to use the consolidated crate
- Delete ecdsa-spartan2-jwt/ entirely

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Verifies the full WASM + web pipeline compiles: circom circuit
compilation, Rust WASM bindings build (nightly + wasm-bindgen),
and Vite/TypeScript web app build.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
wasm-bindgen-rayon requires imported shared memory. Without
--import-memory, the WASM binary exports memory instead, causing
wasm-bindgen to panic: assertion failed: mem.import.is_some()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When using --import-memory with -Z build-std, wasm-bindgen needs
__wasm_init_tls and related TLS/stack symbols to be explicitly
exported for thread pool initialization.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The generated openac_wasm.js imports workerHelpers.js from a relative
./snippets/ path. The CI step was only copying individual files from
pkg/, missing the snippets/ directory entirely, causing Vite to fail.

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

The web build and local dev setup now fetch ecdsa-spartan2-keys.zip from
the latest GitHub release when local keys are unavailable. Also fixes the
key filename mismatch (default_rs256_* → rs256_*) in pipeline.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The wasm-bindgen-rayon workerHelpers.js uses import('../../..') to resolve
back to the package root. Without a package.json in src/wasm/, Vite cannot
resolve the bare directory import, causing the build to fail.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
wasm-bindgen (unlike wasm-pack) does not generate package.json, so the
CI artifact lacks it. Generate a minimal one inline with the module entry
point needed for workerHelpers.js import('../../..') resolution.

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

The workerHelpers.js uses dynamic import() which requires code-splitting,
incompatible with Vite's default IIFE worker format. Setting worker format
to 'es' allows the dynamic import to resolve correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…of downloading from GitHub release

Proving keys depend on the circuit structure and should be generated per
session rather than fetched from a shared release artifact. The WASM module
already exports setup_rs256() which generates keys in-browser.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@moven0831
Copy link
Copy Markdown
Collaborator Author

The refreshed implementation with separate WASM modules in feat/web-prover shows out-of-memory error with peak memory > 4 GiB in the web app. This proves that the monolithic sha256rsa4096 circuit is infeasible.

We need to refactor/optimize the circuit further. e.g.,

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.

1 participant