DOC# WASMNA SLUG wasm_native_proving_sdk_authors_take PRINTED 2026-05-06 03:47 UTC

WASM-native proving for ZK SDKs: an SDK author's take

Why zera-sdk ships native Rust on Node and snarkjs in the browser — and what it would actually cost to ship a WASM-compiled Rust prover for the browser path. A design post about the dual-target build pipeline.

FROM
Dax the Dev <[email protected]>
SOURCE
https://blog.skill-issue.dev/blog/wasm_native_proving_sdk_authors_take/
FILED
2026-05-03 19:00 UTC
REVISED
2026-05-03 19:00 UTC
TIME
9 min read
SERIES
Building the Zera SDK
TAGS
#wasm #sdk #neon #rust #snarkjs #arkworks #zera #zk #phd

The most-asked engineering question on every ZK SDK call I take is some shape of:

“Why are you using snarkjs in the browser when you have a Rust core?”

The honest answer is that we made a decision in March 2026, captured it in RFC 001 under the heading “notes are too sensitive to round-trip through WASM”, and have been quietly re-evaluating it ever since. The dishonest answer is that we shipped what was working. Both answers contain something true. This post is the long version that fits neither into a Twitter thread nor into the RFC.

The shape of the problem is: the Rust core exists, it’s faster than snarkjs, and yet for the browser path we ship snarkjs. Why? And what would it actually cost to swap?

The dual-target shape

Every ZK SDK in 2026 has the same engineering shape, even if its authors don’t admit it:

There is a Rust core that does crypto primitives (Poseidon, Merkle, nullifiers, note construction). The Rust core compiles two ways:

  1. Native, via neon-rs, into a Node.js addon that ships zero-copy across the Buffer ABI.
  2. WebAssembly, via wasm-pack / wasm-bindgen, for browser environments.

There is also a prover — a separate concern from the crypto primitives — that takes a circuit’s R1CS plus a witness plus a zkey and produces a proof. The prover is structurally separate from the core and ships as one of:

  1. snarkjs, a JavaScript prover with a hand-tuned WASM bigint inside it. Browser-native, mature.
  2. arkworks-circom, a Rust prover that consumes the same R1CS and zkey, compiled either native (server) or WASM (browser).

ZERA today ships option 1 of the core via neon-rs (native) and option 1 of the prover (snarkjs) in the browser. The path that doesn’t exist is the Rust prover compiled to WASM. That’s the gap this post is about.

Why we deferred the WASM prover

Three reasons, in honest order of how much each weighed:

1. The marshalling cost of crypto-primitive calls is real

When the SDK computes a Poseidon commitment, it calls zera-core from TypeScript. Through neon-rs, that call is zero-copy: the JS Buffer holding the note bytes is a pointer the Rust side reads directly. Through wasm-bindgen, the same call requires copying the bytes into the WASM linear memory, calling the function, and copying the result back. For a 32-byte input and a 32-byte output that’s tens of microseconds — negligible per call, real when you’re hashing 32 Merkle nodes per proof.

Measured numbers, on a 2024 MacBook Air M3, hashing one BN254 Poseidon node:

PathCostNotes
zera-core via neon-rs~12 µsNative Rust, zero-copy
circomlibjs Poseidon~280 µsPure JS BigInt
zera-core via wasm-bindgen~85 µsMarshalling dominates
zera-core via wasm-bindgen, batched 32~430 µs (= ~13 µs/hash)Marshalling amortises

The batched WASM call is competitive with the native path because the marshalling overhead is paid once per batch and not once per hash. That’s the engineering punch line: WASM-from-Rust is fine if you design the API around batched calls, and bad if you ship a one-call-per-primitive ergonomic API. snarkjs gets this right by accident — its internals are batched because they’re polynomial-time, not constraint-time. A naive port of neon-rs’s API surface to WASM would lose performance vs the native path while also losing performance vs snarkjs, because it would batch neither.

2. The wasm-bindgen-rayon deployment story is fragile

Multi-threaded Rust in the browser depends on the wasm-bindgen-rayon adapter, which depends on SharedArrayBuffer, which depends on the cross-origin isolation headers Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp being served by your CDN. Without those headers, the WASM prover runs single-threaded, at which point it loses to snarkjs because snarkjs is allowed to use Web Workers from JavaScript directly without needing isolation.

That’s not theoretical. Several wallet integration partners we’ve talked to embed our SDK inside an iframe on third-party sites where they don’t control the headers. snarkjs works there. wasm-bindgen-rayon does not. Until the embedding situation improves — Worker threads as a first-class WASM feature, ideally via the wasi-threads proposal — the deployment surface for a Rust-WASM prover is narrower than the deployment surface for snarkjs, even if the prover itself is faster on the supported subset.

3. snarkjs, today, is good enough for the circuits we ship

This is the part the cypherpunk in me hates and the shipping engineer in me has made peace with. The circuits inside zera-sdk — deposit, transfer, withdraw — are in the 5,000–25,000 constraint range. snarkjs proves them in 1–4 seconds in the browser, threads on, IndexedDB-cached zkey. That’s slow enough to need a loading state and fast enough that users don’t bail. (Numbers from Proving in the browser, by the numbers.)

The arkworks-WASM prover would prove the same circuits in 0.5–1.2 seconds — a 3–5× win. That’s a real win and not a transformative one. Transformative would be folding (Nova, SuperNova, ProtoStar) for batch operations, or a small-field STARK migration for the substrate. The marginal-cost calculation said: ship snarkjs, queue arkworks-WASM, prioritise folding for the v2 batch flow.

What the WASM prover would actually cost

Concretely, if I were spec’ing the work:

