Experimental — This project is a proof of concept and has not been thoroughly tested. Use at your own risk.
WebAssembly build of Midnight's zkir keygen — generate ZK prover/verifier keys from ZKIR circuit descriptions in the browser or Node.js.
The pkg/ directory contains ready-to-use WASM files:
pkg/
midnight_zkir_keygen_wasm_bg.wasm (2.4 MB) — compiled WASM binary
midnight_zkir_keygen_wasm.js (28 KB) — JS glue code
midnight_zkir_keygen_wasm.d.ts (6 KB) — TypeScript definitions
package.json
Install directly from the package:
npm install ./pkgimport init, {
keygenFromJson,
getCircuitKFromJson,
jsonIrToBinary,
} from 'midnight-zkir-keygen-wasm';
// Initialize the WASM module
await init();
// Read k value from a ZKIR circuit
const k = getCircuitKFromJson(zkirJson);
// Create a params provider (SRS trusted setup data)
const provider = {
async getParams(k) {
const resp = await fetch(`/srs/bls_midnight_2p${k}`);
return new Uint8Array(await resp.arrayBuffer());
}
};
// Generate prover + verifier keys
const result = await keygenFromJson(zkirJson, provider);
console.log(result.proverKey); // Uint8Array
console.log(result.verifierKey); // Uint8Array
result.free(); // release WASM memory
// Convert JSON ZKIR to binary format (.bzkir)
const bzkir = jsonIrToBinary(zkirJson);const wasm = require('./pkg-node/midnight_zkir_keygen_wasm.js');
wasm.init();
const result = await wasm.keygenFromJson(zkirJson, provider);Or use the CLI wrapper:
node keygen-cli.mjs <contract-output-dir>
# Reads: <dir>/zkir/*.zkir
# Writes: <dir>/keys/*.prover, <dir>/keys/*.verifier| Function | Description |
|---|---|
init() |
Initialize panic hook (call once) |
keygen(zkirBytes, provider) |
Generate keys from binary .bzkir data |
keygenFromJson(json, provider) |
Generate keys from JSON .zkir string |
keygenMany(entries, provider, progress?) |
Batch keygen for multiple circuits |
getCircuitK(zkirBytes) |
Get circuit size parameter k from binary |
getCircuitKFromJson(json) |
Get circuit size parameter k from JSON |
jsonIrToBinary(json) |
Convert .zkir JSON to .bzkir binary |
interface ParamsProvider {
getParams(k: number): Promise<Uint8Array>;
}The provider fetches SRS (Structured Reference String) parameters for a given k value. Each circuit requires the file bls_midnight_2p{k} from Midnight's S3 bucket:
https://midnight-s3-fileshare-dev-eu-west-1.s3.eu-west-1.amazonaws.com/bls_midnight_2p{k}
interface ProgressCallback {
onProgress(name: string, current: number, total: number): void;
}- Rust (stable)
- wasm-pack
- Homebrew LLVM (macOS) — required because the
blstcrate compiles C/assembly for BLS12-381, and Apple's clang cannot targetwasm32-unknown-unknown - Midnight ledger crates (path dependencies in
Cargo.toml)
brew install llvm
rustup target add wasm32-unknown-unknown
cargo install wasm-packThe zkir crate depends on blst (BLS12-381 cryptography), which compiles C and assembly code. Apple's bundled clang does not support the wasm32-unknown-unknown target. Homebrew LLVM ships with full WASM target support.
You must set these environment variables before building:
export CC=/opt/homebrew/opt/llvm/bin/clang
export AR=/opt/homebrew/opt/llvm/bin/llvm-ar./build.shOr manually:
export CC=/opt/homebrew/opt/llvm/bin/clang
export AR=/opt/homebrew/opt/llvm/bin/llvm-ar
# Browser target (ES module, manual init)
wasm-pack build --target web --out-dir pkg
# Node.js target (CommonJS, auto-init)
wasm-pack build --target nodejs --out-dir pkg-node./test.shOr manually:
export CC=/opt/homebrew/opt/llvm/bin/clang
export AR=/opt/homebrew/opt/llvm/bin/llvm-ar
wasm-pack test --node[lib]
crate-type = ["cdylib", "rlib"]cdylib— required for wasm-pack to produce a.wasmbinary
wasm-bindgen = "=0.2.104"- Must be pinned exactly to match the installed
wasm-bindgen-cliversion. A mismatch causes build failures.
getrandom = { version = "^0.2.8", features = ["js"] }- Without the
"js"feature, any randomness call panics in WASM — there's no/dev/urandomin browsers. This feature wiresgetrandomtocrypto.getRandomValues().
[target.wasm32-unknown-unknown]
rustflags = ["-C", "target-feature=+reference-types"]Enables WASM reference types for better wasm-opt optimization and smaller binaries.
The crate depends on four Midnight ledger crates via local path:
| Crate | Package | What It Provides |
|---|---|---|
zkir |
midnight-zkir |
IrSource, keygen(), circuit parsing |
serialize |
midnight-serialize |
tagged_serialize / tagged_deserialize |
transient-crypto |
midnight-transient-crypto |
ParamsProverProvider trait, SRS types |
base-crypto |
midnight-base-crypto |
BLS12-381 primitives (via blst) |
To build from source, you need the midnight-ledger repository checked out at the path specified in Cargo.toml.
No modifications were made to the midnight-ledger crates. This project is a thin wrapper crate that imports the existing zkir, serialize, transient-crypto, and base-crypto crates as-is via path dependencies and re-exports their functionality through wasm-bindgen.
What was written from scratch for this crate:
| File | Purpose |
|---|---|
src/lib.rs |
#[wasm_bindgen] exports that call into the unmodified zkir crate (IrSource::keygen(), tagged_serialize, etc.) |
src/provider.rs |
JsParamsProvider — bridges a JavaScript { getParams(k): Promise<Uint8Array> } object to the Rust ParamsProverProvider trait |
The native zkir CLI does:
read .zkir file -> deserialize IrSource -> ir.keygen(¶ms_provider) -> serialize keys
The WASM version does the same, but crosses the JS/WASM boundary:
JS Uint8Array/string -> deserialize IrSource -> ir.keygen(&JsParamsProvider) -> Uint8Array back to JS
The core challenge: ir.keygen() expects a Rust trait (ParamsProverProvider) returning impl Future, but the SRS parameters come from JavaScript (fetched via fetch()).
JsParamsProvider bridges this by wrapping a JS object and converting Promises to Rust Futures:
pub struct JsParamsProvider {
inner: JsValue, // JS object with getParams(k): Promise<Uint8Array>
}
impl ParamsProverProvider for JsParamsProvider {
fn get_params(&self, k: u8) -> impl Future<Output = Result<impl AsRef<[u8]>, ...>> {
async move {
let func = Reflect::get(&self.inner, &"getParams".into())?;
let promise = func.call1(&self.inner, &JsValue::from(k))?;
let result = JsFuture::from(promise).await?;
let array: Uint8Array = result.dyn_into()?;
Ok(array.to_vec())
}
}
}Keygen requires Structured Reference String (SRS) parameters — precomputed trusted setup data. Each circuit has a k value (size parameter) determining which file is needed: bls_midnight_2p{k}.
| Source | Location |
|---|---|
| Environment variable | $MIDNIGHT_PP directory |
| Local cache | ~/.cache/midnight/zk-params/ |
| S3 (remote) | https://midnight-s3-fileshare-dev-eu-west-1.s3.eu-west-1.amazonaws.com/bls_midnight_2p{k} |
In browsers, S3 doesn't serve CORS headers, so you need a proxy (the webapp uses a dev server proxy or Cloudflare Pages Function).
| Problem | Cause | Fix |
|---|---|---|
error: linker cc failed on wasm32 |
Apple clang can't target WASM | export CC=/opt/homebrew/opt/llvm/bin/clang |
unreachable panic in WASM |
Missing getrandom "js" feature |
Add getrandom = { features = ["js"] } to Cargo.toml |
wasm-bindgen version mismatch |
CLI and crate versions differ | Pin wasm-bindgen = "=0.2.104" exactly |
Failed to fetch SRS params in browser |
CORS blocks cross-origin S3 | Proxy requests through your server |
zkir-wasm/
├── Cargo.toml # Rust crate config
├── build.sh # WASM build script
├── test.sh # Test runner
├── src/
│ ├── lib.rs # WASM exports (keygen, getCircuitK, etc.)
│ └── provider.rs # JS<->Rust async bridge for SRS params
├── tests/
│ ├── web.rs # wasm-bindgen-test suite
│ └── fixtures/basic.zkir # Test circuit
├── pkg/ # wasm-pack output (web target) — distributable
├── pkg-node/ # wasm-pack output (node target)
├── keygen-cli.mjs # Node.js CLI for keygen
├── zkir-keygen.d.ts # TypeScript definitions
└── webapp/ # Demo app (Compact compiler + keygen)
├── wrangler.jsonc # Cloudflare Pages deploy config (→ compact-wasm.pages.dev)
├── webpack.config.js
├── src/
│ ├── index.js # UI + orchestration
│ ├── compiler.js # Compact compiler WASM wrapper
│ └── keygen.js # Keygen WASM wrapper
└── functions/srs/ # Cloudflare Pages S3 proxy