solana-private-state-toolkit
v0.1.0
Published
Privacy infrastructure for Solana: private but verifiable state using cryptographic commitments without zero-knowledge proofs. Build apps with encrypted off-chain state and 81-byte on-chain commitments.
Maintainers
Readme
Private State Toolkit (PST)
Private State Toolkit is privacy infrastructure for Solana programs. It enables private but verifiable application state without zk by storing only cryptographic commitments on-chain and keeping encrypted state off-chain. This is not a private payments app — it's a reusable primitive for builders.
Installation
npm install @thewoodfish/private-state-toolkitOr with yarn:
yarn add @thewoodfish/private-state-toolkitWhy this exists
Many Solana apps need integrity without public data leakage:
- Games with hidden state
- Identity attestations or claims
- App configuration or secrets
- Collaborative apps with private state
PST gives you:
- On-chain verifiability (hash commitment + nonce)
- Off-chain privacy (encrypted payload stays with the app/user)
- Minimal on-chain footprint (cheap, indexer‑resistant)
Core idea
- App encrypts state locally (AES‑256‑GCM).
- It computes
commitment = sha256(nonce || encrypted_payload). - On-chain account stores only:
authoritycommitmentnoncepolicy
- Update is valid only if:
old_commitmentmatchesnoncerule obeys policy
Everyone can verify ordering and consistency — no one sees the payload without the key.
Quick Start
1. Install the SDK
npm install @thewoodfish/private-state-toolkit2. Set up environment
# .env file
PST_PROGRAM_ID=4FeUYtneSbfieLwjUT1ceHtv8nDXFk2autCZFyDhpkeD3. Initialize private state
import { Connection, Keypair } from "@solana/web3.js";
import { initPrivateState, encryptPayload, UpdatePolicy } from "@thewoodfish/private-state-toolkit";
const connection = new Connection("https://api.devnet.solana.com");
const payer = Keypair.generate(); // Your wallet keypair
const encryptionKey = Buffer.from("your-32-byte-key"); // Store securely!
// Your app state
const appState = { counter: 0, gameState: "active" };
// Encrypt and create commitment
const encrypted = encryptPayload(Buffer.from(JSON.stringify(appState)), encryptionKey);
// Initialize on-chain
const { statePubkey, signature } = await initPrivateState(
connection,
payer,
encrypted,
UpdatePolicy.StrictSequential
);
console.log("PST account created:", statePubkey.toBase58());4. Update private state
import { updatePrivateState, commitment } from "@thewoodfish/private-state-toolkit";
// Update your state
const newState = { counter: 1, gameState: "active" };
const newEncrypted = encryptPayload(Buffer.from(JSON.stringify(newState)), encryptionKey);
// Compute new commitment
const newCommitment = commitment(newNonce, newEncrypted);
// Submit update
const sig = await updatePrivateState(
connection,
payer,
statePubkey,
oldCommitment,
oldNonce,
newCommitment,
newNonce
);5. Use in your Solana program (CPI)
use anchor_lang::prelude::*;
use private_state_toolkit::cpi::accounts::AssertState;
use private_state_toolkit::program::PrivateStateToolkit;
#[derive(Accounts)]
pub struct ValidatePrivateState<'info> {
#[account(mut)]
pub private_state: AccountInfo<'info>,
pub pst_program: Program<'info, PrivateStateToolkit>,
}
pub fn my_instruction(
ctx: Context<ValidatePrivateState>,
expected_commitment: [u8; 32],
expected_nonce: u64,
) -> Result<()> {
// Validate private state before executing logic
let cpi_ctx = CpiContext::new(
ctx.accounts.pst_program.to_account_info(),
AssertState {
private_state: ctx.accounts.private_state.to_account_info(),
},
);
private_state_toolkit::cpi::assert_state(
cpi_ctx,
expected_commitment,
expected_nonce,
)?;
// Your program logic here - state is validated!
Ok(())
}Composability via CPI (assert_state)
PST now exposes a CPI‑friendly validation hook:
pub fn assert_state(
ctx: Context<AssertState>,
expected_commitment: [u8; 32],
expected_nonce: u64
) -> Result<()>- Read‑only (no mutation)
- Deterministic + cheap
- Works from any program via CPI
CPI example (from pst_consumer)
let cpi_accounts = private_state_toolkit::cpi::accounts::AssertState {
private_state: ctx.accounts.private_state.to_account_info(),
};
let cpi_ctx = CpiContext::new(
ctx.accounts.pst_program.to_account_info(),
cpi_accounts,
);
private_state_toolkit::cpi::assert_state(
cpi_ctx,
expected_commitment,
expected_nonce,
)?;This lets any program gate actions on fresh private state without decrypting.
Update policies
PST supports two minimal, real update policies:
pub enum UpdatePolicy {
StrictSequential = 0, // next_nonce == stored_nonce + 1
AllowSkips = 1, // next_nonce > stored_nonce
}Why it matters:
- StrictSequential for deterministic apps (games, turn‑based state)
- AllowSkips for async/offline workflows (batched or delayed updates)
Policies are enforced on-chain during update and can be changed via set_policy.
Local/Chain Sync Protocol (2‑phase)
PST avoids race conditions with a two‑phase local protocol:
Files:
state/state.committed.json= last confirmed state (local source of truth)state/state.pending.json= staged update awaiting confirmationdemo-key.json= demo encryption key
Flow:
inc.tswritesstate.pending.jsonbefore submitting the transaction.- After confirmation, it atomically promotes to
state.committed.json. watch.tstreats chain nonce/commitment as source of truth and emits status:IN_SYNC— chain == committedPENDING— pending exists, chain == committedLANDED_PENDING— chain == pending (auto‑promote)STALE— chain ahead of committedDIVERGED— local ahead of chain
This avoids “sleep and retry” hacks and works reliably under websocket lag.
Demo: Private Counter
What it proves
- Counter value is encrypted locally
- On-chain only stores commitment + nonce + policy
- Real‑time updates via WebSocket subscriptions
- Observer can verify updates but cannot decrypt
Setup and Installation
Prerequisites
- Rust 1.75+
- Solana CLI 1.18+
- Anchor CLI 0.30.1
- Node.js 18+
Install dependencies
npm installBuild programs
anchor buildDeploy to devnet
# Configure Solana CLI for devnet
solana config set --url https://api.devnet.solana.com
# Airdrop SOL for deployment
solana airdrop 2
# Deploy both programs
anchor deploy
# Note the program IDs and update Anchor.toml if differentRun tests
anchor testRun (devnet)
export PST_PROGRAM_ID=<deployed_program_id>
export SOLANA_RPC_URL=https://api.devnet.solana.com
# terminal 1
npx ts-node scripts/init.ts
# terminal 2
TS_NODE_CACHE=false npx ts-node scripts/watch.ts
# terminal 3
npx ts-node scripts/inc.ts
npx ts-node scripts/inc.ts
# optional observer (no key)
TS_NODE_CACHE=false npx ts-node scripts/observer.tsPolicy selection
# strict (default)
POLICY=strict npx ts-node scripts/init.ts
# allow_skips
POLICY=allow_skips npx ts-node scripts/init.ts
# demo skip in allow_skips mode
npx ts-node scripts/inc.ts --skip 3Inspect pending vs committed
ls state
cat state/state.pending.json
cat state/state.committed.jsonYou should see:
state.pending.jsonappear immediately oninc.tsstartstate.committed.jsonupdate only after confirmation
Demo: CPI consumer (composability)
This demo proves any program can gate actions using PST without decrypting.
export PST_PROGRAM_ID=<deployed_pst_id>
export PST_CONSUMER_PROGRAM_ID=<deployed_consumer_id>
export SOLANA_RPC_URL=https://api.devnet.solana.com
npx ts-node scripts/consumer_demo.tsFlow:
- Initializes a PST private counter
- Initializes a consumer program referencing that PST account
- Calls
gated_actionwith expected commitment/nonce (succeeds) - Updates PST
- Calls
gated_actionagain with new expected values (succeeds)
SDK highlights
- AES‑256‑GCM encryption helpers
- Commitment hash =
sha256(nonce || payload) - Manual account decoding (skip discriminator, parse u64 LE, policy)
assertState+setPolicyhelpers- WebSocket subscription helper
Why indexers can’t see your data
Indexers only see:
- Account authority
- Commitment hash
- Nonce increments
- Policy byte
They cannot derive or decrypt the payload without the key.
Honest limitations
- This is not anonymity — observers still see account + update cadence
- Payload durability depends on your off‑chain storage
- No zk proofs — integrity is commitment‑based only
Project structure
programs/private_state_toolkit/src/lib.rs
programs/pst_consumer/src/lib.rs
sdk/index.ts
scripts/init.ts
scripts/inc.ts
scripts/watch.ts
scripts/observer.ts
scripts/consumer_demo.ts
scripts/fs_atomic.ts
README.mdPST makes private state easy on Solana: verifiable updates on‑chain, encrypted data off‑chain, zero zk complexity, and composable CPI validation.