TaskEstimateRisk
Vendor arkworks-circom and pin to a known-good commit2 daysLow
Build it for the wasm32-unknown-unknown target with wasm-bindgen-rayon3 daysLow
Add COOP/COEP headers to the SDK reference deployment1 dayLow
API parity with the snarkjs path (proof format, zkey loader)5 daysMedium — proof byte-format differences exist
Browser benchmark suite + regression tests5 daysMedium
Iframe-fallback path that auto-degrades to snarkjs without isolation5 daysHigh — this is the actual hard part
Documentation, partner integration guides5 daysMedium
Total~5 person-weeksThe fallback path is the load-bearing risk

The genuinely hard part isn’t compiling the prover. It’s the fallback path. We can’t ship a browser SDK that breaks on every embedded iframe deployment. So the SDK has to detect at runtime whether SharedArrayBuffer is available, and silently fall back to snarkjs if it isn’t. That dual-prover fallback path adds maintenance overhead — two provers, two zkey loaders, two test matrices — that doesn’t exist today.

This is the calculation that keeps coming out the same way: 5 person-weeks for a 3–5× speedup on the supported subset, plus permanent dual-prover maintenance, vs. shipping the same code budget on folding for batch ops or on Solana-side STARK readiness. The folding work has more upside; the STARK work has more strategic value. The WASM prover work has the most concrete win for the current shape of usage.

We’re going to ship the WASM prover. It’s on the v0.5 milestone. But it’s been on a milestone for two quarters now, and the reason it keeps slipping is that every quarter the alternative work has bigger expected value.

The four-way SDK-author tradeoff

Four prover-deployment shapes for a ZK SDK. We're option 1 today; option 2 is the v2; option 3 is a flavour of option 2; option 4 is incompatible with the privacy threat model.
OptionCostLatencyBlast radiusNotes
snarkjs (browser) + native Rust (Node) Two provers; two test matrices; some duplication Fast on Node; acceptable in browser; deploys anywhere snarkjs is mature; native path is mature; the seam between is small What zera-sdk ships today; the pragmatic 2026 default
arkworks-WASM (browser) + native Rust (Node) Single prover codebase; needs COOP/COEP headers in browser 3-5x faster in browser; same on Node Iframe and third-party embedding paths break without isolation Where I'd ship a v2 if I had a quarter to invest
wasm-bindgen-rayon (browser) + native Rust (Node) Same as above; explicit threading surface Same as above Same as above; same isolation requirement Effectively a flavour of the arkworks-WASM choice
OffchainLabs nitro-prover style — proof off-chain server Server-side proving; client just submits Variable; depends on server capacity Centralisation surface; server compromise = wallet compromise (for transactions) Wrong threat model for a privacy SDK; right for some L2 use cases

What changed my mind in 2026

Two things, both external:

Header support went mainstream. Every major hosting provider — Vercel, Netlify, Cloudflare Pages — now ships COOP/COEP header configuration as a first-class feature. In 2024 you had to write custom worker code to inject the headers; in 2026 it’s a checkbox. That moves the “fallback path complexity” from load-bearing to secondary risk.

The Mopro / zkMopro project published clean comparison numbers. Their Circom prover comparison gives a third-party benchmark that I can point partners at when I’m justifying a 3–5× speedup. Internal benchmarks are also useful, but the question changes when there’s external corroboration.

The combination of these two means the case for shipping the WASM prover is meaningfully stronger in mid-2026 than it was in early 2026. I’d put 70% confidence that we ship arkworks-WASM in the browser path before the end of 2026, and that the snarkjs fallback survives as a secondary path indefinitely.

A note on the bigger architectural question

The deeper question — the one I think about more often than I write about — is whether the crypto-primitives core and the prover should even be the same artefact. They’re not in zera-sdk: zera-core is the primitives, snarkjs is the prover, they don’t share code. That separation has been quietly excellent for shipping velocity.

What we don’t do is share the same separation in our partner SDKs. Several integrators have asked: “can I use zera-core for the primitives but a different prover for my circuit?” The answer today is yes, with caveats — the witness format has to match, the zkey has to be Groth16-over-BN254, and the on-chain verifier has to accept the resulting proof. In practice nobody has done this yet. But the architectural shape supports it, and if a partner wanted to ship a Halo2 verifier on Solana (when that’s possible), they could keep using zera-core’s primitives and swap the prover wholesale.

This is the right shape, in retrospect. The crypto core is small, well-tested, audited. The prover is big, fast-moving, swappable. Conflating them — as some early SDKs do — bakes the prover choice into every wallet integration, and makes the prover migration ZERA is currently considering much more painful than it needs to be.

What I’d ship differently for v0.5

Three concrete deliverables, in order of how much they’d actually move the needle:

  1. @zera-labs/sdk-prover-wasm — a separate npm package containing arkworks-circom compiled to WASM with wasm-bindgen-rayon. Opt-in via a constructor flag; falls back to snarkjs on unsupported platforms. This is the work I described above; the new shape is to ship it as a separate package so existing integrators don’t pull a 4 MB WASM blob unless they want it.
  2. MCP-side prover-selection tool. The @zera-labs/mcp-server currently uses snarkjs unconditionally. An MCP-level configuration for “prefer the fastest available prover” would let agents tune for batch operations vs. one-shot transactions. More upside than it sounds.
  3. A shared zkey-loader abstraction. Today the SDK reads zkeys from URLs; the MCP server reads them from disk; the test harness reads them from a fixtures directory. A ZkeyLoader trait — backed by URL, IndexedDB, fs, or arbitrary user code — would unify the three paths and unblock a “user provides their own zkey” advanced flow that several research partners have asked for.

Further reading

← Back to article