zk-crypto-helpers
v0.1.2
Published
Unified TypeScript library for ZK and crypto operations with minimal conversions. Features privacy-preserving workflows, Merkle trees, ECDH encryption, and Solana integration.
Maintainers
Readme
zk-crypto-helpers
Unified TypeScript library for ZK and crypto operations with minimal conversions.
Features
- Bigint-First Architecture: All crypto operations work with
bigintnatively, 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
Installation
npm install zk-crypto-helpersOr with yarn:
yarn add zk-crypto-helpersQuick Start
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);High-Level Workflows
Deposit Workflow
Create privacy-preserving deposits with optional recovery and compliance encryption:
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);Withdrawal Workflow
Generate ZK proofs for withdrawals:
import {
generateWithdrawal,
verifyWithdrawal,
createWithdrawalCircuitInputs,
validateWithdrawalPossible,
} from "zk-crypto-helpers";
// Validate withdrawal is possible
const usedNullifiers = new Set<bigint>();
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);Recovery Workflow
Recover deposit information from encrypted data:
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,
});Core Modules
Merkle Trees
Highly optimized sparse Merkle tree implementation with Poseidon hashing:
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);ECDH and Encryption
Secure encryption using Baby Jubjub elliptic curve:
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);Field Operations
BN254 field arithmetic and utilities:
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 invalidHashing
Poseidon hash functions optimized for ZK circuits:
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]));Solana Integration
Seamless PublicKey conversions:
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
);Type Conversions
Comprehensive conversion utilities:
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);
}API Reference
Workflow Module
Deposit Functions
generateDeposit(options: DepositOptions): Promise<DepositData>- Generate deposit with optional recovery/compliance encryptionverifyDeposit(depositData: DepositData): Promise<boolean>- Verify deposit consistencycreateDepositNote(depositData: DepositData): string- Serialize deposit for storageparseDepositNote(note: string, mintPubkey: PublicKey): DepositData- Parse deposit note
Withdrawal Functions
generateWithdrawal(input: WithdrawalInput): Promise<WithdrawalData>- Generate withdrawal with Merkle proofverifyWithdrawal(withdrawalData: WithdrawalData, commitment: bigint): Promise<boolean>- Verify withdrawal datacreateWithdrawalCircuitInputs(withdrawalData: WithdrawalData): Record<string, string | string[]>- Format for circuitvalidateWithdrawalPossible(commitment, merkleTree, nullifierHash, usedNullifiers)- Validate withdrawal prerequisitesfindCommitmentIndex(commitment: bigint, merkleTree: MerkleTree): number- Find commitment in treegenerateBatchWithdrawal(input: BatchWithdrawalInput): Promise<WithdrawalData[]>- Batch withdrawal generation
Recovery Functions
recoverDeposit(options: RecoveryOptions): Promise<RecoveredDepositData | null>- Recover deposit from encrypted dataverifyRecoveredDeposit(recovered: RecoveredDepositData): Promise<boolean>- Verify recovered depositrecoveredToDepositData(recovered: RecoveredDepositData): DepositData- Convert recovered to deposit formatbatchRecoverDeposits(options: BatchRecoveryOptions): Promise<BatchRecoveryResult>- Batch recoverycreateRecoveryNote(encryptedData: EncryptedRecoveryData): string- Serialize recovery dataparseRecoveryNote(note: string, mintPubkey: PublicKey): EncryptedRecoveryData- Parse recovery note
Field Module
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
Crypto Module
Functions
initCrypto(): Promise<CryptoContext>- Initialize crypto librariesrandomFieldElement(): bigint- Generate random field elementgenerateKeypair(): Promise<Keypair>- Generate Baby Jubjub keypairderivePublicKey(privKey: bigint): Promise<Point>- 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
Hash Module
Functions
poseidon1(input: bigint): Promise<bigint>- Hash single field elementposeidon2(a: bigint, b: bigint): Promise<bigint>- Hash two field elementsposeidon4(a: bigint, b: bigint, c: bigint, d: bigint): Promise<bigint>- Hash four field elementscomputeCommitment(nullifier, secret, deposit, mint): Promise<bigint>- Compute commitment hashcomputeNullifierHash(nullifier: bigint): Promise<bigint>- Compute nullifier hashposeidonMany(inputs: bigint[]): Promise<bigint>- Hash array of field elementsposeidonBytes(bytes: Uint8Array | number[]): Promise<bigint>- Hash bytes
ECDH Module
Functions
computeSharedSecret(privKey: bigint, pubKey: Point): Promise<bigint>- Compute ECDH shared secretderiveEncryptionKey(sharedSecret: bigint, nonce: bigint): Promise<bigint>- Derive encryption keyecdhKeyExchange(privKey: bigint, pubKey: Point, nonce: bigint): Promise<bigint>- Complete ECDH key exchangeverifyECDHSymmetry(keypair1: Keypair, keypair2: Keypair, nonce: bigint): Promise<boolean>- Verify ECDH symmetry
Encryption Module
Functions
authenticatedEncrypt(plaintext, encryptionKey, nonce): Promise<EncryptedData>- Encrypt with authenticationauthenticatedDecrypt(encrypted, encryptionKey, nonce): Promise<bigint | null>- Decrypt and verifyecdhEncrypt(plaintext, senderPrivKey, recipientPubKey, nonce): Promise<EncryptedData>- ECDH encryptecdhDecrypt(encrypted, recipientPrivKey, senderPubKey, nonce): Promise<bigint | null>- ECDH decryptencrypt(plaintext, senderPrivKey, recipientPubKey, nonce): Promise<EncryptedData>- Convenience wrapperdecrypt(encrypted, recipientPrivKey, senderPubKey, nonce): Promise<bigint | null>- Convenience wrapper
Merkle Tree
Class: MerkleTree
constructor(levels: number, zeroValue?: bigint)Methods
async insert(leaf: bigint): Promise<void>- Insert single leaf at next available positionasync insertBatch(leaves: bigint[]): Promise<void>- Insert multiple leaves efficientlyasync update(index: number, element: bigint): Promise<void>- Update leaf at specific indexgetRoot(): bigint- Get current root (synchronous)async getRootAsync(): Promise<bigint>- Force async root computationgetProof(leafIndex: number): MerkleProof- Generate Merkle proof for leafasync verify(proof: MerkleProof, leaf: bigint, root: bigint): Promise<boolean>- 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<number, bigint>- 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<bigint>- Get any node in the tree (advanced)
Types
// 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;
}Development
Building
npm run buildTesting
# Run all tests
npm test
# Run with coverage
npm run test:coverage
# Watch mode
npm run test:watchLinting and Formatting
npm run lint
npm run formatPerformance 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 multipleinsert()calls for better performance - Bigint-First: The library uses
bigintthroughout to minimize conversions and ensure precision.
Examples
See the /examples directory for complete working examples:
basic-deposit-withdrawal.ts- Simple deposit and withdrawal flowrecovery-workflow.ts- Using recovery encryptionbatch-operations.ts- Batch processing examplesmerkle-tree-usage.ts- Merkle tree operations
Roadmap
- [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
License
MIT
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
