Skip to main content

Overview

Stealth Pool Note creation and burning require client-side generation of Groth16 zero-knowledge proofs. The SDK does not bundle a default prover — you provide one as a dependency injection. The prover ships inside @umbra-privacy/sdk/zk-prover. The standalone @umbra-privacy/web-zk-prover package has been deleted. All factories listed below wrap snarkjs and handle proving-key loading from Umbra’s CDN by default.

Quick start

Per-circuit factories are exported from @umbra-privacy/sdk/zk-prover. Pair each one with the matching high-level factory in /deposit or /burn.
import {
  getETAIntoStealthPoolNoteCreatorProver,
  getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver,
} from "@umbra-privacy/sdk/zk-prover";
import { getETAIntoReceiverBurnableStealthPoolNoteCreatorFunction } from "@umbra-privacy/sdk/deposit";
import { getReceiverBurnableStealthPoolNoteIntoETABurnerFunction } from "@umbra-privacy/sdk/burn";
import { getUmbraRelayer } from "@umbra-privacy/sdk";

// 1. Create the provers (uses CDN asset provider by default).
const createProver = getETAIntoStealthPoolNoteCreatorProver();
const burnProver   = getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver();

// 2. Pass them to the SDK factories.
const createNote = getETAIntoReceiverBurnableStealthPoolNoteCreatorFunction(
  { client },
  { zkProver: createProver },
);

const r = getUmbraRelayer({ apiEndpoint: "https://relayer.api.umbraprivacy.com" });
const burn = getReceiverBurnableStealthPoolNoteIntoETABurnerFunction(
  { client },
  {
    fetchBatchMerkleProof: client.fetchBatchMerkleProof!,
    zkProver: burnProver,
    relayer: {
      submitBurn:        r.submitClaim,
      pollBurnStatus:    r.pollClaimStatus,
      getRelayerAddress: r.getRelayerAddress,
    },
  },
);

Available factory functions

All factories accept an optional provider parameter of type IZkAssetProvider. When omitted, the default CDN provider is used. Creator provers (renamed in V5):
  • getETAIntoStealthPoolNoteCreatorProver(provider?)IZkProverForETAIntoStealthPoolNote. Shared by both self-burnable and receiver-burnable ETA-source creators.
  • getATAIntoStealthPoolNoteCreatorProver(provider?)IZkProverForATAIntoStealthPoolNote. Shared by both self-burnable and receiver-burnable ATA-source creators.
Burner provers:
  • getClaimSelfClaimableUtxoIntoEncryptedBalanceProver(provider?)IZkProverForClaimSelfClaimableUtxoIntoEncryptedBalance.
  • getClaimReceiverClaimableUtxoIntoEncryptedBalanceProver(provider?)IZkProverForClaimReceiverClaimableUtxoIntoEncryptedBalance.
  • getClaimSelfClaimableUtxoIntoPublicBalanceProver(provider?)IZkProverForClaimSelfClaimableUtxoIntoPublicBalance.
Registration:
  • getUserRegistrationProver(provider?)IZkProverForUserRegistration.
The burner prover factory names use the getClaim…ClaimableUtxo… spelling, while the creator prover factories use the Stealth Pool Note vocabulary (getETAIntoStealthPoolNoteCreatorProver, getATAIntoStealthPoolNoteCreatorProver). Both sets coexist — the naming difference is by design to match the high-level factory vocabulary.

Custom asset provider

By default, all prover factories use getCdnZkAssetProvider() to fetch proving keys from Umbra’s CDN — no configuration required. For advanced use cases, customise the asset provider.

Custom CDN URL

To load assets from a different CDN or self-hosted location, use getCdnZkAssetProvider with a custom baseUrl:
import { getCdnZkAssetProvider } from "@umbra-privacy/sdk/zk-prover/cdn";
import { getUserRegistrationProver } from "@umbra-privacy/sdk/zk-prover";

const myProvider = getCdnZkAssetProvider({
  baseUrl: "https://my-cdn.example.com/zk-assets",
});

const prover = getUserRegistrationProver(myProvider);

Fully custom provider

