Skip to main content

Note Management

Source: ~/workspace/source/src/lib/privacy/note.ts

generateNote

Generate a shielded note for private deposits.
async function generateNote(
  denomination: string,
  vaultId?: string
): Promise<ShieldedNote>
Parameters:
  • denomination: Pool denomination key (e.g., "sentinel_1x", "dn_2x", "stable_50")
  • vaultId: Vault identifier (defaults to denomination)
Returns:
interface ShieldedNote {
  nullifier: string;      // Random 31-byte felt252
  secret: string;         // Random 31-byte felt252
  commitment: string;     // Poseidon(nullifier, secret)
  nullifierHash: string;  // Poseidon(nullifier)
  denomination: string;   // Pool denomination key
  vaultId: string;        // Vault identifier
  leafIndex: number;      // Position in Merkle tree (-1 until deposited)
  batchId: number;        // Batch this deposit belongs to (-1 until deployed)
  batchStart: number;     // First leafIndex in batch (-1 until deployed)
  batchSize: number;      // Deposits in batch (-1 until deployed)
  timestamp: number;      // Note creation time (unix ms)
  spent: boolean;         // True if withdrawn
}
Example:
import { generateNote, addNote } from '@/lib/privacy/note';

// Generate note for 0.0002 BTC deposit
const note = await generateNote('sentinel_1x', 'sentinel');

console.log('Commitment (public):', note.commitment);
console.log('Nullifier (KEEP SECRET):', note.nullifier);
console.log('Secret (KEEP SECRET):', note.secret);

// Save to localStorage
addNote(note);
Security Critical: Never share the nullifier or secret. Anyone with access to these values can withdraw your funds. The commitment is public and safe to share.

poseidonHash2 / poseidonHash1

Compute Poseidon hashes (matches Circom circuit implementation).
async function poseidonHash2(a: string, b: string): Promise<string>
async function poseidonHash1(a: string): Promise<string>
Example:
import { poseidonHash2, poseidonHash1 } from '@/lib/privacy/note';

const nullifier = '0x123...';
const secret = '0x456...';

// Commitment = Poseidon(nullifier, secret)
const commitment = await poseidonHash2(nullifier, secret);

// Nullifier hash = Poseidon(nullifier)
const nullifierHash = await poseidonHash1(nullifier);

Note Storage Functions

Manage notes in localStorage.
// Load all notes
function loadNotes(): ShieldedNote[]

// Save notes
function saveNotes(notes: ShieldedNote[]): void

// Add new note
function addNote(note: ShieldedNote): void

// Update leaf index after deposit
function updateNoteLeafIndex(commitment: string, leafIndex: number): void

// Update batch info after deployment
function updateNoteBatchInfo(
  leafIndex: number,
  batchId: number,
  batchStart: number,
  batchSize: number
): void

// Mark note as spent after withdrawal
function markNoteSpent(nullifierHash: string): void

// Get unspent notes
function getUnspentNotes(denomination?: string): ShieldedNote[]
function getUnspentNotesByVaultId(vaultId: string): ShieldedNote[]
Example:
import { loadNotes, getUnspentNotes, markNoteSpent } from '@/lib/privacy/note';

// Get all unspent notes
const unspent = getUnspentNotes();
console.log(`You have ${unspent.length} unspent notes`);

// Filter by denomination
const sentinel1x = getUnspentNotes('sentinel_1x');

// Mark as spent after withdrawal
markNoteSpent(note.nullifierHash);

Export / Import Notes

Backup and restore notes.
function exportNotes(): string
function importNotes(json: string): void
Example:
import { exportNotes, importNotes } from '@/lib/privacy/note';

// Backup notes
const backup = exportNotes();
localStorage.setItem('note_backup', backup);

// Restore notes
const backup = localStorage.getItem('note_backup');
if (backup) importNotes(backup);

Proof Generation

Source: ~/workspace/source/src/lib/privacy/prover.ts

generateWithdrawalProof

Generate a Groth16 ZK proof for private withdrawal.
async function generateWithdrawalProof(
  note: ShieldedNote,
  merkle: MerkleProof,
  recipient: string,
  relayer: string,
  fee: string,
  batchStart: number,
  batchSize: number
): Promise<ProofResult>
Parameters:
  • note: Shielded note to withdraw (must have batchStart/batchSize set)
  • merkle: Merkle proof for the note’s commitment
  • recipient: Starknet address receiving funds (felt252)
  • relayer: Relayer address (felt252)
  • fee: Relayer fee in WBTC sats (string)
  • batchStart: First leafIndex in batch
  • batchSize: Number of deposits in batch (always 3 for V4)
Returns:
interface ProofResult {
  proof: {
    pi_a: string[];
    pi_b: string[][];
    pi_c: string[];
    protocol: string;
    curve: string;
  };
  publicSignals: string[]; // [root, nullifierHash, recipient, relayer, fee, batchStart, batchSize]
}
Example:
import { generateWithdrawalProof } from '@/lib/privacy/prover';
import { buildTreeFromEvents, getMerkleProof } from '@/lib/privacy/merkle';

// Build Merkle tree from deposit events
const tree = await buildTreeFromEvents(depositEvents);
const merkleProof = getMerkleProof(tree, note.leafIndex);

// Generate proof (takes 10-30 seconds)
const { proof, publicSignals } = await generateWithdrawalProof(
  note,
  merkleProof,
  '0x0123...', // recipient
  '0x0007...', // relayer
  '450',       // 450 sats fee
  note.batchStart,
  note.batchSize
);

