@protocol-01/zk-sdk
v0.2.1
Published
Zero-knowledge SDK for Protocol 01 shielded transactions
Maintainers
Readme
@protocol-01/zk-sdk
Zero-knowledge privacy SDK for Solana. Shield, transfer, and unshield tokens with mathematical privacy guarantees using Groth16 proofs and Poseidon hashing.
Install
npm install @protocol-01/zk-sdk @solana/web3.jsRequires Node.js >= 22.0.0 and @solana/web3.js ^1.98.0 as a peer dependency.
Quick Start
import { ShieldedClient } from '@protocol-01/zk-sdk';
import { Connection } from '@solana/web3.js';
// Create and initialize the client
const client = new ShieldedClient({
connection: new Connection('https://api.devnet.solana.com'),
wallet: yourAnchorWallet,
network: 'devnet',
wasmPath: './circuits/transfer.wasm',
zkeyPath: './circuits/transfer_final.zkey',
});
await client.initialize('your seed phrase');
// Get your ZK address to receive shielded payments
const zkAddr = client.getZkAddress();
console.log('Share this address:', zkAddr.encoded); // "zk:base64..."
// Shield 0.1 SOL into the shielded pool
const shieldTx = await client.shield(100_000_000n);
// Transfer privately within the pool
const recipient = ShieldedClient.decodeZkAddress('zk:...');
const transferTx = await client.transfer(recipient, 50_000_000n);
// Unshield back to a public address
const unshieldTx = await client.unshield(recipientPublicKey, 50_000_000n);Configuration
Basic (devnet)
const client = new ShieldedClient({
connection: new Connection('https://api.devnet.solana.com'),
wallet: anchorWallet,
// Defaults: network='devnet', uses the deployed devnet program ID
});Custom program ID
const client = new ShieldedClient({
connection,
wallet,
programId: 'YourCustomProgramId11111111111111111111111111',
});Hosted circuit files
const client = new ShieldedClient({
connection,
wallet,
circuitBaseUrl: 'https://cdn.example.com/circuits/v1',
// Prover will load transfer.wasm and transfer_final.zkey from this URL
});Network selection
import { getProgramId } from '@protocol-01/zk-sdk';
getProgramId('devnet'); // 'GbVM5yvetrSD194Hnn1BXnR56F8ZWNKnij7DoVP9j27c'
getProgramId('localnet'); // 'GbVM5yvetrSD194Hnn1BXnR56F8ZWNKnij7DoVP9j27c'
getProgramId('mainnet-beta'); // throws: not yet deployedAPI Reference
Exports from @protocol-01/zk-sdk
| Export | Type | Description |
|---|---|---|
| ShieldedClient | class | High-level client for shield/transfer/unshield operations |
| ShieldedClientConfig | type | Configuration interface for ShieldedClient |
| Note | class | Represents a shielded UTXO note |
| EncryptedNote | class | Encrypted note for storage/transmission |
| createNote | function | Create a new shielded note with Poseidon commitment |
| encryptNote | function | Encrypt a note using NaCl authenticated encryption |
| decryptNote | function | Decrypt an encrypted note |
| MerkleTree | class | Sparse Merkle tree with Poseidon hashing |
| generateMerkleProof | function | Generate a Merkle inclusion proof |
| verifyMerkleProof | function | Verify a Merkle proof against a root |
| ZkProver | class | Groth16 proof generation engine |
| generateProof | function | Convenience wrapper for proof generation |
| poseidonHash | function | Poseidon hash over field elements |
| computeCommitment | function | Compute Poseidon(amount, owner, randomness, mint) |
| computeNullifier | function | Compute Poseidon(commitment, spendingKeyHash) |
| deriveOwnerPubkey | function | Derive owner public key from spending key |
| computeSpendingKeyHash | function | Derive spending key hash for nullifiers |
| FIELD_MODULUS | constant | BN254 field modulus |
| ZK_SHIELDED_PROGRAM_ID | constant | Default devnet program ID |
| PROGRAM_IDS | constant | Program IDs by network |
| getProgramId | function | Get program ID for a network (with validation) |
Sub-path Imports
import { ShieldedClient } from '@protocol-01/zk-sdk/client';
import { createNote, encryptNote } from '@protocol-01/zk-sdk/notes';
import { MerkleTree } from '@protocol-01/zk-sdk/merkle';
import { ZkProver } from '@protocol-01/zk-sdk/prover';
import { poseidonHash, computeCommitment } from '@protocol-01/zk-sdk/circuits';Modules
ShieldedClient
The main entry point for most users. Manages spending keys, note selection, proof generation, and transaction building.
const client = new ShieldedClient(config);
await client.initialize(seedPhrase);
// Core operations
await client.shield(amount); // deposit to shielded pool
await client.transfer(recipientZkAddress, amount); // private transfer
await client.unshield(recipientPubkey, amount); // withdraw to public
// State
const balance = await client.getShieldedBalance();
const zkAddr = client.getZkAddress();
client.destroy(); // securely wipe keys from memoryNotes
Create and manage shielded UTXO notes.
import { createNote, encryptNote, decryptNote } from '@protocol-01/zk-sdk';
const note = await createNote(amount, ownerPubkey, tokenMintField);
const encrypted = encryptNote(note, viewingKey);
const decrypted = decryptNote(encrypted, viewingSecretKey);Merkle Tree
Sparse Poseidon Merkle tree (depth 20, supports ~1M notes).
import { MerkleTree } from '@protocol-01/zk-sdk';
const tree = new MerkleTree(20);
await tree.initialize();
const leafIndex = tree.insert(noteCommitment);
const proof = tree.generateProof(leafIndex);
const valid = tree.verifyProof(proof, noteCommitment, tree.root);ZK Prover
Generate Groth16 proofs client-side using snarkjs.
import { ZkProver, generateProof } from '@protocol-01/zk-sdk';
// Class-based
const prover = new ZkProver('./transfer.wasm', './transfer_final.zkey');
const { proof, publicSignals } = await prover.generateTransferProof(pubInputs, privInputs);
// Function-based
const result = await generateProof(fullInputs, wasmPath, zkeyPath);
// With hosted circuits
const prover = new ZkProver(undefined, undefined, {
circuitBaseUrl: 'https://cdn.example.com/circuits/v1',
});Viewing Keys
Zcash-style viewing key hierarchy for selective disclosure.
import {
generateSpendingKey,
deriveFullViewingKey,
deriveIncomingViewingKey,
} from '@protocol-01/zk-sdk/keys/viewKeys';
const sk = generateSpendingKey(entropy);
const fvk = deriveFullViewingKey(sk); // view all transactions
const ivk = deriveIncomingViewingKey(fvk); // view incoming onlyCircuits (Poseidon)
Low-level Poseidon hash primitives used throughout the system.
import { poseidonHash, computeCommitment, computeNullifier } from '@protocol-01/zk-sdk';
const hash = await poseidonHash([field1, field2]);
const commitment = await computeCommitment(amount, owner, randomness, tokenMint);
const nullifier = await computeNullifier(commitment, spendingKeyHash);Circuit Files
The prover requires two circuit files to generate Groth16 proofs:
| File | Description | Size |
|---|---|---|
| transfer.wasm | Compiled circuit (WebAssembly) | ~11 MB |
| transfer_final.zkey | Proving key (Groth16) | ~24 MB |
| verification_key.json | Verification key (for local verify) | ~2 KB |
Where to get them
Download pre-built circuit files from: https://github.com/protocol-01/circuits
How to configure
// Option 1: Local file paths (Node.js)
new ZkProver('./circuits/transfer.wasm', './circuits/transfer_final.zkey');
// Option 2: Hosted URL
new ZkProver(undefined, undefined, {
circuitBaseUrl: 'https://cdn.example.com/circuits/v1',
});
// Option 3: Via ShieldedClient config
new ShieldedClient({
connection,
wallet,
circuitBaseUrl: 'https://cdn.example.com/circuits/v1',
});Building circuits from source
# Requires circom v2.2.2 and snarkjs v0.7.5
cd circuits/
circom transfer.circom --wasm --r1cs
snarkjs groth16 setup transfer.r1cs pot20_final.ptau transfer_0000.zkey
echo "random entropy" | snarkjs zkey contribute transfer_0000.zkey transfer_final.zkey
snarkjs zkey export verificationkey transfer_final.zkey verification_key.jsonError Handling
Common errors and solutions
| Error | Cause | Fix |
|---|---|---|
| Circuit file not found | WASM/zkey files missing | Set circuitBaseUrl, or pass explicit wasmPath/zkeyPath |
| Client not initialized | Called methods before initialize() | Call await client.initialize(seed) first |
| getProgramId: unknown network | Invalid network name | Use 'devnet', 'mainnet-beta', or 'localnet' |
| getProgramId: not yet deployed on 'mainnet-beta' | Mainnet not available | Use 'devnet' for testing, or pass a custom programId |
| Proof generation timeout | Proof took > 2 minutes | Ensure circuit files are correct; check available memory |
| Insufficient shielded balance | Not enough notes to cover amount | Shield more tokens first |
| createNote: amount must be non-negative | Negative amount passed | Use amount >= 0n |
| sync() is not yet implemented | Called sync() | Use scanForNotes() with a custom indexer instead |
| SPL token shielding requires token account resolution | Tried to shield an SPL token | Use @protocol-01/zkspl-sdk for SPL tokens |
Prover troubleshooting
If proof generation fails, verify:
- Circuit files exist at the configured paths
- The WASM file matches the zkey (same circuit, same trusted setup)
- Input values are within the BN254 field (< FIELD_MODULUS)
- Sufficient memory is available (~512 MB for the transfer circuit)
Network Support
| Network | Status | Program ID |
|---|---|---|
| devnet | Fully supported | GbVM5yvetrSD194Hnn1BXnR56F8ZWNKnij7DoVP9j27c |
| mainnet-beta | Deployment pending | Pass custom programId when available |
| localnet | Supported (same ID) | GbVM5yvetrSD194Hnn1BXnR56F8ZWNKnij7DoVP9j27c |
Security
- Spending keys never leave the device. All key derivation and proof generation happens client-side.
- All proofs generated locally. There is no remote prover fallback. If local proving fails, it fails.
- No remote dependencies at runtime. The SDK only talks to Solana RPC; there are no Protocol 01 servers in the path.
- Viewing keys are separate from spending keys. You can share an incoming viewing key for auditing without granting spend authority.
- Nullifier-based double-spend prevention. Each note can only be spent once, enforced on-chain.
Migration from Internal Usage
If you were importing from the monorepo directly, the public API is identical. The only differences:
sync()now throws with a descriptive message instead of silently doing nothingscanForNotes()logs a warning that it returns local cache onlyRelayerNetworklogs a deprecation warning on construction
All existing function signatures, return types, and default values are preserved.
Circuit Setup (Quick)
The ZK prover needs compiled circuit files (WASM + zkey) that are too large to bundle with the npm package (~35 MB total). Run the setup script to copy them from the monorepo:
# From within the monorepo (after cloning Protocol-01):
cd packages/zk-sdk
npm run setup
# Or via npx (after installing @protocol-01/zk-sdk):
npx p01-setupThis copies all 6 circuits into a circuits/ directory in your current working directory:
| Circuit | WASM | zkey | vkey | |---|---|---|---| | transfer | transfer.wasm | transfer_final.zkey | transfer_vk.json | | confidential_balance | confidential_balance.wasm | confidential_balance_final.zkey | confidential_balance_vk.json | | balance_proof | balance_proof.wasm | balance_proof_final.zkey | balance_proof_vk.json | | denominated_pool | denominated_pool.wasm | denominated_pool_final.zkey | denominated_pool_vk.json | | denominated_transfer | denominated_transfer.wasm | denominated_transfer_final.zkey | denominated_transfer_vk.json | | subscriber_ownership | subscriber_ownership.wasm | subscriber_ownership_final.zkey | subscriber_ownership_vk.json |
If you are using the SDK outside the monorepo, download the circuit files from: https://github.com/protocol-01/circuits/releases
Then point the prover at them:
const prover = new ZkProver('./circuits/transfer.wasm', './circuits/transfer_final.zkey');License
MIT
