Solana Integration
FHE-powered slot-based private token transfers on Solana.
This is the first release of Private Token Slots. It is for testing purposes only — do not use with real value. The system is evolving rapidly and APIs may change.
Overview
Private Token Slots lets you build confidential token vaults on Solana where:
- Balances are encrypted (no one sees how much anyone holds)
- Transfers happen privately (sender, receiver, amount all hidden)
- Only the vault owner can decrypt balances
┌─────────────────────────────────────────────────────────┐
│ Your Token Vault │
├─────────┬─────────┬─────────┬─────────┬────────────────┤
│ Slot 0 │ Slot 1 │ Slot 2 │ Slot 3 │ ... Slot 255 │
│ [ENC] │ [ENC] │ [ENC] │ [ENC] │ [ENC] │
└─────────┴─────────┴─────────┴─────────┴────────────────┘
↓ Transfer 50 from Slot 0 → Slot 1
┌─────────┬─────────┬─────────┬─────────┬────────────────┐
│ [ENC] │ [ENC] │ [ENC] │ [ENC] │ [ENC] │
└─────────┴─────────┴─────────┴─────────┴────────────────┘
All balances remain encrypted throughout!
How It Works
Architecture
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Your Client │ │ Solana │ │ ZKSDK │
│ │ │ │ │ Coprocessor │
│ 1. Generate │ │ 3. Vault CPI │ │ │
│ FHE keys │────→│ to Oracle │────→│ 4. Compute │
│ │ │ │ │ on FHE │
│ 2. Encrypt │ │ │ │ │
│ transfer │ │ │ │ 5. Return │
│ │ │ │◄──── │ result │
│ 6. Decrypt │◄────│ │ │ │
│ (only you)│ │ │ │ │
└──────────────┘ └──────────────┘ └──────────────┘
What happens at each step:
- Generate FHE keys — You create keys locally. Your secret key (
clientKey) never leaves your machine. - Encrypt transfer — You encrypt the transfer details (which slot, how much). No one sees the plaintext.
- Vault CPI — Your vault program sends the encrypted request to the coprocessor via Solana.
- Compute on FHE — The coprocessor does math on encrypted data. It updates balances without knowing what they are.
- Return result — New encrypted balances are written back to your vault on-chain.
- Decrypt — Only you can decrypt and see the actual balances.
Why Deploy Your Own Vault Program?
You deploy your own vault program (Solana program) because:
- Custom logic — Add your own deposit/withdrawal rules, access control, fees, etc.
- Your keys — You generate FHE keys locally, register with coprocessor, link to your vault
- Slots = accounts — Each slot (0-255) holds an encrypted balance within your vault
The coprocessor is a singleton — all vault programs CPI to the same coprocessor program. You don't deploy the coprocessor, just your vault.
How deposits work:
- User calls your vault's
deposit()→ tokens go to vault's token account - Your vault CPIs to coprocessor → coprocessor encrypts amount into a slot
- New balance blob is stored (currently in worker storage, future: IPFS/Arweave/user DB)
- Vault on-chain gets updated CID pointer — only you (with clientKey) can decrypt
Zero-Trust Security
Your keys never leave your machine:
| Component | Where It Lives | Who Can Access |
|---|---|---|
| ClientKey | Your machine only | Only you |
| ServerKey | ZKSDK Coprocessor | Can compute, cannot decrypt |
| PublicKey | ZKSDK Coprocessor | Anyone can encrypt to vault |
The coprocessor mathematically cannot decrypt your data. It only has evaluation keys that can perform operations on ciphertext.
Quick Start
Install the SDK
npm install @zksdk/private-token-slots-fhe @coral-xyz/anchor @solana/web3.js @solana/spl-token
Network Configuration
// Program IDs (Solana Devnet)
const YOUR_VAULT_PROGRAM_ID = new PublicKey('YOUR_DEPLOYED_VAULT_PROGRAM_ID');
const COPROCESSOR_PROGRAM_ID = new PublicKey('5LLZpQKc8DydeB6mnFU7SD6LRtNRBVRhzhUR3aNBbnmV');
// ZKSDK Alpha Network
const WORKER_URL = 'https://alpha.coprocessor.zksdk.com';
const RPC_URL = 'https://api.devnet.solana.com';
The on-chain program uses "oracle" naming (oracleProgram, oracleRequest). This is legacy terminology — conceptually it's a coprocessor that performs FHE computation.
Step-by-Step Guide
Understanding the Two IDs
Before starting, understand there are two separate IDs:
| ID | What It Is | Where It Lives |
|---|---|---|
| Vault Key ID | 64-char hex ID for your FHE keys | FHE Worker (coprocessor) |
| Vault Program ID | Your deployed Solana program | Solana blockchain |
1. Generate FHE Keys & Register with Coprocessor
First, generate FHE keys locally and register them with the coprocessor. This gives you a vaultKeyId.
import { SlotVaultClient } from '@zksdk/private-token-slots-fhe';
const client = new SlotVaultClient({
workerUrl: 'https://alpha.coprocessor.zksdk.com'
});
// Generate FHE keys locally (ZERO TRUST!)
const { vaultKeyId, clientKey, serverKey, publicKey } = await client.createVault();
// ⚠️ CRITICAL: Save clientKey securely - only YOU can decrypt!
console.log('Vault Key ID:', vaultKeyId);
// Example: "a1b2c3d4e5f6...64 chars..."
What happens:
- Keys generated on YOUR machine (clientKey never sent anywhere)
- ServerKey + PublicKey sent to coprocessor
- Coprocessor returns
vaultKeyId(64-char hex)
2. Deploy Your Vault Program
Now deploy your Solana program. The reference vault has the ZKSDK already integrated.
Option A: Clone and deploy the reference vault
git clone https://github.com/zksdk-labs/zksdk
cd zksdk/chain-adapters/solana/token_vault
anchor build
anchor deploy --provider.cluster devnet
After deploy, note your program ID:
solana address -k target/deploy/token_vault-keypair.json
# Example: "YourVau1tPr0gramID..."
Option B: Add SDK to your existing program
# Cargo.toml
[dependencies]
zksdk-private-token-slots = "0.1.0"
The SDK gives you the CPI functions to call the coprocessor.
3. Initialize Vault Account On-Chain
Now link your on-chain vault to your FHE keys using the vaultKeyId from Step 1.
// Derive vault PDA
const [vaultPda] = PublicKey.findProgramAddressSync(
[Buffer.from('vault'), tokenMint.toBuffer()],
YOUR_VAULT_PROGRAM_ID
);
// Initialize vault with your FHE vault key ID
await vaultProgram.methods
.initializeVault(vaultKeyId)
.accounts({
vault: vaultPda,
tokenMint: tokenMint,
vaultTokenAccount: vaultTokenAccount,
authority: wallet.publicKey,
systemProgram: SystemProgram.programId,
})
.rpc();
4. Deposit Tokens
// Derive user position PDA
const [userPositionPda] = PublicKey.findProgramAddressSync(
[Buffer.from('position'), vaultPda.toBuffer(), wallet.publicKey.toBuffer()],
YOUR_VAULT_PROGRAM_ID
);
// Deposit 100 tokens (6 decimals)
await vaultProgram.methods
.deposit(new anchor.BN(100_000_000))
.accounts({
vault: vaultPda,
userPosition: userPositionPda,
userTokenAccount: userTokenAccount,
vaultTokenAccount: vaultTokenAccount,
oracleProgram: COPROCESSOR_PROGRAM_ID, // Legacy name - this is the coprocessor
user: wallet.publicKey,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
})
.rpc();
// Wait for coprocessor to create balance blob
let balanceBlobCid = '';
for (let i = 0; i < 30; i++) {
await new Promise(r => setTimeout(r, 2000));
const vault = await vaultProgram.account.vault.fetch(vaultPda);
if (vault.balanceBlobCid) {
balanceBlobCid = vault.balanceBlobCid;
break;
}
}
5. Private Transfer
// Encrypt transfer: Slot 0 → Slot 1, Amount 50
const { encrypted_data } = client.encryptTransfer(0, 1, 50);
// Upload to coprocessor storage
const cid = await client.storeBalanceBlob(encrypted_data);
// Submit to Solana
const oracleRequestKeypair = Keypair.generate();
await vaultProgram.methods
.requestPrivateTransfer(`c=${cid}`, 0, Buffer.from([]))
.accounts({
vault: vaultPda,
userPosition: userPositionPda,
oracleRequest: oracleRequestKeypair.publicKey,
oracleProgram: COPROCESSOR_PROGRAM_ID, // Legacy name - this is the coprocessor
user: wallet.publicKey,
systemProgram: SystemProgram.programId,
})
.signers([oracleRequestKeypair])
.rpc();
6. Wait & Decrypt
// Poll for completion
let resultCid = null;
for (let i = 0; i < 45; i++) {
await new Promise(r => setTimeout(r, 2000));
const account = await oracleProgram.account.oracleRequest.fetch(
oracleRequestKeypair.publicKey
);
const status = Object.keys(account.status)[0];
if (status === 'completed') {
resultCid = account.resultCid;
break;
}
}
// Fetch and decrypt (only YOU can!)
const encryptedResult = await client.fetchBalanceBlob(resultCid);
const result = client.decryptResult(encryptedResult);
console.log('New balances:', result.balances); // [205, 50, 0, 0, ...]
console.log('Transfer valid:', result.isValid); // true
Rust Integration
For Solana programs that need to CPI to the coprocessor:
# Cargo.toml
[dependencies]
zksdk-private-token-slots = "0.1.0"
use zksdk_core::prelude::*;
pub fn request_transfer(ctx: Context<RequestTransfer>, input_cid: String) -> Result<()> {
let metadata = VaultMetadataBuilder::new(
&ctx.accounts.vault.key(),
&ctx.accounts.vault.vault_key_id,
&ctx.accounts.vault.balance_blob_cid,
).build();
// Legacy function name - calls the coprocessor
submit_oracle_request(
&ctx.accounts.oracle_program,
&ctx.accounts.oracle_request,
&ctx.accounts.user,
&ctx.accounts.system_program,
CircuitType::CmuxTokenV1Linear.as_str(),
vec![],
&input_cid,
&metadata,
)?;
Ok(())
}
Network Configuration
ZKSDK Alpha Network (Testnet)
| Resource | Value |
|---|---|
| FHE Worker | https://alpha.coprocessor.zksdk.com |
| Coprocessor Program | 5LLZpQKc8DydeB6mnFU7SD6LRtNRBVRhzhUR3aNBbnmV |
| Reference Vault | BsVtbakPuEKJfsyFDXtCjWgHSxNbSXy1U4VPwsErSTDM (example only) |
| Chain | Solana Devnet |
| RPC URL | https://api.devnet.solana.com |
The Reference Vault above is just an example. You deploy your own vault program — the coprocessor listens to all vault programs that CPI to it.
Packages
| Package | Platform | Version |
|---|---|---|
| @zksdk/core | npm | 0.1.0 |
| @zksdk/private-token-slots-fhe | npm | 0.1.0 |
| zksdk-core | crates.io | 0.1.0 |
| zksdk-private-token-slots | crates.io | 0.1.0 |
API Reference
SlotVaultClient
import { SlotVaultClient } from '@zksdk/private-token-slots-fhe';
const client = new SlotVaultClient({
workerUrl: string,
apiKey?: string
});
// Create new vault (generates keys locally)
await client.createVault(): Promise<{
vaultKeyId: string; // 64-char hex ID
clientKey: string; // KEEP SECRET - for decryption
serverKey: string; // Safe to send - for computation
publicKey: string; // Safe to send - for encryption
}>;
// Load existing vault
await client.loadVault(vaultKeyId: string, clientKey?: string): Promise<VaultParams>;
// Encrypt transfer parameters
client.encryptTransfer(senderIdx: number, receiverIdx: number, amount: number): {
encrypted_data: string;
vault_key_id: string;
};
// Store encrypted blob
await client.storeBalanceBlob(encryptedBlob: string): Promise<string>; // Returns CID
// Fetch encrypted blob
await client.fetchBalanceBlob(cid: string): Promise<string>;
// Decrypt result (requires ClientKey)
client.decryptResult(encryptedResultB64: string): {
balances: number[]; // [205, 50, 0, 0, ...]
isValid: boolean; // Transfer validation result
};
Limitations (Alpha)
- Slot balances: 0-255 per slot (8-bit)
- Slots per vault: Up to 256
- Processing time: ~10-30 seconds per transfer
- Chain support: Solana devnet only
- Storage: Temporary centralized storage for demo purposes
- Not audited: Do not use with real value
Storage Roadmap
Currently, encrypted data is stored in temporary centralized storage for demonstration purposes.
Future plans:
- Decentralized storage (IPFS, Arweave)
- On-chain encrypted blobs
- User-controlled storage options
Full End-to-End Example
A complete example using the published SDK.
-
Vault Key ID — 64-char hex from
createVault()(Step 1)- This registers your FHE keys with the coprocessor
- You pass this to
initializeVault()on-chain
-
Your Vault Program — You deploy this on Solana (Step 2)
- Use the reference vault OR build your own with the SDK
- Your program CPIs to the singleton coprocessor
The coprocessor listens to ALL vault programs — you just deploy your vault, not the coprocessor.
import { SlotVaultClient } from '@zksdk/private-token-slots-fhe';
import { Connection, Keypair, PublicKey, SystemProgram } from '@solana/web3.js';
import * as anchor from '@coral-xyz/anchor';
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
/**
* What talks to what:
*
* SDK (SlotVaultClient) ──HTTP──→ WORKER_URL (FHE coprocessor API)
* - createVault(), storeBalanceBlob(), fetchBalanceBlob()
* - encryptTransfer(), decryptResult() are local (no network)
*
* Your Vault Program ──CPI──→ COPROCESSOR_PROGRAM_ID (on-chain)
* - deposit(), requestPrivateTransfer()
* - The reference vault has this hardcoded already
*/
const WORKER_URL = 'https://alpha.coprocessor.zksdk.com';
const RPC_URL = 'https://api.devnet.solana.com';
const YOUR_VAULT_PROGRAM_ID = new PublicKey('YOUR_DEPLOYED_VAULT_PROGRAM_ID');
const COPROCESSOR_PROGRAM_ID = new PublicKey('5LLZpQKc8DydeB6mnFU7SD6LRtNRBVRhzhUR3aNBbnmV');
async function main() {
// ============ STEP 1: Generate FHE Keys & Register ============
// Keys generated locally, only serverKey + publicKey sent to coprocessor
const client = new SlotVaultClient({ workerUrl: WORKER_URL });
const { vaultKeyId, clientKey } = await client.createVault();
console.log('✅ Vault Key ID:', vaultKeyId);
// ⚠️ SAVE clientKey SECURELY - only YOU can decrypt!
// ============ STEP 2: Deploy Vault Program ============
// Already done - YOUR_VAULT_PROGRAM_ID is your deployed program
// See "Deploy Your Vault Program" section above
// ============ STEP 3: Initialize Vault On-Chain ============
// Links your on-chain vault to your FHE keys
const [vaultPda] = PublicKey.findProgramAddressSync(
[Buffer.from('vault'), tokenMint.toBuffer()],
YOUR_VAULT_PROGRAM_ID
);
await vaultProgram.methods
.initializeVault(vaultKeyId) // Pass the vault key ID from Step 1
.accounts({
vault: vaultPda,
tokenMint: tokenMint,
vaultTokenAccount: vaultTokenAccount,
authority: wallet.publicKey,
systemProgram: SystemProgram.programId,
})
.rpc();
console.log('✅ Vault initialized on-chain');
// ============ STEP 4: Deposit Tokens ============
const [userPositionPda] = PublicKey.findProgramAddressSync(
[Buffer.from('position'), vaultPda.toBuffer(), wallet.publicKey.toBuffer()],
YOUR_VAULT_PROGRAM_ID
);
await vaultProgram.methods
.deposit(new anchor.BN(100_000_000)) // 100 tokens (6 decimals)
.accounts({
vault: vaultPda,
userPosition: userPositionPda,
userTokenAccount: userTokenAccount,
vaultTokenAccount: vaultTokenAccount,
oracleProgram: COPROCESSOR_PROGRAM_ID,
user: wallet.publicKey,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
})
.rpc();
console.log('✅ Tokens deposited');
// Wait for coprocessor to create balance blob
let balanceBlobCid = '';
for (let i = 0; i < 30; i++) {
await new Promise(r => setTimeout(r, 2000));
const vault = await vaultProgram.account.vault.fetch(vaultPda);
if (vault.balanceBlobCid) {
balanceBlobCid = vault.balanceBlobCid;
break;
}
}
console.log('✅ Balance blob created:', balanceBlobCid);
// ============ STEP 5: Encrypt & Submit Transfer ============
const { encrypted_data } = client.encryptTransfer(0, 1, 50); // Slot 0 → Slot 1, 50 tokens
const cid = await client.storeBalanceBlob(encrypted_data);
console.log('✅ Encrypted transfer uploaded, CID:', cid);
const oracleRequestKeypair = Keypair.generate();
await vaultProgram.methods
.requestPrivateTransfer(`c=${cid}`, 0, Buffer.from([]))
.accounts({
vault: vaultPda,
userPosition: userPositionPda,
oracleRequest: oracleRequestKeypair.publicKey,
oracleProgram: COPROCESSOR_PROGRAM_ID,
user: wallet.publicKey,
systemProgram: SystemProgram.programId,
})
.signers([oracleRequestKeypair])
.rpc();
console.log('✅ Transfer request submitted');
// ============ STEP 6: Wait & Decrypt ============
let resultCid = null;
for (let i = 0; i < 45; i++) {
await new Promise(r => setTimeout(r, 2000));
const account = await oracleProgram.account.oracleRequest.fetch(oracleRequestKeypair.publicKey);
if (Object.keys(account.status)[0] === 'completed') {
resultCid = account.resultCid;
break;
}
}
const encryptedResult = await client.fetchBalanceBlob(resultCid!);
const result = client.decryptResult(encryptedResult);
console.log('✅ New balances:', result.balances); // [205, 50, 0, 0, ...]
console.log('✅ Transfer valid:', result.isValid);
}
main().catch(console.error);
The flow:
- Generate keys locally →
createVault()registers with coprocessor, returnsvaultKeyId - Deploy vault program → Your Solana program (already done)
- Initialize on-chain → Links vault PDA to your
vaultKeyId - Deposit tokens → Coprocessor creates encrypted balance blob
- Encrypt & submit → Encrypt transfer, upload, call vault
- Decrypt result → Only you can decrypt with your
clientKey
Run it:
npx ts-node your-e2e-test.ts
Support
- GitHub: zksdk-labs/zksdk
- Issues: Report bugs
- Contact: Get in touch
For access to the private repository, email team@zksdk.com