console.log('Public signals:', publicSignals);
Circuit Details:
  • Circuit: withdraw_v4.circom
  • WASM: /circuits-groth16-v4/withdraw_v4.wasm
  • Proving key: /circuits-groth16-v4/circuit_final.zkey
  • Verification key: /circuits-groth16-v4/verification_key.json
  • Public inputs: 7 (root, nullifierHash, recipient, relayer, fee, batchStart, batchSize)
  • Curve: BN254
Proof generation runs entirely in the browser using snarkjs. No sensitive data (nullifier, secret) leaves the client.

fetchRelayerFee

Get current relayer fee estimate.
async function fetchRelayerFee(poolAddress?: string): Promise<{
  feeSats: number;
  feeBreakdown: {
    depositGasStrk: number;
    deployBatchGasStrk: number;
    withdrawGasStrk: number;
    totalGasStrk: number;
    totalGasUsd: number;
  };
}>
Example:
import { fetchRelayerFee } from '@/lib/privacy/prover';

const feeInfo = await fetchRelayerFee(poolAddress);
console.log(`Relayer fee: ${feeInfo.feeSats} sats (~$${feeInfo.feeBreakdown.totalGasUsd.toFixed(2)})`);

Merkle Tree Operations

Source: ~/workspace/source/src/lib/privacy/merkle.ts

buildTreeFromEvents

Reconstruct Merkle tree from deposit events.
async function buildTreeFromEvents(
  events: Array<{ commitment: string; leafIndex: number }>
): Promise<MerkleTree>
Example:
import { buildTreeFromEvents, getRoot } from '@/lib/privacy/merkle';
import { useDepositEvents } from '@/hooks/use-shielded-vault';

const { events } = useDepositEvents(poolAddress);
const tree = await buildTreeFromEvents(events);

const root = getRoot(tree);
console.log('Merkle root:', root);

getMerkleProof

Generate Merkle proof for a leaf.
function getMerkleProof(tree: MerkleTree, leafIndex: number): MerkleProof
Returns:
interface MerkleProof {
  pathElements: string[];  // Sibling hashes (hex strings)
  pathIndices: number[];   // 0 = left, 1 = right at each level
  root: string;            // Merkle root (hex string)
}
Example:
import { getMerkleProof } from '@/lib/privacy/merkle';

const proof = getMerkleProof(tree, note.leafIndex);
console.log('Proof has', proof.pathElements.length, 'siblings');
console.log('Root:', proof.root);

Tree Structure

interface MerkleTree {
  depth: number;          // Always 10 for V4
  leaves: bigint[];       // 2^10 = 1024 leaves
  layers: bigint[][];     // layers[0] = leaves, layers[10] = [root]
  zeros: bigint[];        // Pre-computed zero hashes
}
Tree Details:
  • Depth: 10
  • Max leaves: 1,024
  • Hash function: Poseidon (circomlib implementation)
  • Empty leaf: 0

Calldata Serialization

Source: ~/workspace/source/src/lib/privacy/calldata.ts

generateVerifyCalldata

Convert Groth16 proof to Starknet calldata.
async function generateVerifyCalldata(
  proof: Record<string, unknown>,
  publicSignals: string[]
): Promise<string[]>
Example:
import { generateVerifyCalldata } from '@/lib/privacy/calldata';

const { proof, publicSignals } = await generateWithdrawalProof(...);
const calldata = await generateVerifyCalldata(proof, publicSignals);

// Submit to relayer
await fetch('/api/relayer/withdraw', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ poolAddress, calldata })
});
Process:
  1. Load verification key from /circuits-groth16-v4/verification_key.json
  2. Convert snarkjs proof to Garaga format (G1/G2 points on BN254)
  3. Pack proof + VK + hints into felt252 array
  4. Return as hex strings for Starknet calldata
Garaga Integration: Calldata is generated using the Garaga library which provides efficient BN254 operations and Groth16 verification on Starknet.

Security Considerations

Private Data

Never expose or transmit:
  • nullifier - Anyone with this can compute nullifierHash and track withdrawals
  • secret - Combined with nullifier, allows full note control

Public Data

Safe to share:
  • commitment - Public identifier for deposits
  • nullifierHash - Only revealed during withdrawal to prevent double-spend
  • leafIndex, batchStart, batchSize - Public tree metadata

Note Storage

Notes are stored in localStorage:
  • Only accessible by the same origin (domain)
  • Not synced across devices
  • Cleared if browser data is wiped
Backup Recommendation: Use exportNotes() regularly and store backup in a secure location (encrypted file, password manager, hardware wallet).

Proof Generation

  • Runs entirely in browser (no server-side proving)
  • Uses official snarkjs library
  • Circuit WASM and zkey are publicly auditable
  • Takes 10-30 seconds depending on device

Relayer Trust Model

Relayer can:
  • See withdrawal recipient address
  • Censor transactions (refuse to submit)
  • Charge dynamic fees
Relayer cannot:
  • Steal funds (proof validates on-chain)
  • Link deposits to withdrawals (zero-knowledge proof)
  • Spend notes without nullifier+secret
For maximum privacy, wait before withdrawing and withdraw to a fresh address. This breaks timing analysis and on-chain graph clustering.

Build docs developers (and LLMs) love