Skip to content

docs: Rewrite confidential transfer examples for Token-2022 v11#1574

Merged
ZYJLiu merged 11 commits into
solana-foundation:mainfrom
gitteri:docs-conf-transfer-v11-examples
Jun 24, 2026
Merged

docs: Rewrite confidential transfer examples for Token-2022 v11#1574
ZYJLiu merged 11 commits into
solana-foundation:mainfrom
gitteri:docs-conf-transfer-v11-examples

Conversation

@gitteri

@gitteri gitteri commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Now that Token-2022 v11 is live on mainnet, this regrounds the confidential transfer docs in the confidential-balances-exploration reference. The old examples were on a two-major-versions-old stack (solana-sdk 2.2 / spl-token-client 0.14) and several used inline proofs, so they no longer match the deployed program.

Pages updated (the full operation set)

create-mint, create-token-account, deposit, apply-pending-balance, transfer, withdraw. Each now has both a Rust tab and a TypeScript tab.

Rust: raw instruction builders on the granular v11 crate stack. Where proofs are involved (configure, transfer, withdraw) they're pre-verified into context state accounts via the ZK ElGamal Proof program and referenced with ProofLocation::ContextStateAccount, replacing the old inline InstructionOffset proofs. Every Rust example compiles against the v11 reference.

TypeScript: the new helpers from @solana-program/token-2022/confidential (getCreateConfidentialTransferAccountInstructionPlan, getConfidentialTransferInstructionPlan, getConfidentialWithdrawInstructionPlan, getApplyConfidentialPendingBalanceInstructionFromToken) with recoverable ElGamal/AES key derivation, plus getCreateMintInstructionPlan and getConfidentialDepositInstruction for the proof-less operations. Every TS example typechecks against the published packages.

Also updated the proof-flow prose/diagrams, removed a stale duplicate [dependencies] block on create-mint, switched create-mint to configure an auditor (rather than None), and replaced the run instructions: confidential transfers need the ZK ElGamal Proof program, which is enabled on mainnet, devnet, and a mainnet-forking local validator like Surfpool, but not the stock solana-test-validator.

Notes

  • The Rust Cargo.toml pins the solana-client 4.0.0-rc.0 line plus granular crates rather than the solana-sdk umbrella: stable solana-client 4.0.0 caps solana-instruction below what token-2022 v11 (solana-system-interface 3.2) needs. Each manifest has a comment noting it collapses back to solana-sdk once a compatible stable client ships.
  • TS examples are focused on the confidential-transfer logic; the @solana/kit client/instruction-plan setup is left to the reader's standard kit setup.
  • English source only; translations regenerate.

Follow-ups left out: the index.mdx overview and issuer-guide.mdx (and the new integration-guide.mdx that just landed on main), plus a few proof-type doc links that still point at the renamed zk-token-sdk crate.

Reground the create-token-account, transfer, and withdraw pages in the
confidential-balances-exploration reference now that Token-2022 v11 is live on
mainnet. Replaces the outdated examples (solana-sdk 2.2 / spl-token-client 0.14,
inline proofs) with the v11 context-state-account proof flow.

Each page now has both a Rust tab and a TypeScript tab:
- Rust: raw instruction builders on the granular v11 crate stack (proofs
  pre-verified into context state accounts via the ZK ElGamal Proof program).
  Every example compiles against the v11 reference.
- TypeScript: the new helpers from @solana-program/token-2022/confidential
  (getCreateConfidentialTransferAccountInstructionPlan,
  getConfidentialTransferInstructionPlan, getConfidentialWithdrawInstructionPlan)
  with recoverable key derivation. Every example typechecks against the
  published packages.

Also updates the proof-flow prose/diagram and the run instructions: confidential
transfers need the ZK ElGamal Proof program, available on mainnet, devnet, and a
mainnet-forking local validator like Surfpool, but not the stock
solana-test-validator.
@vercel

