veilo-sdk-core
v0.1.37
Published
Tiny TypeScript SDK for the `privacy-pool` Anchor program.
Readme
@zkprivacysol/sdk-core
Tiny TypeScript SDK for the privacy-pool Anchor program.
This package wraps the on-chain program with a small set of ergonomic helpers for:
- Deriving PDAs (
config,vault,note_tree,nullifiers) - Initializing the pool with fixed SOL denominations
- Depositing using a note commitment + off-chain Merkle root
- Withdrawing via an authorized relayer (with fee + TVL accounting)
- Building simple Merkle roots off-chain (for demo / testing)
- Handling note commitments (
createRandomNote,commitNote, etc.)
Status: internal/dev SDK. No production guarantees.
ZK verification is expected to happen off-chain in a relayer service.
On-chain,proof: Vec<u8>is treated as opaque bytes (hook for a future verifier).
1. Install
From the monorepo root (or inside the package folder):
cd packages/sdk-core
npm installIf you publish it somewhere later:
npm install @zkprivacysol/sdk-core2. Prerequisites
You need:
A running Solana validator (localnet recommended):
solana-test-validatorThe
privacy-poolprogram built and deployed to that validator.The
privacy-poolAnchor IDL available (the SDK tests load it from):../../privacy-pool/target/idl/privacy_pool.jsonA funded keypair on that validator:
solana config set --url http://127.0.0.1:8899 solana-keygen new --outfile ~/.config/solana/id.json solana airdrop 10
Environment variables for tests:
export ANCHOR_PROVIDER_URL=http://127.0.0.1:8899
export ANCHOR_WALLET=$HOME/.config/solana/id.json3. Build & Test
From packages/sdk-core:
# Typecheck & build to dist/
npm run build
# Run unit + integration tests
npm testWhat the tests do:
Unit tests (
note.test.ts)createRandomNote/encodeNoteToBytes/commitNote/createNoteWithCommitment- Ensures 32-byte commitments, deterministic encoding, etc.
Integration test (
sdk.integration.test.ts)- Loads the
privacy-poolIDL fromprivacy-pool/target/idl/privacy_pool.json - Constructs an Anchor
Programwith a provider fromANCHOR_PROVIDER_URL/ANCHOR_WALLET - Runs end-to-end flow:
initializePool(configures denoms + fee)createNoteAndDeposit(creates note, commits it, calls on-chaindepositFixedand updates the Merkle root)addRelayerwithdrawViaRelayer(using a Merkle root that actually contains the note + a demo nullifier, empty proof)- Asserts vault TVL decreased, recipient gained funds, relayer received fee.
- Loads the
The integration test uses an empty proof for now; in a real deployment your relayer would generate and verify a Groth16/Plonk proof off-chain, then pass the proof bytes into withdraw.
4. SDK Surface
4.1 PDA helpers
import { getPoolPdas } from "@zkprivacysol/sdk-core";
import { PublicKey } from "@solana/web3.js";
const programId = new PublicKey("YourProgram1111111111111111111111111111111111");
const { config, vault, noteTree, nullifiers } = getPoolPdas(programId);These must match the on-chain seeds (v3 layout):
["privacy_config_v3"]["privacy_vault_v3"]["privacy_note_tree_v3"]["privacy_nullifiers_v3"]
4.2 Pool init / configuration
import * as anchor from "@coral-xyz/anchor";
import { initializePool } from "@zkprivacysol/sdk-core";
import { sol } from "@zkprivacysol/sdk-core/config";
import type { Program, Idl } from "@coral-xyz/anchor";
async function initPool(program: Program<Idl>, adminWallet: anchor.Wallet) {
await initializePool({
program,
admin: adminWallet,
denomsLamports: [sol(1), sol(5)], // 1 SOL & 5 SOL
feeBps: 50, // 0.5% fee
});
}This calls the on-chain initialize instruction and sets:
- fixed denominations (in lamports)
- vault + note tree + nullifier set PDAs
- fee in basis points
- initial TVL = 0
4.3 Notes & commitments
src/note.ts is a small “note” helper module. It does not implement real zk-friendly Poseidon hashing yet; it’s just using SHA-256 to get 32-byte commitments that the on-chain program treats as opaque.
import {
createRandomNote,
encodeNoteToBytes,
commitNote,
createNoteWithCommitment,
} from "@zkprivacysol/sdk-core/note";
import { Keypair } from "@solana/web3.js";
const owner = Keypair.generate().publicKey;
// 1. Create a note
const note = createRandomNote({
value: 1_000_000n, // lamports
owner,
});
// 2. Encode deterministically
const bytes = encodeNoteToBytes(note);
// 3. Hash to a 32-byte commitment (placeholder)
const commitment = commitNote(note);
// 4. Convenience combo
const full = createNoteWithCommitment({
value: 1_000_000n,
owner,
});
// full.commitment is 32 bytesEncoding layout:
value (u64 LE, 8 bytes)
|| owner pubkey (32 bytes)
|| rho (32 bytes random)
|| r (32 bytes random)Hash:
commitment = sha256(encodedBytes);Later, a real implementation should swap this out for the exact hash function used inside the zk circuit (Poseidon/Rescue/etc.). The on-chain program just sees [u8; 32].
4.4 Merkle helpers (demo-only)
src/merkle.ts provides very basic Merkle helpers so callers can build roots off-chain. This is meant for demos/tests, not production.
Key functions:
import {
merkleLeafFromCommitment,
merkleHashPair,
merkleRootFromLeaves,
MerkleTree,
} from "@zkprivacysol/sdk-core/merkle";
// Stateless helpers
const leaf = merkleLeafFromCommitment(commitment);
const parent = merkleHashPair(left, right);
const root = merkleRootFromLeaves([leaf1, leaf2, leaf3]);
// Simple incremental tree (toy)
const tree = new MerkleTree();
const { index, root: newRoot } = tree.insert(commitment);
const path = tree.getPath(index); // Merkle path for proofsNotes:
- Uses SHA-256 under the hood, returning
Uint8Arrayof length 32. - Pads with a “zero node” derived from hashing the all-zero leaf repeatedly up the tree.
- This is intentionally “toy” to keep the SDK usable while the real circuit/Merkle design is still in flux.
- On-chain, the program only stores the latest Merkle root in the
NoteTreeaccount (v3 layout exposes acurrentRootfield).
4.5 Deposits
Low-level helper (you supply both commitment + Merkle root):
import * as anchor from "@coral-xyz/anchor";
import { depositFixedSol } from "@zkprivacysol/sdk-core";
const provider = anchor.getProvider() as anchor.AnchorProvider;
const wallet = provider.wallet as anchor.Wallet;
await depositFixedSol({
program,
depositor: wallet,
denomIndex: 0, // index into cfg.denoms
commitment, // 32-byte note commitment
newRoot, // 32-byte Merkle root (caller computed off-chain)
});High-level helper (with note creation, but you still feed a root):
import * as anchor from "@coral-xyz/anchor";
import { createNoteAndDeposit } from "@zkprivacysol/sdk-core";
import { sol } from "@zkprivacysol/sdk-core/config";
const provider = anchor.getProvider() as anchor.AnchorProvider;
const wallet = provider.wallet as anchor.Wallet;
const dummyRoot = new Uint8Array(32).fill(7); // replace with real Merkle root
const note = await createNoteAndDeposit({
program,
depositor: wallet,
denomIndex: 0,
valueLamports: sol(1),
newRoot: dummyRoot,
});
// note.commitment can later be used in your off-chain treeThere’s also a higher-level helper that integrates directly with an in-memory MerkleTree:
import { createNoteDepositWithMerkle } from "@zkprivacysol/sdk-core";
import { MerkleTree } from "@zkprivacysol/sdk-core/merkle";
const tree = new MerkleTree();
const { note, leafIndex, root, merklePath } =
await createNoteDepositWithMerkle({
program,
depositor: wallet,
denomIndex: 0,
valueLamports: sol(1),
tree,
});
// `root` is what got written to the on-chain NoteTree
// `merklePath` can be used as witness for the zk circuitOn-chain, the depositFixed instruction:
- moves SOL from
depositorto the vault PDA, - updates TVL,
- writes
new_rootinto the on-chain note tree’s current root field.
4.6 Relayers & Withdrawals
Add a relayer (admin-only):
import * as anchor from "@coral-xyz/anchor";
import { addRelayer } from "@zkprivacysol/sdk-core";
import { Keypair } from "@solana/web3.js";
const provider = anchor.getProvider() as anchor.AnchorProvider;
const wallet = provider.wallet as anchor.Wallet;
const relayer = Keypair.generate();
await addRelayer({
program,
admin: wallet,
newRelayer: relayer.publicKey,
});Withdraw via relayer (SDK-level helper):
import { withdrawViaRelayer } from "@zkprivacysol/sdk-core";
import { Keypair } from "@solana/web3.js";
const relayer = Keypair.generate();
const recipient = Keypair.generate();
const root = /* 32-byte Merkle root containing the note */;
const nullifier = new Uint8Array(32).fill(3); // demo only
// In the real world, `proof` will be zk-proof bytes coming from your prover.
const proofBytes = new Uint8Array([]); // currently ignored by on-chain program
await withdrawViaRelayer({
program,
relayer,
recipient: recipient.publicKey,
denomIndex: 0,
root,
nullifier,
proof: proofBytes,
});There is also a higher-level helper (withdrawViaRelayerWithProof) that takes:
noteData(serialized note),merklePath,feeBps,- a
builder: ProofBuildercallback
and lets you plug in your own proof generator. In practice, your relayer service will own that logic.
Production pattern:
- A backend relayer service (see
packages/relayeror similar) owns:- the proving key / circuits,
- a mirror view of the Merkle tree and nullifier set,
- a funded relayer keypair.
- The front-end sends a withdraw request to that service:
root,nullifier,denomIndex,recipient, plus any private witness data.
- The relayer:
- Builds & verifies the zk proof off-chain.
- Packs it into bytes (e.g. via a
packProofToByteshelper). - Calls the on-chain
withdrawvia Anchor, using the same program/PDAs as the SDK.
From the SDK’s perspective, proof: Uint8Array is already-built; this package doesn’t know how you generated it.
5. Environment & Localnet
To run the integration tests successfully, you should:
Start local validator:
solana-test-validatorBuild & deploy the
privacy-poolAnchor program inpackages/privacy-pool:cd packages/privacy-pool anchor build anchor deployEnsure your CLI and wallet match the validator:
solana config set --url http://127.0.0.1:8899 solana-keygen new --outfile ~/.config/solana/id.json solana airdrop 10Export env vars (or inject via
npm testscript):export ANCHOR_PROVIDER_URL=http://127.0.0.1:8899 export ANCHOR_WALLET=$HOME/.config/solana/id.jsonThen from
packages/sdk-core:npm run build npm test
6. Limitations & TODOs
- Merkle tree is minimal.
- Toy implementation, primarily for demos/tests.
- No persisted tree; you’re expected to maintain state in your own service.
- On-chain NoteTree only stores the latest root.
- Historical roots/nullifiers must be mirrored off-chain.
- Proofs are relayer-only.
- SDK does not generate Groth16/Plonk proofs.
- On-chain program currently only sees
Vec<u8>and does not verify it yet.
- API is still evolving.
- Types, exports, and function signatures may change as the circuit + relayer design solidifies.
This SDK is meant as a thin, hackable layer around the Anchor program while the core privacy design (circuit, proof system, Merkle layout, relayer flow) is being explored.
