Unified TypeScript library for ZK and crypto operations with minimal conversions. Features privacy-preserving workflows, Merkle trees, ECDH encryption, and Solana integration.
npm install zk-crypto-helpersUnified TypeScript library for ZK and crypto operations with minimal conversions.
- Bigint-First Architecture: All crypto operations work with bigint natively, minimizing type conversions
- Layered Abstraction: Clean separation between core primitives, domain operations, and high-level workflows
- Type-Safe: Full TypeScript support with comprehensive type definitions
- ZK-Friendly: Optimized for zero-knowledge proof workflows using Poseidon and Baby Jubjub
- Privacy-Preserving Workflows: Complete deposit, withdrawal, and recovery workflows
- Sparse Merkle Trees: Highly optimized sparse Merkle tree implementation with Poseidon hashing, node caching, and
support for depths up to 30 (1B+ leaves)
- ECDH Encryption: Secure key exchange and encryption using Baby Jubjub elliptic curve
- Solana Integration: Native support for Solana PublicKey conversions
- Cross-Platform: Works in both Node.js and browser environments
- Well-Tested: 261 comprehensive tests with full coverage
``bash`
npm install zk-crypto-helpers
Or with yarn:
`bash`
yarn add zk-crypto-helpers
`typescript
import {
generateDeposit,
generateWithdrawal,
MerkleTree,
} from "zk-crypto-helpers";
import { Keypair } from "@solana/web3.js";
// Generate a privacy-preserving deposit
const mintPubkey = Keypair.generate().publicKey;
const deposit = await generateDeposit({
deposit: 1000000n,
mint: mintPubkey,
});
// Save the deposit note securely (needed to withdraw later)
console.log("Commitment:", deposit.commitment);
console.log("Nullifier:", deposit.nullifier);
console.log("Secret:", deposit.secret);
// Create a Merkle tree and add the commitment
const tree = new MerkleTree(20); // 2^20 = ~1M leaves
await tree.insert(deposit.commitment);
// Generate withdrawal proof
const recipientPubkey = Keypair.generate().publicKey;
const withdrawal = await generateWithdrawal({
nullifier: deposit.nullifier,
secret: deposit.secret,
deposit: deposit.deposit,
mint: mintPubkey,
recipient: recipientPubkey,
merkleTree: tree,
commitmentIndex: 0,
});
console.log("Withdrawal proof generated!");
console.log("Root:", withdrawal.root);
console.log("Nullifier hash:", withdrawal.nullifierHash);
`
Create privacy-preserving deposits with optional recovery and compliance encryption:
`typescript
import { generateDeposit, verifyDeposit } from "zk-crypto-helpers";
// Simple deposit
const deposit = await generateDeposit({
deposit: 1000000n,
mint: mintPubkey,
});
// Deposit with recovery mechanism
const senderKeypair = await generateKeypair();
const recoveryKeypair = await generateKeypair();
const depositWithRecovery = await generateDeposit({
deposit: 1000000n,
mint: mintPubkey,
senderPrivKey: senderKeypair.privKey,
recoveryPubKey: recoveryKeypair.pubKey,
});
// Verify deposit
const isValid = await verifyDeposit(deposit);
`
Generate ZK proofs for withdrawals:
`typescript
import {
generateWithdrawal,
verifyWithdrawal,
createWithdrawalCircuitInputs,
validateWithdrawalPossible,
} from "zk-crypto-helpers";
// Validate withdrawal is possible
const usedNullifiers = new Set
const validation = validateWithdrawalPossible(
deposit.commitment,
tree,
deposit.nullifierHash,
usedNullifiers,
);
if (!validation.valid) {
throw new Error(validation.error);
}
// Generate withdrawal
const withdrawal = await generateWithdrawal({
nullifier: deposit.nullifier,
secret: deposit.secret,
deposit: deposit.deposit,
mint: mintPubkey,
recipient: recipientPubkey,
merkleTree: tree,
commitmentIndex: validation.commitmentIndex,
});
// Format for ZK circuit
const circuitInputs = createWithdrawalCircuitInputs(withdrawal);
// Verify withdrawal data
const isValid = await verifyWithdrawal(withdrawal, deposit.commitment);
`
Recover deposit information from encrypted data:
`typescript
import {
recoverDeposit,
verifyRecoveredDeposit,
recoveredToDepositData,
} from "zk-crypto-helpers";
// Recover single deposit
const recovered = await recoverDeposit({
encryptedData: encryptedRecoveryData,
recoveryPrivKey: recoveryKeypair.privKey,
});
if (recovered) {
// Verify recovered data
const isValid = await verifyRecoveredDeposit(recovered);
// Convert to deposit format
const depositData = recoveredToDepositData(recovered);
}
// Batch recovery
const { successful, failed } = await batchRecoverDeposits({
encryptedDataList: [data1, data2, data3],
recoveryPrivKey: recoveryKeypair.privKey,
});
`
Highly optimized sparse Merkle tree implementation with Poseidon hashing:
`typescript
import { MerkleTree } from "zk-crypto-helpers";
// Create tree with 20 levels (2^20 = 1,048,576 max leaves)
// Sparse implementation supports depths up to 30 efficiently
const tree = new MerkleTree(20);
// Insert commitments
await tree.insert(commitment1);
await tree.insert(commitment2);
// Update existing leaf (new feature!)
await tree.update(0, newCommitment);
// Get root
const root = tree.getRoot();
// Generate Merkle proof
const proof = tree.getProof(0);
// Verify proof
const isValid = tree.verify(proof, commitment1, root);
// Batch operations (highly optimized)
await tree.insertBatch([c1, c2, c3]);
// Tree info
console.log("Leaves:", tree.getLeafCount());
console.log("Capacity:", tree.getCapacity());
console.log("Is empty:", tree.isEmpty());
console.log("Is full:", tree.isFull());
// Access individual leaves
const leaf = tree.getLeaf(0); // Returns bigint | undefined
// Get sparse leaf map (efficient for sparse trees)
const sparseLeaves = tree.getSparseLeaves();
// Get statistics
const stats = tree.getStats();
console.log("Cache size:", stats.cacheSize);
console.log("Utilization:", stats.utilization);
`
Secure encryption using Baby Jubjub elliptic curve:
`typescript
import {
ecdhEncrypt,
ecdhDecrypt,
computeSharedSecret,
deriveEncryptionKey,
} from "zk-crypto-helpers";
// Generate keypairs
const alice = await generateKeypair();
const bob = await generateKeypair();
// Encrypt message from Alice to Bob
const nonce = randomFieldElement();
const encrypted = await ecdhEncrypt(message, alice.privKey, bob.pubKey, nonce);
// Bob decrypts the message
const decrypted = await ecdhDecrypt(
encrypted,
bob.privKey,
alice.pubKey,
nonce,
);
// Manual ECDH
const sharedSecret = await computeSharedSecret(alice.privKey, bob.pubKey);
const encryptionKey = await deriveEncryptionKey(sharedSecret, nonce);
`
BN254 field arithmetic and utilities:
`typescript
import {
mod,
modBytes,
isInField,
add,
mul,
sub,
neg,
} from "zk-crypto-helpers";
// Reduce to field element
const fieldElement = mod(someValue);
// Convert bytes to field element
const fromBytes = modBytes(new Uint8Array([1, 2, 3]));
// Field arithmetic
const sum = add(a, b);
const product = mul(a, b);
const difference = sub(a, b);
const negation = neg(a);
// Validation
if (isInField(value)) {
// value is valid
}
assertInField(value, "myValue"); // throws if invalid
`
Poseidon hash functions optimized for ZK circuits:
`typescript
import {
poseidon1,
poseidon2,
poseidon4,
computeCommitment,
computeNullifierHash,
} from "zk-crypto-helpers";
// Hash single value
const hash1 = await poseidon1(value);
// Hash two values (e.g., Merkle tree nodes)
const parentHash = await poseidon2(leftChild, rightChild);
// Hash four values (commitment)
const commitment = await poseidon4(nullifier, secret, deposit, mint);
// Convenience functions
const commitment = await computeCommitment(nullifier, secret, deposit, mintId);
const nullifierHash = await computeNullifierHash(nullifier);
// Hash arrays
const hash = await poseidonMany([val1, val2, val3, val4]);
// Hash bytes
const hash = await poseidonBytes(new Uint8Array([1, 2, 3]));
`
Seamless PublicKey conversions:
`typescript
import {
pubkeyToBigint,
pubkeyToBytes,
pubkeyToHex,
bigintToPubkey,
generateCommitment,
} from "zk-crypto-helpers";
import { PublicKey } from "@solana/web3.js";
// Convert PublicKey to various formats
const bigintValue = pubkeyToBigint(pubkey);
const bytes = pubkeyToBytes(pubkey);
const hex = pubkeyToHex(pubkey, true); // with 0x prefix
// Convert back to PublicKey
const pubkey = bigintToPubkey(bigintValue);
// Generate commitment with Solana PublicKey
const commitment = await generateCommitment(
nullifier,
secret,
deposit,
mintPubkey, // Solana PublicKey
);
`
Comprehensive conversion utilities:
`typescript
import {
// BigInt conversions
bigintToBytes32,
bytesToBigint,
bigintToHex,
hexToBigint,
// Hex conversions
bytesToHex,
hexToBytes,
padHex,
stripHexPrefix,
// Number conversions
numberToBigint,
bigintToNumber,
canConvertToNumber,
} from "zk-crypto-helpers";
// BigInt <-> Bytes
const bytes = bigintToBytes32(value);
const value = bytesToBigint(bytes);
// BigInt <-> Hex
const hex = bigintToHex(value, 32);
const value = hexToBigint(hex);
// Safe number conversions
if (canConvertToNumber(bigValue)) {
const num = bigintToNumber(bigValue);
}
`
#### Deposit Functions
- generateDeposit(options: DepositOptions): Promise - Generate deposit with optional recovery/complianceverifyDeposit(depositData: DepositData): Promise
encryption
- - Verify deposit consistencycreateDepositNote(depositData: DepositData): string
- - Serialize deposit for storageparseDepositNote(note: string, mintPubkey: PublicKey): DepositData
- - Parse deposit note
#### Withdrawal Functions
- generateWithdrawal(input: WithdrawalInput): Promise - Generate withdrawal with Merkle proofverifyWithdrawal(withdrawalData: WithdrawalData, commitment: bigint): Promise
- - Verify withdrawal datacreateWithdrawalCircuitInputs(withdrawalData: WithdrawalData): Record
- - Format forvalidateWithdrawalPossible(commitment, merkleTree, nullifierHash, usedNullifiers)
circuit
- - Validate withdrawalfindCommitmentIndex(commitment: bigint, merkleTree: MerkleTree): number
prerequisites
- - Find commitment in treegenerateBatchWithdrawal(input: BatchWithdrawalInput): Promise
- - Batch withdrawal generation
#### Recovery Functions
- recoverDeposit(options: RecoveryOptions): Promise - Recover deposit from encrypted dataverifyRecoveredDeposit(recovered: RecoveredDepositData): Promise
- - Verify recovered depositrecoveredToDepositData(recovered: RecoveredDepositData): DepositData
- - Convert recovered to deposit formatbatchRecoverDeposits(options: BatchRecoveryOptions): Promise
- - Batch recoverycreateRecoveryNote(encryptedData: EncryptedRecoveryData): string
- - Serialize recovery dataparseRecoveryNote(note: string, mintPubkey: PublicKey): EncryptedRecoveryData
- - Parse recovery note
#### Constants
- BN254_ORDER: The BN254 curve order (field modulus)ZERO
- : Field element zero (0n)ONE
- : Field element one (1n)
#### Functions
- mod(value: bigint): bigint - Reduce value modulo BN254_ORDERmodNumber(value: number): bigint
- - Convert number to field elementmodBytes(bytes: Uint8Array | number[]): bigint
- - Convert bytes to field elementisInField(value: bigint): boolean
- - Check if value is in valid field rangeassertInField(value: bigint, name?: string): void
- - Assert value is in fieldadd(a: bigint, b: bigint): bigint
- - Field additionsub(a: bigint, b: bigint): bigint
- - Field subtractionmul(a: bigint, b: bigint): bigint
- - Field multiplicationneg(a: bigint): bigint
- - Field negationisZero(a: bigint): boolean
- - Check if zeroisOne(a: bigint): boolean
- - Check if one
#### Functions
- initCrypto(): Promise - Initialize crypto librariesrandomFieldElement(): bigint
- - Generate random field elementgenerateKeypair(): Promise
- - Generate Baby Jubjub keypairderivePublicKey(privKey: bigint): Promise
- - Derive public key from private keygetRandomBytes(length?: number): Uint8Array
- - Generate random bytesbytesToHex(bytes: Uint8Array): string
- - Convert bytes to hex stringisBrowser(): boolean
- - Check if running in browserisNode(): boolean
- - Check if running in Node.js
#### Functions
- poseidon1(input: bigint): Promise - Hash single field elementposeidon2(a: bigint, b: bigint): Promise
- - Hash two field elementsposeidon4(a: bigint, b: bigint, c: bigint, d: bigint): Promise
- - Hash four field elementscomputeCommitment(nullifier, secret, deposit, mint): Promise
- - Compute commitment hashcomputeNullifierHash(nullifier: bigint): Promise
- - Compute nullifier hashposeidonMany(inputs: bigint[]): Promise
- - Hash array of field elementsposeidonBytes(bytes: Uint8Array | number[]): Promise
- - Hash bytes
#### Functions
- computeSharedSecret(privKey: bigint, pubKey: Point): Promise - Compute ECDH shared secretderiveEncryptionKey(sharedSecret: bigint, nonce: bigint): Promise
- - Derive encryption keyecdhKeyExchange(privKey: bigint, pubKey: Point, nonce: bigint): Promise
- - Complete ECDH key exchangeverifyECDHSymmetry(keypair1: Keypair, keypair2: Keypair, nonce: bigint): Promise
- - Verify ECDH symmetry
#### Functions
- authenticatedEncrypt(plaintext, encryptionKey, nonce): Promise - Encrypt with authenticationauthenticatedDecrypt(encrypted, encryptionKey, nonce): Promise
- - Decrypt and verifyecdhEncrypt(plaintext, senderPrivKey, recipientPubKey, nonce): Promise
- - ECDH encryptecdhDecrypt(encrypted, recipientPrivKey, senderPubKey, nonce): Promise
- - ECDH decryptencrypt(plaintext, senderPrivKey, recipientPubKey, nonce): Promise
- - Convenience wrapperdecrypt(encrypted, recipientPrivKey, senderPubKey, nonce): Promise
- - Convenience wrapper
#### Class: MerkleTree
`typescript`
constructor(levels: number, zeroValue?: bigint)
#### Methods
- async insert(leaf: bigint): Promise - Insert single leaf at next available positionasync insertBatch(leaves: bigint[]): Promise
- - Insert multiple leaves efficientlyasync update(index: number, element: bigint): Promise
- - Update leaf at specific indexgetRoot(): bigint
- - Get current root (synchronous)async getRootAsync(): Promise
- - Force async root computationgetProof(leafIndex: number): MerkleProof
- - Generate Merkle proof for leafasync verify(proof: MerkleProof, leaf: bigint, root: bigint): Promise
- - Verify Merkle proofgetLeafCount(): number
- - Get number of non-zero leavesgetCapacity(): number
- - Get maximum capacity (2^levels)getLevels(): number
- - Get tree heightisEmpty(): boolean
- - Check if tree has no non-zero leavesisFull(): boolean
- - Check if tree is at maximum capacitygetLeaf(index: number): bigint | undefined
- - Get specific leaf (undefined if not set)getLeaves(): bigint[]
- - Get all leaves from 0 to highest indexgetSparseLeaves(): Map
- - Get only non-zero leaves as sparse mapgetZeroValue(): bigint
- - Get the zero value for empty leavesgetStats(): TreeStats
- - Get tree statistics (levels, capacity, utilization, cache size, etc.)async getNodeAt(level: number, index: number): Promise
- - Get any node in the tree (advanced)
`typescript
// Point on Baby Jubjub curve
interface Point {
x: bigint;
y: bigint;
}
// Keypair
interface Keypair {
privKey: bigint;
pubKey: Point;
}
// Encrypted data with authentication
interface EncryptedData {
ciphertext: bigint;
authTag: bigint;
}
// Merkle proof
interface MerkleProof {
root: bigint;
leaf: bigint;
pathElements: bigint[];
pathIndices: number[];
leafIndex: number;
}
// Deposit data
interface DepositData {
nullifier: bigint;
secret: bigint;
deposit: bigint;
mint: PublicKey;
commitment: bigint;
nullifierHash: bigint;
recoveryEncryption?: {
encryptedNullifier: EncryptedData;
encryptedSecret: EncryptedData;
nonce: bigint;
};
complianceEncryption?: {
encryptedNullifier: EncryptedData;
encryptedSecret: EncryptedData;
nonce: bigint;
};
}
// Withdrawal data
interface WithdrawalData {
nullifier: bigint;
secret: bigint;
pathElements: bigint[];
pathIndices: number[];
root: bigint;
nullifierHash: bigint;
recipient: bigint;
mint: bigint;
deposit: bigint;
leafIndex: number;
}
`
`bash`
npm run build
`bashRun all tests
npm test
$3
`bash
npm run lint
npm run format
`Performance Notes
- Sparse Merkle Trees: The library uses an optimized sparse implementation with:
- Sparse Storage: Only stores non-zero leaves in a Map, dramatically reducing memory usage
- Node Caching: Computed intermediate nodes are cached with selective cache invalidation
- Empty Subtree Detection: Entire empty branches use precomputed zero hashes, skipping unnecessary computation
- Recommended Depths:
- Testing: depth 3-10 (8 to 1,024 leaves)
- Development: depth 15-20 (32K to 1M leaves)
- Production: depth 20-30 (1M to 1B+ leaves) - sparse implementation handles this efficiently!
- Memory Efficiency: O(actual leaves + cached nodes) instead of O(2^levels)
- Async Operations: All cryptographic operations are async. Use
Promise.all() for parallel operations.
- Batch Operations: Always use insertBatch() instead of multiple insert() calls for better performance
- Bigint-First: The library uses bigint throughout to minimize conversions and ensure precision.Examples
See the
/examples directory for complete working examples:-
basic-deposit-withdrawal.ts - Simple deposit and withdrawal flow
- recovery-workflow.ts - Using recovery encryption
- batch-operations.ts - Batch processing examples
- merkle-tree-usage.ts` - Merkle tree operations- [x] Phase 1: Core Foundation (Field, Crypto, Hash)
- [x] Phase 2: ECDH and Encryption
- [x] Phase 3: Merkle Trees
- [x] Phase 4: Circuit Integration
- [x] Phase 5: Type Conversions
- [x] Phase 6: Solana Integration
- [x] Phase 7: High-level Workflows
- [x] Phase 8: Documentation & Examples
MIT
Contributions are welcome! Please feel free to submit a Pull Request.