vercel Bot commented Jun 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
solana-com-docs Ready Ready Preview, Comment Jun 19, 2026 7:47pm
5 Skipped Deployments
Project Deployment Actions Updated (UTC)
solana-com Skipped Skipped Jun 19, 2026 7:47pm
solana-com-accelerate Skipped Skipped Jun 19, 2026 7:47pm
solana-com-breakpoint-2 Skipped Skipped Jun 19, 2026 7:47pm
solana-com-media Skipped Skipped Jun 19, 2026 7:47pm
templates Skipped Skipped Jun 19, 2026 7:47pm

Request Review

@vercel vercel Bot temporarily deployed to Preview – solana-com-breakpoint-2 June 17, 2026 23:28 Inactive
@vercel vercel Bot temporarily deployed to Preview – solana-com June 17, 2026 23:28 Inactive
@vercel vercel Bot temporarily deployed to Preview – templates June 17, 2026 23:28 Inactive
@vercel vercel Bot temporarily deployed to Preview – solana-com-accelerate June 17, 2026 23:28 Inactive
@vercel vercel Bot temporarily deployed to Preview – solana-com-media June 17, 2026 23:28 Inactive
@vercel

vercel Bot commented Jun 17, 2026

Copy link
Copy Markdown

@gitteri is attempting to deploy a commit to the Solana Foundation Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps

greptile-apps Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR rewrites the confidential transfer documentation to match the Token-2022 v11 stack — replacing two-major-version-old spl-token-client/solana-sdk examples with granular v11 crate builds (Rust) and the new @solana-program/token-2022/confidential helpers (TypeScript). All six operation pages (create-mint, create-token-account, deposit, apply-pending-balance, transfer, withdraw) now have dual Rust + TypeScript tabs, updated proof flows using ProofLocation::ContextStateAccount, and correct AES-based balance decryption throughout.

  • Rust examples use the v11 crate stack with pre-verified context state accounts replacing old inline instruction-offset proofs; balance decryption uses AES throughout.
  • TypeScript examples use the new @solana-program/token-2022/confidential helpers with deterministic ElGamal/AES key derivation.
  • The stale "ZK program temporarily disabled" callouts are removed and replaced with accurate Surfpool guidance.

Confidence Score: 4/5

Safe to merge for the TypeScript and most Rust content; the create-mint Rust example creates a mint with a random, unrecoverable auditor key that should be addressed before the doc goes live.

The create-mint.mdx Rust example calls ElGamalKeypair::new_rand() for the auditor key and never persists it — the private key is silently dropped when main returns. Any developer following the example will have an auditor pubkey on-chain with no matching private key, making the auditor role permanently non-functional. The TypeScript counterparts correctly use deterministic derivation via deriveElGamalKeypairForOwnerMint. All other changes look correct.

apps/docs/content/docs/en/tokens/extensions/confidential-transfer/create-mint.mdx and the collapsed setup helper in create-token-account.mdx

Important Files Changed

