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 | Withdraw from pool to public address |
| Transfer | CircuitType.Transfer |
7 | Private-to-private transfer |
| ValueProof | CircuitType.ValueProof |
4 | Prove note value and ownership without revealing the spending key |
| PrivateLink | CircuitType.PrivateLink |
2 | Privacy-preserving cross-chain identity |
The ValueProof circuit has a dedicated helper that computes owner_hash = Poseidon(ownerPubkey) and decodes the public signals into named fields:
import { generateValueProof } from '@orbinum/proof-generator';
const result = await generateValueProof(
1000n, // value (bigint, u64)
pubkey, // ownerPubkey (bigint, BN254 scalar)
blinding, // blinding factor (bigint)
1n, // assetId (bigint, u32)
commitment, // note commitment (bigint)
);
console.log(result.proof); // "0x..." (128 bytes)
console.log(result.publicSignals); // 4 hex signals
console.log(result.decoded);
// {
// commitment: "0x...",
// value: "1000", // decimal string
// assetId: 1, // number
// ownerHash: "0x..." // Poseidon(ownerPubkey)
// }You can also use generateProof(CircuitType.ValueProof, inputs) directly if you build the circuit inputs object manually.
By default, the library auto-detects the environment:
- Node.js: reads artifacts from
node_modules/@orbinum/circuitson disk. - Browser / Web Worker: fetches artifacts from the npm CDN (unpkg).
import { generateProof, NodeArtifactProvider, CircuitType } from '@orbinum/proof-generator';
const provider = new NodeArtifactProvider('/path/to/my/artifacts');
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 });The WebArtifactProvider in manifest mode (default, no options) fetches a manifest.json from the CDN to resolve exact versioned artifact filenames. You can pin specific circuits to a version:
const provider = new WebArtifactProvider({
circuitVersions: { unshield: 1 }, // force v1 unshield artifacts
});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;
}>