@protocol-01/privacy-toolkit
v1.0.1
Published
TypeScript primitives for building privacy protocols on Solana. Merkle trees, Poseidon commitments, nullifiers, and proof format conversion.
Downloads
149
Maintainers
Readme
@protocol-01/privacy-toolkit
Built by Protocol 01 — The Privacy Layer for Solana
TypeScript primitives for building privacy protocols on Solana. Merkle trees, Poseidon commitments, nullifiers, and proof format conversion.
Why This Package
@protocol-01/privacy-toolkit is the foundation layer of the Protocol 01 stack. It provides the core cryptographic building blocks that every higher-level SDK depends on: note commitments, nullifier derivation, Merkle tree operations, and proof format conversion.
If you are building:
- A privacy pool (shielded transactions) -- you need commitments + nullifiers + Merkle proofs
- Confidential balances (zkSPL) -- you need balance commitments + proof formatting
- Any ZK-SNARK application on Solana -- you need proof format conversion for the alt_bn128 precompile
This package has zero Solana dependencies -- it is pure TypeScript + Poseidon and works in Node.js, browsers, and React Native.
Security Notes
These are cryptographic primitives. Incorrect usage can compromise privacy:
- Never reuse secrets or nullifier preimages across different notes. Use
generateSecret()andgenerateNullifierPreimage()for each new note. - Never expose nullifier preimages before spending. The nullifier preimage is the spending authority for a note.
- BN254 field constraint: All field elements must be less than the BN254 scalar field modulus (
21888242871839275222246405745257275088548364400416034343698204186575808495617). Values outside this range will produce incorrect results that may break proof verification. - The
randomFieldElement()function uses rejection sampling fromcrypto.getRandomValues(CSPRNG). There is no insecure fallback.
Install
npm install @protocol-01/privacy-toolkitModules
Merkle Trees
Incremental Merkle tree utilities optimized for on-chain state. The key innovation is computeRootAndProofFromSubtrees, which reads the minimal filledSubtrees array from an on-chain account and computes both the new root and Merkle proof in a single pass -- no local tree synchronization needed.
import {
computeZeroHashes,
getZeroHashes,
computeRootAndProofFromSubtrees,
computeRootFromSubtrees,
} from '@protocol-01/privacy-toolkit';
// Compute zero hashes for a depth-15 tree
const zeros = computeZeroHashes(15);
// Insert a leaf using on-chain filledSubtrees
const { newRoot, updatedSubtrees, pathElements, pathIndices } =
computeRootAndProofFromSubtrees(leaf, leafIndex, filledSubtrees, 15);Poseidon Commitments
Note commitments, nullifiers, and balance commitments using Poseidon hash.
These primitives map directly to the circom circuit templates:
createCommitment->circuits/transfer.circom(NoteCommitment template)computeNullifier->circuits/transfer.circom(NullifierDerivation template)createBalanceCommitment->circuits/confidential_balance.circom(BalanceCommitment template)deriveOwnerPubkey->circuits/poseidon.circom(SpendingKeyDerivation template)
import {
createCommitment,
computeNullifier,
createBalanceCommitment,
deriveOwnerPubkey,
} from '@protocol-01/privacy-toolkit';
// 4-input note commitment
const commitment = createCommitment(nullifierPreimage, secret, epoch, tokenId);
// Nullifier for double-spend prevention
const nullifier = computeNullifier(nullifierPreimage, secret);
// Account-model balance commitment (with nonce binding)
const balanceCommitment = createBalanceCommitment(balance, salt, nonce, owner, mint);Amount Hashes
Link sender and recipient proofs without revealing amounts.
import { createAmountHash, zeroAmountHash } from '@protocol-01/privacy-toolkit';
// Shared between sender and recipient
const hash = createAmountHash(amount, salt);
// For deposit/withdraw (no private transfer)
const zero = zeroAmountHash();Proof Format Conversion
Convert snarkjs Groth16 proofs to the 256-byte format expected by Solana's alt_bn128 pairing precompile.
import {
proofToOnChainBytes,
publicInputsToLE,
publicInputsToBE,
} from '@protocol-01/privacy-toolkit';
// Convert snarkjs proof to on-chain format (handles G2 real/imaginary swap)
const proofBytes = proofToOnChainBytes(snarkjsProof);
// Convert public inputs to little-endian for on-chain storage
const inputsLE = publicInputsToLE(publicSignals);Utilities
BigInt conversion helpers and cryptographic random field element generation.
import {
bigintToLeBytes32,
bigintToBeBytes32,
hexToBigint,
bigintToHex,
leBytesToBigint,
beBytesToBigint,
randomFieldElement,
generateSecret,
generateNullifierPreimage,
} from '@protocol-01/privacy-toolkit';API Reference
Merkle
| Function | Description |
|---|---|
| computeZeroHashes(depth, zeroValue?) | Compute zero hashes for a tree of given depth |
| getZeroHashes(depth?, zeroValue?) | Cached version of computeZeroHashes |
| computeRootAndProofFromSubtrees(leaf, leafIndex, filledSubtrees, depth?, zeroValue?) | Compute new root + Merkle proof from on-chain subtrees |
| computeRootFromSubtrees(filledSubtrees, nextLeafIndex, depth?, zeroValue?) | Compute root only (no proof) |
Commitment
| Function | Description |
|---|---|
| createCommitment(nullifierPreimage, secret, epoch, tokenIdentifier) | 4-input Poseidon note commitment |
| computeNullifier(nullifierPreimage, secret) | 2-input Poseidon nullifier |
| createBalanceCommitment(balance, salt, nonce, ownerPubkey, tokenMint) | Account-model balance commitment with nonce binding |
| deriveOwnerPubkey(spendingKey) | Derive owner pubkey from spending key |
| createAmountHash(amount, salt) | Amount commitment linking sender/recipient |
| zeroAmountHash() | Zero amount hash for deposits/withdrawals |
Proof
| Function | Description |
|---|---|
| proofToOnChainBytes(proof) | Convert snarkjs proof to 256-byte on-chain format |
| publicInputsToLE(inputs) | Convert public inputs to LE byte arrays |
| publicInputsToBE(inputs) | Convert public inputs to BE byte arrays |
Utils
| Function | Description |
|---|---|
| bigintToLeBytes32(n) | BigInt to 32-byte LE array |
| bigintToBeBytes32(n) | BigInt to 32-byte BE Uint8Array |
| hexToBigint(hex) | Hex string to BigInt |
| bigintToHex(n) | BigInt to 64-char hex string |
| leBytesToBigint(bytes) | LE Uint8Array to BigInt |
| beBytesToBigint(bytes) | BE Uint8Array to BigInt |
| randomFieldElement() | Crypto-random BN254 field element |
| generateSecret() | Random secret for commitments |
| generateNullifierPreimage() | Random nullifier preimage |
Input Validation
All public commitment and proof functions validate their inputs at runtime and throw TypeError with a descriptive message if validation fails. For example:
createCommitment(123 as any, 0n, 0n, 0n);
// TypeError: createCommitment: nullifierPreimage must be a bigint, got number
proofToOnChainBytes(null as any);
// TypeError: proofToOnChainBytes: proof must be a SnarkjsProof object with pi_a, pi_b, and pi_c fields
publicInputsToLE([]);
// TypeError: publicInputsToLE: inputs must be a non-empty array of numeric stringsDevelopment
npm install
npm test
npm run buildLicense
MIT
Part of the Protocol 01 Ecosystem
This library is extracted from Protocol 01, the privacy layer for Solana. P01 uses denominated privacy pools with client-side Groth16 proving to provide complete unlinkability on Solana.
| Library | Purpose | |---------|---------| | @protocol-01/privacy-sdk | Full privacy SDK (shield, stealth, streams, vault, MPC) | | @protocol-01/react-native-zk | Client-side ZK proving on mobile | | @protocol-01/solana-verifier | On-chain Groth16 verification | | @protocol-01/privacy-toolkit | Merkle trees, commitments, proof formatting | | @protocol-01/zk-pipeline | End-to-end guide: circuit -> mobile -> on-chain |