Filename Overview
apps/docs/content/docs/en/tokens/extensions/confidential-transfer/create-mint.mdx Rewrites create-mint to Token-2022 v11; Rust example generates an ephemeral random auditor key that is permanently lost on exit.
apps/docs/content/docs/en/tokens/extensions/confidential-transfer/create-token-account.mdx Rewrites create-token-account with v11 crates and ContextStateAccount proof flow; collapsed helper also uses ephemeral auditor key.
apps/docs/content/docs/en/tokens/extensions/confidential-transfer/apply-pending-balance.mdx Rewritten to v11 stack; correctly uses AES decryption throughout both main and helper functions.
apps/docs/content/docs/en/tokens/extensions/confidential-transfer/deposit-tokens.mdx Rewritten to v11 stack with both Rust and TypeScript tabs; deposit requires no ZK proof.
apps/docs/content/docs/en/tokens/extensions/confidential-transfer/transfer-tokens.mdx Major rewrite to v11 with three ContextStateAccount proofs and correct AES balance decryption.
apps/docs/content/docs/en/tokens/extensions/confidential-transfer/withdraw-tokens.mdx Rewritten to v11 with two ContextStateAccount proofs; correctly uses AES decryption throughout.
apps/docs/content/docs/en/tokens/extensions/confidential-transfer/index.mdx Overview page updated; stale warning callouts removed.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Wallet
    participant ZKProg as ZK ElGamal Proof Program
    participant Token22 as Token Extensions Program

    note over Wallet,Token22: create-mint (no ZK needed)
    Wallet->>Token22: InitializeMint + ConfidentialTransferMint extension

    note over Wallet,Token22: create-token-account
    Wallet->>ZKProg: VerifyPubkeyValidity context state account
    Wallet->>Token22: ConfigureAccount (ProofLocation::ContextStateAccount)

    note over Wallet,Token22: deposit (no ZK needed)
    Wallet->>Token22: Deposit public to pending confidential

    note over Wallet,Token22: apply-pending-balance (no ZK needed)
    Wallet->>Wallet: AES-decrypt decryptable_available_balance
    Wallet->>Token22: ApplyPendingBalance pending to available

    note over Wallet,Token22: transfer (3 proofs)
    Wallet->>ZKProg: VerifyBatchedGroupedCiphertext3HandlesValidity
    Wallet->>ZKProg: VerifyBatchedRangeProofU128
    Wallet->>ZKProg: VerifyCiphertextCommitmentEquality
    Wallet->>Token22: Transfer with 3 ContextStateAccounts
    Wallet->>ZKProg: Close 3 proof accounts

    note over Wallet,Token22: withdraw (2 proofs)
    Wallet->>ZKProg: VerifyCiphertextCommitmentEquality
    Wallet->>ZKProg: VerifyBatchedRangeProofU64
    Wallet->>Token22: Withdraw with 2 ContextStateAccounts
    Wallet->>ZKProg: Close 2 proof accounts
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Wallet
    participant ZKProg as ZK ElGamal Proof Program
    participant Token22 as Token Extensions Program

    note over Wallet,Token22: create-mint (no ZK needed)
    Wallet->>Token22: InitializeMint + ConfidentialTransferMint extension

    note over Wallet,Token22: create-token-account
    Wallet->>ZKProg: VerifyPubkeyValidity context state account
    Wallet->>Token22: ConfigureAccount (ProofLocation::ContextStateAccount)

    note over Wallet,Token22: deposit (no ZK needed)
    Wallet->>Token22: Deposit public to pending confidential

    note over Wallet,Token22: apply-pending-balance (no ZK needed)
    Wallet->>Wallet: AES-decrypt decryptable_available_balance
    Wallet->>Token22: ApplyPendingBalance pending to available

    note over Wallet,Token22: transfer (3 proofs)
    Wallet->>ZKProg: VerifyBatchedGroupedCiphertext3HandlesValidity
    Wallet->>ZKProg: VerifyBatchedRangeProofU128
    Wallet->>ZKProg: VerifyCiphertextCommitmentEquality
    Wallet->>Token22: Transfer with 3 ContextStateAccounts
    Wallet->>ZKProg: Close 3 proof accounts

    note over Wallet,Token22: withdraw (2 proofs)
    Wallet->>ZKProg: VerifyCiphertextCommitmentEquality
    Wallet->>ZKProg: VerifyBatchedRangeProofU64
    Wallet->>Token22: Withdraw with 2 ContextStateAccounts
    Wallet->>ZKProg: Close 2 proof accounts
Loading

Reviews (10): Last reviewed commit: "greptile fix" | Re-trigger Greptile

…lance

Same v11 treatment as the other confidential transfer pages, completing the set:
both a Rust tab and a TypeScript tab, on the granular v11 crate stack.

- create-mint: raw initialize-confidential-transfer-mint + initialize-mint
  (Rust) and getCreateMintInstructionPlan with a ConfidentialTransferMint
  extension (TS); now configures an auditor rather than passing None.
- deposit: the deposit instruction (no proof); public -> pending.
- apply-pending-balance: decrypt pending/available, re-encrypt, and
  apply_pending_balance (Rust); getApplyConfidentialPendingBalanceInstructionFromToken
  (TS), which does the decrypt/re-encrypt internally.

