The ProveKit WASM Demo is a browser-based zero-knowledge proof system with GPU acceleration. It consists of three main applications and a comprehensive build/verification pipeline.
flowchart TD
Start([User Opens Browser]) --> Load[Load index.html]
Load --> Init[Initialize WASM Module]
Init --> ThreadPool{SharedArrayBuffer<br/>Available?}
ThreadPool -->|Yes| MultiThread[Initialize Thread Pool<br/>N workers]
ThreadPool -->|No| SingleThread[Single-threaded Mode]
MultiThread --> CheckGPU
SingleThread --> CheckGPU
CheckGPU{WebGPU<br/>Available?}
CheckGPU -->|Yes| InitGPU[Initialize GPU NTT Bridge]
CheckGPU -->|No| CPUOnly[CPU NTT Only]
InitGPU --> SelectCircuit
CPUOnly --> SelectCircuit
SelectCircuit[User Selects Circuit]
SelectCircuit --> CircuitType{Circuit Type?}
CircuitType -->|SHA256| LoadSHA[Load SHA256 Artifacts]
CircuitType -->|Poseidon| LoadPos[Load Poseidon Artifacts]
CircuitType -->|Custom| Upload[User Uploads Files]
LoadSHA --> LoadArtifacts
LoadPos --> LoadArtifacts
Upload --> ValidateFiles{All Files<br/>Present?}
ValidateFiles -->|No| Upload
ValidateFiles -->|Yes| LoadArtifacts
LoadArtifacts[Load Circuit + Inputs]
LoadArtifacts --> InitNoir[Initialize noir_js]
InitNoir --> ClickGenerate[User Clicks Generate Proof]
ClickGenerate --> GenWitness[Generate Witness<br/>ACVM Execution]
GenWitness --> Prove[Call proveBytes]
Prove --> ProveInternal{NTT Engine?}
ProveInternal -->|GPU| GPUProve[Prove with GPU NTT]
ProveInternal -->|CPU| CPUProve[Prove with CPU NTT]
GPUProve --> ShowProof[Display Proof + Metrics]
CPUProve --> ShowProof
ShowProof --> UserAction{User Action?}
UserAction -->|Verify| VerifyProof[Call verifyBytes]
UserAction -->|Change Circuit| SelectCircuit
UserAction -->|Done| End([End])
VerifyProof --> VerifyResult{Valid?}
VerifyResult -->|Yes| ShowSuccess[β
Proof Valid]
VerifyResult -->|No| ShowError[β Proof Invalid]
ShowSuccess --> UserAction
ShowError --> UserAction
style Init fill:#4a90e2,color:#fff
style InitGPU fill:#50c878,color:#fff
style GenWitness fill:#9b59b6,color:#fff
style Prove fill:#e74c3c,color:#fff
style ShowSuccess fill:#27ae60,color:#fff
style ShowError fill:#c0392b,color:#fff
flowchart TD
Start([User Opens Benchmark]) --> InitMain[Main Thread: Initialize]
InitMain --> InitGPU[Initialize WebGPU Device]
InitGPU --> LoadCircuit[Load Circuit Metadata]
LoadCircuit --> GenWitness[Generate Witness]
GenWitness --> SpawnWorker[Spawn Web Worker]
SpawnWorker --> WorkerInit[Worker: Load WASM]
WorkerInit --> WorkerThreads[Worker: Init Thread Pool]
WorkerThreads --> WorkerGPU[Worker: Init GPU Bridge]
WorkerGPU --> UserClick[User Clicks Run Benchmark]
UserClick --> CPUPhase[Phase 1: CPU NTT]
CPUPhase --> RegCPU[Register CPU NTT Engine]
RegCPU --> ProveCPU[proveBytes with CPU NTT]
ProveCPU --> RecordCPU[Record CPU Time]
RecordCPU --> GPUPhase[Phase 2: GPU NTT]
GPUPhase --> RegGPU[Register GPU NTT Engine]
RegGPU --> ProveGPU[proveBytes with GPU NTT]
ProveGPU --> RecordGPU[Record GPU Time]
RecordGPU --> Calculate[Calculate Speedup %]
Calculate --> Compare{Speedup β₯ 6%?}
Compare -->|Yes| Success[β
Target Met<br/>Display Results]
Compare -->|No| Warning[β οΈ Below Target<br/>Display Results]
Success --> Done([End])
Warning --> Done
style InitGPU fill:#4a90e2,color:#fff
style ProveCPU fill:#e67e22,color:#fff
style ProveGPU fill:#50c878,color:#fff
style Success fill:#27ae60,color:#fff
style Warning fill:#f39c12,color:#fff
flowchart TD
Start([node verify-speedup.mjs]) --> Test1[Test 1: Limb Conversion]
Test1 --> Test2[Test 2: Serialization Round-trips]
Test2 --> Test3[Test 3: NTT Correctness]
Test3 --> Test4[Test 4: Measure Serialization Overhead]
Test4 --> Test5[Test 5: Benchmark JS CPU NTT]
Test5 --> Project[Project Speedup via Amdahl's Law]
Project --> Compare{Projected<br/>β₯ 6%?}
Compare -->|Yes| Success[β
Exit 0<br/>Projected to Meet Target]
Compare -->|No| Fail[β οΈ Exit 1<br/>Optimization Needed]
style Test3 fill:#9b59b6,color:#fff
style Project fill:#3498db,color:#fff
style Success fill:#27ae60,color:#fff
style Fail fill:#e74c3c,color:#fff
graph TB
subgraph Browser["π Browser Environment"]
subgraph MainThread["Main Thread"]
UI[UI Layer<br/>index.html]
DemoWeb[demo-web.mjs<br/>Application Logic]
NoirJS[noir_js<br/>Witness Generation]
GPUDevice[WebGPU Device<br/>GPU Context]
Bridge[webgpu-ntt-bridge.mjs<br/>GPU Coordinator]
end
subgraph Worker["Web Worker Thread"]
ProveWorker[prove-worker.mjs<br/>Message Handler]
WASM[provekit_wasm<br/>Rust Bindings]
Rayon[Rayon Thread Pool<br/>Parallel Proving]
NTTEngine[NTT Engine<br/>CPU or GPU]
end
subgraph GPU["GPU Compute"]
Shader[ntt_butterfly.wgsl<br/>Compute Shader]
Buffers[GPU Buffers<br/>Elements + Twiddles]
end
end
subgraph Sync["Synchronization Layer"]
SAB[SharedArrayBuffer<br/>Data Exchange]
BC[BroadcastChannel<br/>Messages]
Atomics[Atomics.wait/notify<br/>Blocking Sync]
end
subgraph Data["Data Flow"]
ArkFF[ark-ff Format<br/>4Γu64 LE R=2^256]
BigInt[BigInt<br/>Intermediate]
GPULimbs[GPU Format<br/>9Γ29-bit R=2^261]
end
UI --> DemoWeb
DemoWeb --> NoirJS
DemoWeb --> GPUDevice
GPUDevice --> Bridge
DemoWeb -.Message.-> ProveWorker
ProveWorker --> WASM
WASM --> Rayon
Rayon --> NTTEngine
NTTEngine -.GPU Call.-> BC
BC --> Bridge
Bridge --> Shader
Shader --> Buffers
Bridge -.Result.-> SAB
SAB -.Notify.-> Atomics
Atomics -.Unblock.-> NTTEngine
NTTEngine --> ArkFF
ArkFF --> BigInt
BigInt --> GPULimbs
GPULimbs --> Shader
Shader --> GPULimbs
GPULimbs --> BigInt
BigInt --> ArkFF
style MainThread fill:#e3f2fd
style Worker fill:#fff3e0
style GPU fill:#e8f5e9
style Sync fill:#fce4ec
style Data fill:#f3e5f5
style Bridge fill:#50c878,color:#fff
style Shader fill:#e74c3c,color:#fff
style WASM fill:#4a90e2,color:#fff
sequenceDiagram
participant Rust as Rust Prover<br/>(Worker Thread)
participant Worker as prove-worker.mjs<br/>(Worker)
participant BC as BroadcastChannel
participant Main as Main Thread
participant GPU as WebGPU Device
participant SAB as SharedArrayBuffer
Note over Rust: During proveBytes()
Rust->>Worker: gpuNttComputeSync(elements)
Worker->>Worker: Serialize Fr β bytes
Worker->>SAB: Write elements
Worker->>BC: Post GPU NTT request
Worker->>SAB: Atomics.wait(signal, 0)
Note over Worker: π BLOCKED
BC->>Main: Receive request
Main->>Main: Read elements from SAB
Main->>Main: Convert ark-ff β GPU limbs<br/>(Γ2^5 mod p)
Main->>GPU: Upload elements buffer
Main->>GPU: Upload twiddles buffer
loop For each stage s = 0..logβ(n)
Main->>GPU: Set stage parameters
Main->>GPU: Dispatch compute shader
Note over GPU: 256 threads/workgroup<br/>Process n/2 butterfly pairs
end
Main->>GPU: mapAsync(READ)
GPU-->>Main: Results ready
Main->>Main: Read GPU buffer
Main->>Main: Convert GPU limbs β ark-ff<br/>(Γ2^-5 mod p)
Main->>SAB: Write results
Main->>SAB: Atomics.store(signal, 1)
Main->>SAB: Atomics.notify(signal)
Note over Worker: π UNBLOCKED
SAB-->>Worker: Read results
Worker->>Rust: Return proof bytes
flowchart LR
subgraph Rust["Rust (ark-ff)"]
Fr1[Fr Element<br/>4Γu64 LE<br/>Montgomery R=2^256]
end
subgraph JS["JavaScript Bridge"]
Bytes[32 bytes<br/>Little Endian]
BI1[BigInt<br/>256-bit]
Mult1[Γ 2^5 mod p<br/>Radix Adjustment]
BI2[BigInt<br/>261-bit]
Split[Split into<br/>9Γ29-bit limbs]
end
subgraph GPU["GPU (WGSL)"]
Limbs[9Γu32 limbs<br/>Montgomery R=2^261]
Compute[Montgomery<br/>Arithmetic]
Result[9Γu32 limbs<br/>Result]
end
subgraph JSReturn["JavaScript Bridge"]
Combine[Combine limbs<br/>to BigInt]
BI3[BigInt<br/>261-bit]
Mult2[Γ 2^-5 mod p<br/>Radix Adjustment]
BI4[BigInt<br/>256-bit]
ToBytes[Convert to<br/>32 bytes LE]
end
subgraph RustReturn["Rust (ark-ff)"]
Fr2[Fr Element<br/>4Γu64 LE<br/>Montgomery R=2^256]
end
Fr1 --> Bytes
Bytes --> BI1
BI1 --> Mult1
Mult1 --> BI2
BI2 --> Split
Split --> Limbs
Limbs --> Compute
Compute --> Result
Result --> Combine
Combine --> BI3
BI3 --> Mult2
Mult2 --> BI4
BI4 --> ToBytes
ToBytes --> Fr2
style Rust fill:#e3f2fd
style GPU fill:#e8f5e9
style JS fill:#fff3e0
style JSReturn fill:#fff3e0
style RustReturn fill:#e3f2fd
style Compute fill:#e74c3c,color:#fff
flowchart TD
Start([npm run setup]) --> Check[Check Prerequisites]
Check --> Nargo{nargo<br/>installed?}
Nargo -->|No| InstallNargo[Install Noir]
Nargo -->|Yes| CheckWB
InstallNargo --> CheckWB
CheckWB{wasm-bindgen<br/>installed?}
CheckWB -->|No| InstallWB[cargo install<br/>wasm-bindgen-cli]
CheckWB -->|Yes| NPM
InstallWB --> NPM
NPM[npm install] --> Vendor[Copy Vendor Files<br/>acvm_js + noirc_abi]
Vendor --> BuildWASM[Build WASM<br/>with Atomics Support]
BuildWASM --> Bindgen[wasm-bindgen<br/>Generate JS Bindings]
Bindgen --> CLI[Build Native CLI]
CLI --> Circuits[For Each Circuit]
Circuits --> Compile[nargo compile]
Compile --> Prepare[provekit-cli prepare]
Prepare --> Convert[Convert Prover.toml<br/>to inputs.json]
Convert --> Metadata[Save metadata.json]
Metadata --> Done([β
Setup Complete])
style BuildWASM fill:#4a90e2,color:#fff
style Bindgen fill:#9b59b6,color:#fff
style Done fill:#27ae60,color:#fff
playground/wasm-demo/
βββ index.html # Main demo UI
βββ ntt-bench.html # CPU vs GPU benchmark UI
βββ verify-speedup.mjs # Node.js verification script
βββ scripts/
β βββ setup.mjs # Build pipeline
β βββ serve.mjs # Dev server with COOP/COEP headers
βββ src/
β βββ demo-web.mjs # Main demo logic
β βββ prove-worker.mjs # Web Worker for proving
β βββ webgpu-ntt.mjs # Reference NTT implementation
β βββ webgpu-ntt-bridge.mjs # GPU synchronization bridge
β βββ shaders/
β βββ ntt_butterfly.wgsl # GPU compute shader
βββ noir-web/
β βββ noir-init.mjs # Browser Noir wrapper
βββ pkg/ # WASM bindings (generated)
β βββ provekit_wasm.js
β βββ provekit_wasm_bg.wasm
β βββ snippets/ # Rayon worker helpers
βββ vendor/ # Noir dependencies (copied)
β βββ acvm_js/
β βββ noirc_abi/
βββ artifacts/ # Circuit artifacts (generated)
βββ sha256/
βββ poseidon/
βββ complete_age_check/
βββ circuit.json
βββ prover.pkp
βββ verifier.pkv
βββ inputs.json
βββ Prover.toml
βββ metadata.json
- Main Thread: Owns WebGPU device, handles async GPU operations, runs UI
- Web Worker: Runs WASM prover with Rayon thread pool (up to N cores)
- Rayon Workers: Spawned by wasm-bindgen-rayon for parallel proving
- Synchronization: SharedArrayBuffer + Atomics.wait/notify for blocking GPU calls
ark-ff β GPU Montgomery Form:
ark-ff (4Γu64 LE, R=2^256)
β Read as BigInt
β Multiply by 2^5 mod p (Montgomery radix adjustment)
β Split into 9Γ29-bit limbs
GPU (9Γu32 limbs, R=2^261)
β GPU computation (Montgomery arithmetic)
β Reassemble BigInt from limbs
β Multiply by (2^5)^-1 mod p
β Write as 4Γu64 LE
ark-ff (4Γu64 LE, R=2^256)
Why 9Γ29-bit limbs?
- 29 bits per limb allows 2 limbs to multiply without overflow in u32
- 9 limbs Γ 29 bits = 261 bits (covers 254-bit BN254 field)
- GPU u32 operations are fast, no need for u64
Cooley-Tukey DIT (Decimation in Time):
- Input: Normal order β Output: Reverse bit order
- Stages: logβ(n) stages, each processes n/2 butterfly pairs
- Twiddle factors: Ο^k where Ο = primitive nth root of unity
- Cached per size in reverse bit order for efficient access
GPU Dispatch:
- 256 threads per workgroup
- Each thread processes one butterfly pair
- Workgroups = β(n/2) / 256β
- One dispatch per stage (logβ(n) total dispatches)
Field Arithmetic:
- Montgomery multiplication: (a Γ b Γ R^-1) mod p
- Addition/subtraction with carry/borrow propagation
- Reduction: conditional subtraction if result β₯ p
Three Measurement Approaches:
-
Isolated GPU Benchmark (webgpu-ntt.mjs reference impl)
- Measures GPU compute time only
- No serialization overhead
- Used for GPU capability testing
-
Integrated Benchmark (ntt-bench.html)
- Measures end-to-end proveBytes() wall-clock time
- Includes serialization overhead
- Compares CPU NTT vs GPU NTT in real proving context
- This is the ground truth measurement
-
Projection (verify-speedup.mjs)
- Uses Amdahl's Law: S = 1 / (1 - f + f/s)
- f = 20.5% (NTT fraction from native profiling)
- s = CPU_time / GPU_time (speedup factor)
- Predicts end-to-end improvement
Amdahl's Law Example:
If NTT is 20.5% of prove time and GPU is 3Γ faster:
S = 1 / (1 - 0.205 + 0.205/3)
= 1 / (0.795 + 0.068)
= 1 / 0.863
= 1.159
β 15.9% overall speedup
verify-speedup.mjs tests:
- Limb conversion round-trips (BigInt β 9Γ29-bit)
- Serialization round-trips (ark-ff β GPU Montgomery)
- NTT linearity: NTT(a+b) = NTT(a) + NTT(b)
- Known vector validation: NTT([1,2,3,4])[0] = 10
- Montgomery identity: R_RATIO Γ R_RATIO_INV β‘ 1 (mod p)
- Field element range checks: all outputs < p
Required for SharedArrayBuffer:
Cross-Origin-Opener-Policy: same-originCross-Origin-Embedder-Policy: require-corp- Set by scripts/serve.mjs
- Enables Atomics.wait/notify for synchronous GPU calls
wasm-bindgen-rayon integration:
- Requires
-Z build-stdto rebuild std with atomics - RUSTFLAGS in .cargo/config.toml enable shared memory
- Thread pool initialized with navigator.hardwareConcurrency
- Falls back to single-threaded if SharedArrayBuffer unavailable
- User selects circuit (SHA256, Poseidon, or Custom)
- Load WASM module + initialize thread pool
- Initialize WebGPU (optional, for GPU NTT)
- Register NTT engine (GPU if available, else CPU)
- Load circuit artifacts (prover.pkp, verifier.pkv, inputs.json)
- Generate witness using noir_js (ACVM execution)
- Call proveBytes() β proof generated
- Display proof + metrics
- User clicks Verify β verifyBytes() β result
- Initialize WebGPU on main thread
- Load circuit metadata
- Generate witness on main thread
- Spawn Web Worker
- Worker: Initialize WASM + thread pool
- Worker: Initialize GPU bridge
- CPU Benchmark:
- Register CPU NTT engine
- Call proveBytes() β record time
- GPU Benchmark:
- Register GPU NTT engine
- Call proveBytes() β record time
- Calculate speedup: (CPU_time - GPU_time) / CPU_time Γ 100%
- Display results + target comparison (β₯6%)
- Run correctness tests (limbs, serialization, NTT)
- Measure serialization overhead (BigInt conversions)
- Benchmark JS CPU NTT (comparable to WASM CPU NTT)
- Use hardcoded GPU timings from browser benchmark
- Project speedup using Amdahl's Law
- Compare projection to 6% target
- Exit with status code (0 = pass, 1 = fail)
Added:
ntt-bench.html: Browser-based CPU vs GPU NTT comparison (642 lines)verify-speedup.mjs: Node.js correctness tests + Amdahl's Law projection (248 lines)
Modified:
setup.mjs: Added complete_age_check circuit, improved TOML parser
Purpose:
- Establish ground truth measurement (ntt-bench.html)
- Validate correctness before optimization (verify-speedup.mjs)
- Provide projection methodology for future improvements
Added:
prove-worker.mjs: Dedicated Web Worker for proof generation (96 lines)webgpu-ntt-bridge.mjs: Main thread β worker synchronization bridge (376 lines)gpu_ntt.rs: Rust FFI for GPU NTT integration (158 lines)
Modified:
webgpu-ntt.mjs: Removed GPU code, kept reference implementation (516 β smaller)ntt-bench.html: Refactored to use worker architecture (835 β 642 lines)demo-web.mjs: Added GPU bridge initializationverify-speedup.mjs: Enhanced with serialization overhead measurement
Deleted:
bn254_field.wgsl: Consolidated intontt_butterfly.wgsl
Purpose:
- Separate concerns: main thread (GPU) vs worker (proving)
- Enable synchronous GPU calls from worker via Atomics
- Improve code organization and maintainability
- Reduce duplication between benchmark and main demo
| Metric | Value | Source |
|---|---|---|
| Minimum Speedup | β₯6% | End-to-end prove time improvement |
| NTT Fraction | 20.5% | Native profiling (docs/report.md) |
| Target Circuit | complete_age_check | ~1.3M constraints |
| GPU Compute | ~850ms @ 2^20 | Browser benchmark (Apple Silicon) |
| CPU Compute | ~3500ms @ 2^20 | JS BigInt (comparable to WASM) |
| Serialization | ~150ms @ 2^20 | BigInt β limbs conversion |
Bottleneck Analysis:
- GPU compute is 4Γ faster than CPU
- Serialization overhead is ~18% of GPU time
- Net speedup: ~3.3Γ (GPU+ser vs CPU)
- Amdahl's Law: 1/(1-0.205+0.205/3.3) = 1.14 β 14% improvement
- Current projection: 14% > 6% target β
Optimization Path:
- Replace BigInt with TypedArray-based conversion (eliminate serialization)
- Use GPU-side Montgomery conversion (move conversion to shader)
- Batch multiple NTT calls (reduce dispatch overhead)
- Optimize twiddle factor caching (reduce memory bandwidth)