You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When running SmartContract.compile() → prove() → prove() in a web worker with FinalizationRegistry active and the compile cache populated, the second prove() call hangs indefinitely. The first prove() succeeds. Fresh compilation (cache miss) does not exhibit the hang. Replacing FinalizationRegistry with a no-op before o1js loads avoids it.
Runtime: web worker with SharedArrayBuffer (COOP/COEP headers)
Browser: Chrome 130+
weak-refs feature: enabled in the wasm-bindgen build (confirms FinalizationRegistry is active)
Root cause analysis
The web worker's overrideBindings() in web-backend.js proxies WASM calls to a dedicated worker thread. Return values are wrapped on the main thread via Class.__wrap(ptr), which registers them with wasm-bindgen's per-class FinalizationRegistry. There are 114 such registries in plonk_wasm.js.
The fundamental problem: multiple JS wrappers are created for the same underlying WASM pointer. Each wrapper independently registers with a FinalizationRegistry. When GC collects any one of them, the destructor frees the WASM allocation — even though other wrappers (and the Rust prover) still hold the same pointer.
This manifests specifically with the compile cache because decodeProverKey calls WASM decode functions (e.g. caml_pasta_fp_plonk_index_decode) which go through overrideBindings, producing registered wrappers. Fresh compilation keeps everything on the WASM/OCaml side where JS GC can't reach it.
Additionally, the worker calls __destroy_into_raw() before sending pointers back to the main thread — transferring ownership. For singleton resources like the SRS, this means the worker gives up ownership each time, but the same allocation is returned on subsequent calls. The main thread creates a new wrapper each time, and when any previous wrapper is GC'd, the underlying singleton is freed.
Reproduction
Load app in web worker with COOP/COEP (FinalizationRegistry shim disabled)
compile() with warm cache (prover keys loaded via decodeProverKey)
Summary
When running
SmartContract.compile()→prove()→prove()in a web worker withFinalizationRegistryactive and the compile cache populated, the secondprove()call hangs indefinitely. The firstprove()succeeds. Fresh compilation (cache miss) does not exhibit the hang. ReplacingFinalizationRegistrywith a no-op before o1js loads avoids it.Environment
mesa-srs-fix(commit e0a1022)SharedArrayBuffer(COOP/COEP headers)weak-refsfeature: enabled in the wasm-bindgen build (confirms FinalizationRegistry is active)Root cause analysis
The web worker's
overrideBindings()inweb-backend.jsproxies WASM calls to a dedicated worker thread. Return values are wrapped on the main thread viaClass.__wrap(ptr), which registers them with wasm-bindgen's per-classFinalizationRegistry. There are 114 such registries inplonk_wasm.js.The fundamental problem: multiple JS wrappers are created for the same underlying WASM pointer. Each wrapper independently registers with a FinalizationRegistry. When GC collects any one of them, the destructor frees the WASM allocation — even though other wrappers (and the Rust prover) still hold the same pointer.
This manifests specifically with the compile cache because
decodeProverKeycalls WASM decode functions (e.g.caml_pasta_fp_plonk_index_decode) which go throughoverrideBindings, producing registered wrappers. Fresh compilation keeps everything on the WASM/OCaml side where JS GC can't reach it.Additionally, the worker calls
__destroy_into_raw()before sending pointers back to the main thread — transferring ownership. For singleton resources like the SRS, this means the worker gives up ownership each time, but the same allocation is returned on subsequent calls. The main thread creates a new wrapper each time, and when any previous wrapper is GC'd, the underlying singleton is freed.Reproduction
compile()with warm cache (prover keys loaded viadecodeProverKey)prove()Building Snarkette #1 succeedsprove()Replaces this repo with new-snarkyjs from monorepo #2 hangs indefinitelyWithout cache (fresh compile), both proves succeed.
Working workaround
Replace
FinalizationRegistrywith a no-op before o1js loads:WASM objects are never explicitly freed; memory is reclaimed when the worker context is destroyed.
Reference implementation: zksecurity/mina-guard
ui/lib/disable-wasm-finalizers.ts