Rust examples compile against the v11 reference; TS examples typecheck against
the published packages. Run instructions updated to the same ZK-program note
(mainnet, devnet, or a mainnet-forking local validator like Surfpool).
@vercel vercel Bot temporarily deployed to Preview – templates June 17, 2026 23:52 Inactive
@vercel vercel Bot temporarily deployed to Preview – solana-com-breakpoint-2 June 17, 2026 23:52 Inactive
@vercel vercel Bot temporarily deployed to Preview – solana-com-accelerate June 17, 2026 23:52 Inactive
@vercel vercel Bot temporarily deployed to Preview – solana-com-media June 17, 2026 23:52 Inactive
@vercel vercel Bot temporarily deployed to Preview – solana-com June 17, 2026 23:52 Inactive
Address Greptile review: the withdraw and apply-pending-balance Rust examples
read the available balance from the AES-encrypted decryptable_available_balance
instead of ElGamal decrypt_u32. decrypt_u32 uses baby-step-giant-step and only
recovers values up to 2^32 raw units, so it fails for realistic balances (a
9-decimal token holding a few tokens already exceeds the cap); the AES field has
no range limit. Pending lo/hi stay on decrypt_u32 since they are bounded. Both
examples still compile against the v11 reference.
@vercel vercel Bot temporarily deployed to Preview – solana-com-accelerate June 18, 2026 20:00 Inactive
@vercel vercel Bot temporarily deployed to Preview – templates June 18, 2026 20:00 Inactive
@vercel vercel Bot temporarily deployed to Preview – solana-com-media June 18, 2026 20:00 Inactive
@vercel vercel Bot temporarily deployed to Preview – solana-com June 18, 2026 20:00 Inactive
@vercel vercel Bot temporarily deployed to Preview – solana-com-breakpoint-2 June 18, 2026 20:00 Inactive
@vercel vercel Bot temporarily deployed to Preview – templates June 18, 2026 21:29 Inactive
@gitteri

gitteri commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

@greptile review

The decrypt_u32 range-limit findings on withdraw-tokens.mdx and apply-pending-balance.mdx are fixed (both now read the available balance from the AES-encrypted decryptable_available_balance).

On the remaining transfer note: transfer-tokens.mdx does not contain an apply_pending_balance_for_account helper or any decrypt_u32 call. The transfer example reads the available balance via current_decryptable.decrypt(&sender_aes) (AES) and passes the ElGamal ciphertext to transfer_split_proof_data only for proof generation, so there is no decrypt_u32 range-limit issue in that file.

@vercel vercel Bot temporarily deployed to Preview – templates June 19, 2026 19:15 Inactive
@vercel vercel Bot temporarily deployed to Preview – solana-com-breakpoint-2 June 19, 2026 19:15 Inactive
@vercel vercel Bot temporarily deployed to Preview – solana-com-accelerate June 19, 2026 19:15 Inactive
@vercel vercel Bot temporarily deployed to Preview – solana-com-media June 19, 2026 19:15 Inactive
@vercel vercel Bot temporarily deployed to Preview – solana-com June 19, 2026 19:15 Inactive
@vercel vercel Bot temporarily deployed to Preview – templates June 19, 2026 19:37 Inactive
@vercel vercel Bot temporarily deployed to Preview – solana-com June 19, 2026 19:37 Inactive
@vercel vercel Bot temporarily deployed to Preview – solana-com-accelerate June 19, 2026 19:37 Inactive
@vercel vercel Bot temporarily deployed to Preview – solana-com-media June 19, 2026 19:37 Inactive
@vercel vercel Bot temporarily deployed to Preview – solana-com-breakpoint-2 June 19, 2026 19:37 Inactive
Comment on lines +547 to +561
let available_balance: ElGamalCiphertext = ct_extension
.available_balance
.try_into()
.map_err(|e| anyhow::anyhow!("available_balance: {e:?}"))?;

let pending_lo_amount = pending_lo
.decrypt_u32(elgamal_keypair.secret())
.context("decrypt pending_balance_lo")?;
let pending_hi_amount = pending_hi
.decrypt_u32(elgamal_keypair.secret())
.context("decrypt pending_balance_hi")?;
let current_available = available_balance
.decrypt_u32(elgamal_keypair.secret())
.context("decrypt available_balance")?;
let new_available = current_available + pending_lo_amount + (pending_hi_amount << 16);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 decrypt_u32 on available_balance fails for realistic token amounts. ElGamalCiphertext::decrypt_u32 uses baby-step giant-step and can only recover values up to 2^32 raw units (~4.29 billion). A token with 9 decimals where the holder has just 5 tokens already has 5,000,000,000 raw units, causing decrypt_u32 to return None and the helper to abort at .context("decrypt available_balance")?. The analogous helpers in apply-pending-balance.mdx and withdraw-tokens.mdx were fixed to use AES decryption from decryptable_available_balance, but this helper was not updated consistently.

Suggested change
let available_balance: ElGamalCiphertext = ct_extension
.available_balance
.try_into()
.map_err(|e| anyhow::anyhow!("available_balance: {e:?}"))?;
let pending_lo_amount = pending_lo
.decrypt_u32(elgamal_keypair.secret())
.context("decrypt pending_balance_lo")?;
let pending_hi_amount = pending_hi
.decrypt_u32(elgamal_keypair.secret())
.context("decrypt pending_balance_hi")?;
let current_available = available_balance
.decrypt_u32(elgamal_keypair.secret())
.context("decrypt available_balance")?;
let new_available = current_available + pending_lo_amount + (pending_hi_amount << 16);
let pending_lo_amount = pending_lo
.decrypt_u32(elgamal_keypair.secret())
.context("decrypt pending_balance_lo")? as u64;
let pending_hi_amount = pending_hi
.decrypt_u32(elgamal_keypair.secret())
.context("decrypt pending_balance_hi")? as u64;
// Read the plaintext balance from the AES-encrypted decryptable balance.
// ElGamal's decrypt_u32 only recovers values up to 2^32 raw units, so it
// fails for realistic balances; the AES field has no such limit.
let decryptable_balance: solana_zk_sdk::encryption::auth_encryption::AeCiphertext = ct_extension
.decryptable_available_balance
.try_into()
.map_err(|e| anyhow::anyhow!("decryptable_available_balance: {e:?}"))?;
let current_available = decryptable_balance
.decrypt(&aes_key)
.context("decrypt available balance")?;
let new_available = current_available + pending_lo_amount + (pending_hi_amount << 16);

@vercel vercel Bot temporarily deployed to Preview – solana-com-breakpoint-2 June 19, 2026 19:45 Inactive
@vercel vercel Bot temporarily deployed to Preview – solana-com June 19, 2026 19:45 Inactive
@vercel vercel Bot temporarily deployed to Preview – solana-com-media June 19, 2026 19:45 Inactive
@vercel vercel Bot temporarily deployed to Preview – templates June 19, 2026 19:45 Inactive
@vercel vercel Bot temporarily deployed to Preview – solana-com-accelerate June 19, 2026 19:45 Inactive
Comment on lines +149 to +151
// compliance. Persist this key. Pass `None` to create a mint with no auditor.
let auditor = ElGamalKeypair::new_rand();
let auditor_pubkey: PodElGamalPubkey = (*auditor.pubkey()).into();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Ephemeral auditor key creates a permanently broken mint configuration

ElGamalKeypair::new_rand() generates a random, non-deterministic key. The auditor variable goes out of scope when main returns, and the private key is permanently lost. Any mint created with this example will have an auditor_elgamal_pubkey set in its extension state, but the corresponding secret needed to decrypt auditor ciphertexts is gone — the auditor feature is silently broken.

The TypeScript counterpart (and all the TypeScript helpers throughout this PR) correctly uses deriveElGamalKeypairForOwnerMint, which derives the key deterministically from the signer + mint address, so it can always be re-derived. The Rust example should either use a derivation approach with derive_confidential_keys / a similar HKDF over a wallet signature, or show how to serialize and save the generated secret key before using it. The same pattern also appears in the create_confidential_mint setup helper inside create-token-account.mdx (line 456).

@ZYJLiu ZYJLiu merged commit 3346b41 into solana-foundation:main Jun 24, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants