A comprehensive workshop for understanding and implementing Solana's Confidential Balances feature in Token-2022 (Token Extensions).
Confidential Balances is a set of Token-2022 extensions that enable privacy on Solana asset transfers. Instead of all token amounts being visible on-chain, balances and transfer amounts are encrypted using advanced cryptographic techniques.
Confidential Balances uses the Token-2022 (Token Extensions) program, which allows modular features to be added to tokens.
| Extension | Extension Type | Applied To | Required | Purpose |
|---|---|---|---|---|
| ConfidentialTransferMint | ExtensionType(11) |
Mint | Yes | Configures mint-level settings (auditor, authority, auto-approval) |
| ConfidentialTransferAccount | ExtensionType(12) |
Token Account | Yes | Stores encrypted balances and encryption keys |
| ConfidentialTransferFeeConfig | ExtensionType(13) |
Mint | Optional | Enables confidential transfer fee calculation |
| ConfidentialMintBurn | ExtensionType(33) |
Mint | Optional | Allows private token issuance (disables deposit/withdraw) |
Key Points:
- Extensions must be initialized at creation time (cannot be added later)
- Account space must be allocated to fit extension data
- Token-2022 Program ID:
TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
Confidential Balances support varying degrees of configurable privacy:
- Disabled - No confidentiality (standard SPL tokens)
- Whitelisted - Only approved accounts can use confidential transfers
- Opt-in - Users choose to enable confidentiality
- Required - All transfers must be confidential
The privacy is achieved through:
- Twisted ElGamal Encryption - Homomorphic encryption enabling arithmetic on encrypted data (Curve25519/Ristretto)
- AES-GCM-SIV - Authenticated encryption for efficient balance viewing by account owners
- Pedersen Commitments - Binding, hiding commitments for zero-knowledge proofs
- Sigma Protocols (ZKPs) - Proves validity without revealing amounts
Confidential transfers require zero-knowledge proofs verified by a dedicated Solana program:
- Program ID:
ZkE1Gama1Proof11111111111111111111111111111 - Purpose: Verifies equality, range, and validity proofs on-chain
- Integration: Token-2022 instructions reference proof context accounts
.
├── src/ # Core implementation
│ ├── configure.rs # Configure accounts for confidential transfers
│ ├── deposit.rs # Deposit from public to confidential
│ ├── apply_pending.rs # Apply pending to available balance
│ ├── withdraw.rs # Withdraw from confidential to public
│ ├── transfer.rs # Confidential transfer between accounts
│ └── bin/
│ └── demo-server.rs # HTTP API wrapping the modules (used by the slide-deck demo)
├── examples/
│ ├── run_transfer.rs # End-to-end transfer with balance display
│ └── get_balances.rs # Query and decrypt all balance types
├── tests/
│ ├── integration_test.rs # Integration tests for all operations
│ └── common/ # Test utilities
├── docs/
│ ├── guides/
│ │ ├── product-guide.md # High-level product overview
│ │ └── wallet-integration.md # Guide for wallet developers
│ ├── reference/
│ │ ├── token-extensions.md # Token-2022 program architecture
│ │ ├── cryptography.md # Encryption & proof details
│ │ └── rust-deps.md # Rust crate reference
│ └── FAQ.md # Troubleshooting & common issues
└── README.md # This file
# Solana core. zk-sdk is at 6.0.1 because the deployed devnet ZK ElGamal Proof
# program now expects 6.0.1-format proofs.
solana-sdk = "3.0.0"
solana-client = "3.1.6"
solana-zk-sdk = "6.0.1"
# SPL Token-2022. proof-extraction stays at 0.5.1 (matches spl-token-2022's
# transitive zk-sdk 4.0) so we can name the legacy `ProofLocation` type at the
# instruction boundary. proof-generation is on 0.6.0 (zk-sdk 6.0.1) for the
# actual proof bytes.
spl-token-2022 = "10.0.0"
spl-token-client = "0.18.0"
spl-associated-token-account = "8.0.0"
spl-token-confidential-transfer-proof-generation = "0.6.0"
spl-token-confidential-transfer-proof-extraction = "0.5.1"
# Bypass-mode helpers (see "Bypass mode" below)
solana-zk-elgamal-proof-interface = "0.1.2"
solana-zk-sdk-pod = "0.1.1"
solana-address = "2.6"
bytemuck = "1.25"This split is a stopgap, not a design choice. spl-token-client and
spl-token-2022 should be on the agave v4 beta / rc crates (which target
solana-zk-sdk = 6.0.1 natively), but those Agave v4 crates haven't been
published yet. Until they are, spl-token-2022 = 10.0.0 transitively pulls
solana-zk-sdk = 4.0 for its public API while the deployed ZK ElGamal Proof
program on devnet verifies 6.0.1-format proofs. This repo bridges the gap:
- Derive ElGamal + AES keys with
solana-zk-sdk = 6.0.1directly. - Generate proofs with
proof-generation = 0.6.0. - Pre-verify each proof into a
ProofContextStateaccount. - Reference those accounts in spl-token-2022's instruction builders via
ProofLocation::ContextStateAccount— a phantom-typedPubkeythat carries no proof bytes, so the version mismatch never crosses the FFI. - Cross the type boundary only at on-chain PODs (ElGamal pubkey/ciphertext, AES ciphertext) using zero-copy byte casts, since their wire format is identical between 4.0 and 6.0.1.
Every module in src/ follows this pattern; *Legacy aliases mark the
4.0 side of the boundary. Once the v4 crates are published, the bypass and
the *Legacy aliases can be deleted and everything routes through 6.0.1
directly.
- Solana CLI 2.1.13+ (
solana --version) - SPL Token CLI 5.1.0+ (
spl-token --version) - Rust 1.70+
This repository includes a complete Rust implementation of all confidential transfer operations:
# Start local test validator
solana-test-validator --quiet --reset &
# Run all integration tests
cargo test --test integration_test
# Run a specific test
cargo test test_confidential_transfer_between_accounts -- --nocapture
# Run end-to-end transfer example (shows balance changes throughout)
SOLANA_RPC_URL=https://zk-edge.surfnet.dev:8899 \
PAYER_KEYPAIR=$(cat ~/.config/solana/id.json) \
cargo run --example run_transfer
# Query and display encrypted balances
SOLANA_RPC_URL=https://zk-edge.surfnet.dev:8899 \
MINT_ADDRESS=<mint> \
OWNER_KEYPAIR=$(cat ~/.config/solana/id.json) \
cargo run --example get_balancesAvailable Operations:
src/configure.rs- Configure token accounts for confidential transferssrc/deposit.rs- Deposit from public to confidential balancesrc/apply_pending.rs- Apply pending balance to available balancesrc/withdraw.rs- Withdraw from confidential to public balancesrc/transfer.rs- Transfer confidentially between accounts (with proof context state accounts)
Examples:
examples/run_transfer.rs- Complete end-to-end transfer with balance display at each stepexamples/get_balances.rs- Query and decrypt all balance types (public, pending, available)
All operations are tested in tests/integration_test.rs with complete end-to-end flows.
# Run the official confidential transfer example script
curl -sSf https://raw.githubusercontent.com/solana-program/token-2022/main/clients/cli/examples/confidential-transfer.sh | bashThe demo-server binary wraps the modules above in a small HTTP API so a webapp
deck can drive a live confidential transfer on stage. Single-tenant, in-memory,
all keypairs in .env.
It's the backend for the zkproof8 talk slide deck: gitteri/zkproof8-talk — the webapp there calls these endpoints to drive the live transfer.
One-time setup:
# Generate a fresh .env with five keypairs (PAYER / MINT / SENDER / RECEIVER / AUDITOR)
cargo run --bin demo-server -- generate-env > .env
# The output prints PAYER pubkey to stderr — fund it.
solana airdrop 5 <PAYER_PUBKEY> --url https://api.devnet.solana.comRun the server:
cargo run --bin demo-server
# listens on http://localhost:8088Endpoints:
| Method | Path | Body | Notes |
|---|---|---|---|
| GET | /demo/health |
{ ok, validator_reachable, mint, port, rpc_url } |
|
| GET | /demo/state |
full ledger snapshot for the four-column slide | |
| POST | /demo/init |
idempotent: mint if missing, configure ATAs, top up sender | |
| POST | /demo/transfer |
{ "amount_ui": 250000 } opt. |
runs the full confidential transfer flow |
| POST | /demo/apply-pending |
{ "account": "sender"|"receiver" } |
moves pending balance to available |
SOLANA_RPC_URL selects devnet or local (surfpool, etc). All demo state
resets when keypairs in .env are rotated; soft reset on devnet just re-runs
/demo/init.
┌─────────────────────────────────────────────────────────────┐
│ CONFIDENTIAL TRANSFER FLOW │
├─────────────────────────────────────────────────────────────┤
│ │
│ Sender Recipient │
│ │ │ │
│ │ 1. Deposit (public → pending) │ │
│ │ ──────────────────────────► │ │
│ │ │ │
│ │ 2. Apply (pending → available) │ │
│ │ ──────────────────────────► │ │
│ │ │ │
│ │ 3. Transfer (with ZK proofs) │ │
│ │ ─────────────────────────────────────────► │
│ │ │ │
│ │ 4. Apply │ │
│ │ (pending → │ │
│ │ available) │ │
│ │ │ │
│ │ 5. Withdraw │ │
│ │ (available → │ │
│ │ public) │ │
│ │
└─────────────────────────────────────────────────────────────┘
| Balance Type | Visibility | Purpose |
|---|---|---|
| Public | Visible on-chain | Standard SPL token balance |
| Pending | Encrypted | Incoming transfers waiting to be applied |
| Available | Encrypted | Usable confidential balance for transfers |
Each confidential token account has two encryption keys derived from the owner's signature:
- ElGamal Keypair - Used for transfer encryption (derived from signing
"ElGamalSecretKey") - AES Key - Used for balance decryption (derived from signing
"AeKey")
| Proof Type | Purpose | Size |
|---|---|---|
| Equality Proof | Proves two ciphertexts encrypt the same value | Small |
| Ciphertext Validity | Proves ciphertexts are properly generated | Small |
| Range Proof | Proves value is in range [0, u64::MAX] | Large |
Proof Context State Accounts: To avoid transaction size limitations, each
proof is pre-verified into a temporary on-chain account and the transfer
instruction references it via ProofLocation::ContextStateAccount. The
implementation in src/transfer.rs packs the full flow into 3
transactions:
- Tx 1: create all three proof accounts (equality / validity / range) and verify the validity proof.
- Tx 2: verify the range proof on its own — ~1006-byte ix, the binding constraint on transaction size.
- Tx 3: verify the equality proof, run
inner_transfer, and close all three proof accounts to reclaim rent.
The proof accounts use the payer (not the sender) as the context-state
authority. This keeps the sender out of the verify txs' account_keys,
saving 32 bytes per tx — the difference between fitting and overflowing the
1232-byte legacy tx-size limit on the range-verify tx. The sender still
signs Tx 3 because it's the transfer's token-account authority.
- Solana Program: Confidential Balances - Comprehensive guide
- Anza: ZK ElGamal Proof Program - Proof verification details
- SPL Token Confidential Transfer Overview - Protocol overview
- Token CLI Quickstart - Get started with CLI
- QuickNode: Token-2022 Confidential Guide - Step-by-step implementation
- Token-2022 Program Documentation - Extension system overview
- Token-2022 Program - Main program source
- ZK ElGamal Proof Program - Proof verification program
- Confidential Balances Sample - Rust implementation examples
- Confidential Balances Microsite - Interactive web example
- Product Guide - Understanding the product from a high level
- Wallet Integration - Integration patterns for wallet developers
- Token Extensions Architecture - Token-2022 program-level details
- Cryptography Reference - Deep dive into the crypto primitives
- Rust Dependencies - Using the Rust crates
- JS/WASM Clients - JavaScript and WASM SDK reference
- FAQ & Troubleshooting - Common issues and solutions
This workshop material is provided for educational purposes.