Practical guide to generating ZK-SNARK proofs with @orbinum/proof-generator.
npm install @orbinum/proof-generatorDependencies are installed automatically:
@orbinum/circuits— circuit artifacts (WASM, proving keys)@orbinum/groth16-proofs— arkworks WASM proof generator
import { generateProof, CircuitType } from '@orbinum/proof-generator';
const result = await generateProof(CircuitType.Unshield, {
merkle_root: '12345678...',
nullifier: '98765432...',
amount: '1000000000000000000',
secret: '11223344...',
path_elements: ['0x...', '0x...'],
path_index: '0',
});
console.log(result.proof); // "0xabcd..." (128 bytes, hex)
console.log(result.publicSignals); // ["0x...", "0x...", ...] (hex, LE-encoded)
console.log(result.circuitType); // "unshield"All inputs must be decimal strings or numbers. Public signals are returned as 0x-prefixed 32-byte little-endian hex strings, ready for on-chain submission.
Pass backend: 'arkworks' in the options object. Everything else stays the same:
const result = await generateProof(
CircuitType.Unshield,
{
merkle_root: '12345678...',
// ... same inputs as above
},
{ backend: 'arkworks' }
);The arkworks backend uses .ark proving keys instead of .zkey. Both backends produce identical 128-byte Groth16 proofs — the output format is the same.
See backends.md for a full comparison of speed and artifact size.
| Circuit | CircuitType |
Public signals | Use case |
|---|---|---|---|
| Unshield | CircuitType.Unshield |
7 (v1) / 8 (v2, memo_hash; v3, canonical key) |
Withdraw from pool to public address |
| Transfer | CircuitType.Transfer |
7 (v1) / 8 (v2, memo_hash) / 9 (v3, merkle_roots[2]) |
Private-to-private transfer |
| Shield | CircuitType.Shield |
3 at every version (commitment, value, asset_id) |
Deposit bound to its value and asset |
By default, the library auto-detects the environment:
- Node.js: reads artifacts from
node_modules/@orbinum/circuitson disk. - Browser / Web Worker: fetches the pinned
@orbinum/circuitsrelease from the npm CDN (unpkg).
Both go through the package's manifest.json, which picks each circuit's version
and file names, and verify every artifact's sha256 against it.
import { generateProof, NodeArtifactProvider, CircuitType } from '@orbinum/proof-generator';
const provider = new NodeArtifactProvider({ packageRoot: '/path/to/circuits-package' });
const result = await generateProof(CircuitType.Transfer, inputs, { provider });import { generateProof, WebArtifactProvider, CircuitType } from '@orbinum/proof-generator';
const provider = new WebArtifactProvider({
baseUrl: 'https://my-cdn.example.com/circuits',
});
const result = await generateProof(CircuitType.Transfer, inputs, { provider });Each provider serves the manifest's active_version (v3 for transfer and
unshield since @orbinum/circuits 0.17.1, v1 for shield since 0.16.0) unless a
circuit is pinned:
const provider = new WebArtifactProvider({
circuitVersions: { unshield: 2 }, // v2 unshield artifacts
});generateProof asks the provider which version it serves and checks the proof
against that version's public-signal count. v2 transfer/unshield take an extra
memo_hash input and emit 8 signals; transfer v3 takes two Merkle roots and
emits 9, unshield v3 keeps 8. Passing circuitVersion is optional; if
given, it must match the provider's version, else CircuitVersionMismatchError.
import {
generateProof,
CircuitType,
CircuitNotFoundError,
ProofGenerationError,
InvalidInputsError,
} from '@orbinum/proof-generator';
try {
const result = await generateProof(CircuitType.Unshield, inputs);
} catch (error) {
if (error instanceof InvalidInputsError) {
// A required input field is null, undefined, or missing
console.error('Bad inputs:', error.message);
} else if (error instanceof CircuitNotFoundError) {
// Artifact files could not be loaded (wrong path, network error, etc.)
console.error('Artifacts unavailable:', error.message);
} else if (error instanceof ProofGenerationError) {
// The proof failed to generate (invalid witness, corrupt key, etc.)
console.error('Proof failed:', error.message);
} else {
throw error;
}
}All error classes extend ProofGeneratorError, which exposes a code string alongside the message:
| Class | code |
|---|---|
WitnessCalculationError |
WITNESS_CALCULATION_FAILED |
ProofGenerationError |
PROOF_GENERATION_FAILED |
CircuitNotFoundError |
CIRCUIT_NOT_FOUND |
InvalidInputsError |
INVALID_INPUTS |
Pass verbose: true to log each step to the console — useful for debugging or understanding where time is spent:
const result = await generateProof(CircuitType.Unshield, inputs, { verbose: true });
// [proof-generator] Generating proof for circuit: unshield (backend: snarkjs)
// [proof-generator] Fetching circuit artifacts...
// [proof-generator] Step 1: Generating witness + proof with snarkjs...
// [proof-generator] Proof generated: 0xabcd1234...ef56 (truncated)
// [proof-generator] Public signals: 5
// [proof-generator] Proof generation completed successfully.The first proof call in a process incurs a one-time WASM initialisation cost (~1.5–2s for the arkworks backend). You can pre-warm it explicitly to avoid latency on the first user-facing proof:
import { initWasm } from '@orbinum/proof-generator';
// Call at app startup, before the first proof request
await initWasm();initWasm is idempotent — safe to call multiple times.
await generateProof(
circuitType: CircuitType,
inputs: Record<string, string | number | string[] | number[] | string[][] | number[][]>,
options?: {
backend?: 'snarkjs' | 'arkworks'; // default: 'snarkjs'
provider?: ArtifactProvider; // default: auto-detected
verbose?: boolean; // default: false
}
): Promise<{
proof: string; // 0x-prefixed 128-byte hex
publicSignals: string[]; // 0x-prefixed 32-byte LE hex per signal
circuitType: CircuitType;
}>