Implement the IZkAssetProvider interface directly:
import type {
  IZkAssetProvider,
  ZkAssetUrls,
  ZKeyType,
  ClaimVariant,
} from "@umbra-privacy/sdk/zk-prover";
import { getUserRegistrationProver } from "@umbra-privacy/sdk/zk-prover";

function createMyAssetProvider(storageBaseUrl: string): IZkAssetProvider {
  return {
    async getAssetUrls(type: ZKeyType, variant?: ClaimVariant): Promise<ZkAssetUrls> {
      const name = variant ? `${type}-${variant}` : type;
      return {
        zkeyUrl: `${storageBaseUrl}/${name}.zkey`,
        wasmUrl: `${storageBaseUrl}/${name}.wasm`,
      };
    },
  };
}

const myProvider = createMyAssetProvider("https://my-storage.example.com/zk-assets");
const prover = getUserRegistrationProver(myProvider);

The ZK prover interface

The prover-interface and result types ship at @umbra-privacy/sdk/zk-prover. See ZK Prover Interfaces for the full list. All provers return the same proof shape:
interface Groth16Proof {
  a: Groth16ProofA; // G1 point — 64 bytes (x, y uncompressed)
  b: Groth16ProofB; // G2 point — 128 bytes
  c: Groth16ProofC; // G1 point — 64 bytes
}

Web Worker pattern

Proof generation is CPU-intensive — generating a Groth16 proof takes 2-8 seconds in the browser and 1-3 seconds in Node.js. In browser applications, run the prover in a Web Worker to avoid blocking the main thread:
// prover-worker.ts
import { expose } from "comlink";
import { getETAIntoStealthPoolNoteCreatorProver } from "@umbra-privacy/sdk/zk-prover";

const prover = getETAIntoStealthPoolNoteCreatorProver();

expose(prover);
// main.ts
import { wrap } from "comlink";
import type { IZkProverForETAIntoStealthPoolNote } from "@umbra-privacy/sdk/zk-prover";

const worker   = new Worker(new URL("./prover-worker.ts", import.meta.url));
const zkProver = wrap<IZkProverForETAIntoStealthPoolNote>(worker);

const createNote = getATAIntoReceiverBurnableStealthPoolNoteCreatorFunction(
  { client },
  { zkProver },
);
comlink is a convenient library for wrapping Web Workers with a promise-based RPC interface. Any similar mechanism works.

Custom prover implementation

If the bundled prover does not fit your environment, you can implement the prover interface directly:
import type { IZkProverForETAIntoStealthPoolNote, ETAIntoStealthPoolNoteCircuitInputs } from "@umbra-privacy/sdk/zk-prover";
import type { Groth16ProofA, Groth16ProofB, Groth16ProofC } from "@umbra-privacy/sdk/types";

function createRemoteZkProver(endpoint: string): IZkProverForETAIntoStealthPoolNote {
  return {
    prove: async (inputs: ETAIntoStealthPoolNoteCircuitInputs): Promise<{
      a: Groth16ProofA;
      b: Groth16ProofB;
      c: Groth16ProofC;
    }> => {
      const response = await fetch(`${endpoint}/prove`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(inputs),
      });
      if (!response.ok) throw new Error(`Proving server returned ${response.status}`);
      return response.json();
    },
  };
}
A remote prover receives the circuit inputs, which include private data such as the note amount and recipient. Only use a remote prover if you fully trust the operator of the proving service.

Proof generation timing

Proof generation time varies by device and prover implementation:
  • WebAssembly in browser: 2–8 seconds.
  • Native binary (Node.js): 1–3 seconds.
  • Remote proving service: depends on network latency and server capacity.
Show a loading indicator while proof generation is in progress.

Circuit details

The Umbra circuits are written in Circom. The self-burnable Stealth Pool Note circuit proves:
  • Knowledge of the secret inputs (amount, recipient, nonce, blinding_factor) whose Poseidon hash equals a commitment in the Merkle tree.
  • A valid Merkle inclusion proof from that commitment to the current tree root.
  • The nullifier is correctly computed as Poseidon(poseidon_private_key, commitment).
  • The destination address matches the one embedded in the commitment.
All four facts are proven simultaneously in a single Groth16 proof, which is then verified on-chain in a single transaction.