Browser Proving Engine
Hardware detection, memory budgets, and parallel worker management.
The previous sections described the circuits — what gets proven and how proofs are aggregated. This section explains where all of that runs: entirely in the user's browser, using WebAssembly and web workers.
Apertrue generates ZK proofs client-side. No image data or private metadata leaves the browser during proof generation. The proving engine detects the device's hardware capabilities, allocates workers within a memory budget, loads the correct circuit variants, and orchestrates the entire pipeline from split proofs through aggregation — all inside a browser tab.
Hardware detection
Before allocating any workers, the engine reads two browser APIs to understand the device:
| API | What it provides | Limitations |
|---|---|---|
| navigator.hardwareConcurrency | Logical CPU core count | Always available; falls back to 1 |
| navigator.deviceMemory | Approximate RAM in GB (bucketed: 0.25, 0.5, 1, 2, 4, 8) | Chrome/Edge only. Caps at 8 GB. Absent in Firefox and Safari. |
| SharedArrayBuffer | Multi-threaded WASM support | Requires COOP/COEP headers |
| crossOriginIsolated | Whether COOP/COEP headers are active | Required for SharedArrayBuffer |
Because deviceMemory caps at 8 GB and is absent in Firefox/Safari, the system uses hardwareConcurrency as a proxy for RAM: 16+ cores implies 32 GB, 12+ implies 16 GB, 8+ implies 8 GB. The system takes whichever signal gives the higher tier, preventing false-low estimates.
Memory tiers and worker allocation
Based on the detected hardware, the engine assigns a memory budget and allocates workers:
| Tier | Budget | Split workers | Agg workers | Max batch | Pipelining |
|---|---|---|---|---|---|
| High (16+ cores) | 3000 MB | 4 | 4 (2 overlap) | 10 images | Yes |
| Mid (8+ cores) | 2000 MB | 2 | 3 | 6 images | No |
| Low (<8 cores) | 1200 MB | 1 | 1 | 4 images | No |
Each split worker costs ~400 MB of physical memory (SRS + WASM linear memory + circuit data). Each aggregation worker costs ~350 MB. The budget ensures the total physical memory used by all workers stays within what the device can handle without crashing the browser tab.
Web workers
All proving runs in web workers — background threads that don't block the UI. The workers are esbuild bundles (not native ES modules) served from /wasm/. Bundling with esbuild makes the entire dependency tree available offline and avoids cross-origin issues with worker script loading.
Worker types
- Split worker (
zk-worker-split.js) — generates ProofA and ProofB for a single image. Holds one Barretenberg instance with two UltraHonk backends. - ProofB worker (
zk-worker-proof-b.js) — dedicated ProofB worker for dual-worker mode on high-end devices. Receives ProofB inputs from a split worker that only generates ProofA. - Aggregator worker — generates image aggregation and tree aggregation proofs. Holds its own Barretenberg instance with both aggregation circuit backends.
Message protocol
Workers communicate with the main thread via structured messages:
| Message | Direction | Purpose |
|---|---|---|
| initialize | Main → Worker | Strategy, thread count, algorithm hints |
| initialised | Worker → Main | Success/failure, memory usage |
| prove-split | Main → Worker | Image data, manifest, privacy settings, recursive flag |
| progress | Worker → Main | Stage, percentage, message, memory usage |
| prove-split-complete | Worker → Main | Proof result with optional recursive artefacts |
| prove-split-error | Worker → Main | Error message |
| load-circuit | Main → Worker | Hot-load a different circuit variant |
Worker initialisation
When a worker receives the initialize message, it goes through these steps:
- Module imports. Lazily imports Noir, Barretenberg, the C2PA extractor, circuit input builder, and oracle client via dynamic
import(). - ACVM and ABI init. Initialises the Noir WASM modules for witness generation.
- Circuit fetch. Fetches one or two circuit JSON files from
/circuits/based on algorithm hints. Specialised variants are selected when the algorithm is known in advance. - Barretenberg instantiation. Creates the WASM proving engine with the configured thread count. Multi-threading requires SharedArrayBuffer.
- SRS loading. Loads the Structured Reference String — 221 BN254 curve points (~200 MB). This is the slowest step on first load. Subsequent loads serve from IndexedDB cache (~3 seconds vs ~30 seconds).
- Backend creation. Two UltraHonk backends (one per circuit) sharing the same Barretenberg instance and SRS.
- VK pre-warming. After reporting success, the worker generates and caches verification keys in IndexedDB in the background. This saves 4-10 seconds on subsequent proof jobs.
The SRS
The Structured Reference String is a set of elliptic curve points on BN254, required by the UltraHonk proving system. It's the largest single resource the engine needs — roughly 200 MB for 2,097,152 points.
- Source: Fetched from the Aztec CRS CDN, proxied through a Next.js API route at
/api/crs/to comply with COEP headers. - Caching: Stored in IndexedDB under the
keyval-storedatabase. After the first fetch, subsequent loads are near-instant. - Sharing: Within a single worker, the SRS is loaded once and shared between both UltraHonk backends (ProofA and ProofB).
- Range requests: The proxy forwards HTTP Range headers, allowing Barretenberg to fetch only the portion of the SRS needed for the current circuit size.
credentialless) blocks cross-origin fetches that don't opt in with Cross-Origin-Resource-Policy headers. The Aztec CDN doesn't set these headers. The proxy fetches server-side and streams the response back as same-origin, bypassing the restriction. Worker bundles are patched at build time to replace CDN URLs with /api/crs/.Circuit variant selection
Rather than loading a single large circuit that handles all algorithms, the engine selects the smallest specialised variant that matches the image's cryptographic algorithm. This reduces constraint counts by 40-66% and proportionally reduces proving time.
The circuit registry maps algorithm types to circuit files:
- ProofA variant — selected by the intermediate certificate's key type: ECDSA P-256 (~120K constraints), ECDSA P-384 (~150K), RSA-2048 (~180K), RSA-4096 (~220K), Skip (~80K for 2-cert chains), or Unified fallback (~350K).
- ProofB variant — selected by the COSE signature algorithm: ES256 (~100K) or PS256 (~160K), with Unified fallback (~200K).
If a worker was initialised for one algorithm and receives an image with a different algorithm, it hot-reloads the correct circuit variant — recreating the UltraHonk backend while reusing the existing Barretenberg instance and SRS. If the specialised variant fails to load (network error, 404), the worker falls back to the unified circuit.
The proving flow
- Extraction. Each image's C2PA manifest is parsed, the trust tier is determined, and Tier 4 images are blocked. The algorithm type is detected for circuit variant selection.
- Privacy configuration. The user selects disclosure levels for time (hidden, year, month, exact) and location (hidden, country, city, exact). These become circuit inputs for the range proofs.
- Split proofs. The prover pool distributes images across split workers. Each worker generates ProofA and ProofB in parallel. Progress updates flow from workers to the UI via the message protocol.
- Worker release. After all split proofs complete, split workers are terminated, freeing memory for aggregation.
- Aggregation. The aggregation orchestrator runs image aggregation (one per image) then tree aggregation (binary tree) to produce a single root commitment and proof.
- Backend submission. One API call sends the aggregated proof, root commitment, content hashes, and nullifiers. The backend verifies and stores atomically.
- On-chain record. If blockchain verification is enabled, the browser submits an Aztec private transaction with the root commitment.
Progress reporting
The UI shows real-time progress at three levels:
- Initialisation progress — a 0-100% bar during SRS loading and circuit fetching, shown with a pulsing shield icon.
- Per-image progress — each image shows its current stage (extracting, verifying, proving, uploading, complete) with an animated progress bar.
- Aggregation progress — stage label (aggregating images, aggregating tree), current level, and completion count within the level.
Error recovery
The engine handles failures at multiple levels:
- Worker crash. If a worker's
onerrorfires, the current job is marked as failed and remaining jobs continue on other workers. - Circuit mismatch. If the loaded circuit doesn't match the image's algorithm, the worker hot-reloads the correct variant. If that fails, it falls back to the unified circuit.
- Aggregation failure. If any aggregation step throws, the system falls back to submitting individual proofs without aggregation. The backend accepts both aggregated and individual proof formats.
- Pipeline timeout. Pipelined image aggregation (high-end overlap mode) has a 60-second timeout. If it expires, the system falls back to batch aggregation.
WASM requirements
The proving engine requires specific browser capabilities:
- SharedArrayBuffer — required for multi-threaded WASM. Without it, Barretenberg falls back to single-threaded mode (significantly slower).
- Cross-Origin Isolation — the server must set
Cross-Origin-Opener-Policy: same-originandCross-Origin-Embedder-Policy: credentialless. These headers are configured innext.config.jsand applied to all routes. - IndexedDB — used for SRS caching and verification key caching. If unavailable (private browsing in some browsers), every session re-downloads the SRS.
- WebAssembly — the proving system is compiled to WASM from C++ (via Barretenberg) and Rust (via Noir's ACVM). All modern browsers support WASM.
COEP: credentialless is used rather than require-corp specifically to allow cross-origin resource loading (fonts, analytics) while still enabling SharedArrayBuffer. The credentialless policy permits cross-origin fetches without the Cross-Origin-Resource-Policy header, as long as credentials are omitted.The next section covers the privacy model — how the proving engine's range proofs, selective disclosure, and data stripping work together to protect user privacy while maintaining verifiability.