Skip to content

Instantly share code, notes, and snippets.

@MdSadiqMd
Created April 3, 2026 05:50
Show Gist options
  • Select an option

  • Save MdSadiqMd/87fb9fe6a947974bd3daadd1dd2fba7e to your computer and use it in GitHub Desktop.

Select an option

Save MdSadiqMd/87fb9fe6a947974bd3daadd1dd2fba7e to your computer and use it in GitHub Desktop.

ProveKit WASM Demo - Complete Architecture Documentation

System Overview

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.


1. User Flow Diagram

Main Demo Application (index.html)

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
Loading

NTT Benchmark Application (ntt-bench.html)

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
Loading

Verification Script (verify-speedup.mjs)

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
Loading

2. Technical Architecture Diagram

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
Loading

3. GPU NTT Execution Flow (Detailed)

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
Loading

4. Data Format Conversion Pipeline

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
Loading

5. Build Pipeline (scripts/setup.mjs)

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
Loading

6. Directory Structure

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

7. Key Technical Components

1. Thread Architecture

  • 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

2. Data Format Conversions

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

3. NTT Butterfly Algorithm

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

4. Performance Measurement

Three Measurement Approaches:

  1. Isolated GPU Benchmark (webgpu-ntt.mjs reference impl)

    • Measures GPU compute time only
    • No serialization overhead
    • Used for GPU capability testing
  2. 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
  3. 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

5. Correctness Verification

verify-speedup.mjs tests:

  1. Limb conversion round-trips (BigInt ↔ 9Γ—29-bit)
  2. Serialization round-trips (ark-ff ↔ GPU Montgomery)
  3. NTT linearity: NTT(a+b) = NTT(a) + NTT(b)
  4. Known vector validation: NTT([1,2,3,4])[0] = 10
  5. Montgomery identity: R_RATIO Γ— R_RATIO_INV ≑ 1 (mod p)
  6. Field element range checks: all outputs < p

6. Cross-Origin Isolation

Required for SharedArrayBuffer:

  • Cross-Origin-Opener-Policy: same-origin
  • Cross-Origin-Embedder-Policy: require-corp
  • Set by scripts/serve.mjs
  • Enables Atomics.wait/notify for synchronous GPU calls

7. WASM Threading

wasm-bindgen-rayon integration:

  • Requires -Z build-std to 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

Application Workflows

Main Demo (index.html)

  1. User selects circuit (SHA256, Poseidon, or Custom)
  2. Load WASM module + initialize thread pool
  3. Initialize WebGPU (optional, for GPU NTT)
  4. Register NTT engine (GPU if available, else CPU)
  5. Load circuit artifacts (prover.pkp, verifier.pkv, inputs.json)
  6. Generate witness using noir_js (ACVM execution)
  7. Call proveBytes() β†’ proof generated
  8. Display proof + metrics
  9. User clicks Verify β†’ verifyBytes() β†’ result

NTT Benchmark (ntt-bench.html)

  1. Initialize WebGPU on main thread
  2. Load circuit metadata
  3. Generate witness on main thread
  4. Spawn Web Worker
  5. Worker: Initialize WASM + thread pool
  6. Worker: Initialize GPU bridge
  7. CPU Benchmark:
    • Register CPU NTT engine
    • Call proveBytes() β†’ record time
  8. GPU Benchmark:
    • Register GPU NTT engine
    • Call proveBytes() β†’ record time
  9. Calculate speedup: (CPU_time - GPU_time) / CPU_time Γ— 100%
  10. Display results + target comparison (β‰₯6%)

Verification Script (verify-speedup.mjs)

  1. Run correctness tests (limbs, serialization, NTT)
  2. Measure serialization overhead (BigInt conversions)
  3. Benchmark JS CPU NTT (comparable to WASM CPU NTT)
  4. Use hardcoded GPU timings from browser benchmark
  5. Project speedup using Amdahl's Law
  6. Compare projection to 6% target
  7. Exit with status code (0 = pass, 1 = fail)

Commit Summary

Commit 1: 79c9d09 - Benchmark and Speedup Verification

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

Commit 2: 86f34d9 - Architecture Refactoring

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 initialization
  • verify-speedup.mjs: Enhanced with serialization overhead measurement

Deleted:

  • bn254_field.wgsl: Consolidated into ntt_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

Performance Targets

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:

  1. Replace BigInt with TypedArray-based conversion (eliminate serialization)
  2. Use GPU-side Montgomery conversion (move conversion to shader)
  3. Batch multiple NTT calls (reduce dispatch overhead)
  4. Optimize twiddle factor caching (reduce memory bandwidth)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment