docs: Rewrite confidential transfer examples for Token-2022 v11#1574
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
5 Skipped Deployments
|
|
@gitteri is attempting to deploy a commit to the Solana Foundation Team on Vercel. A member of the Team first needs to authorize it. |
…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).
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.
|
@greptile review The decrypt_u32 range-limit findings on On the remaining transfer note: |
…itor elgamal pubkey
| 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); |
There was a problem hiding this comment.
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.
| 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); |
| // 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(); |
There was a problem hiding this comment.
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).
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 inlineInstructionOffsetproofs. 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, plusgetCreateMintInstructionPlanandgetConfidentialDepositInstructionfor 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 thanNone), 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 stocksolana-test-validator.Notes
Cargo.tomlpins thesolana-client4.0.0-rc.0line plus granular crates rather than thesolana-sdkumbrella: stablesolana-client 4.0.0capssolana-instructionbelow what token-2022 v11 (solana-system-interface 3.2) needs. Each manifest has a comment noting it collapses back tosolana-sdkonce a compatible stable client ships.@solana/kitclient/instruction-plan setup is left to the reader's standard kit setup.Follow-ups left out: the
index.mdxoverview andissuer-guide.mdx(and the newintegration-guide.mdxthat just landed on main), plus a few proof-type doc links that still point at the renamedzk-token-sdkcrate.