如何使用机密转账扩展创建 token account
机密转账扩展通过向 token account 添加额外状态来实现私密代币转账。本节将介绍如何创建启用此扩展的 token account。
下图展示了使用机密转账扩展创建 token account 所涉及的步骤:
机密转账 Token Account 状态
该扩展将 ConfidentialTransferAccount 状态添加到 token account:
#[repr(C)]#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]pub struct ConfidentialTransferAccount {/// `true` if this account has been approved for use. All confidential/// transfer operations for the account will fail until approval is/// granted.pub approved: PodBool,/// The public key associated with ElGamal encryptionpub elgamal_pubkey: PodElGamalPubkey,/// The low 16 bits of the pending balance (encrypted by `elgamal_pubkey`)pub pending_balance_lo: EncryptedBalance,/// The high 48 bits of the pending balance (encrypted by `elgamal_pubkey`)pub pending_balance_hi: EncryptedBalance,/// The available balance (encrypted by `encryption_pubkey`)pub available_balance: EncryptedBalance,/// The decryptable available balancepub decryptable_available_balance: DecryptableBalance,/// If `false`, the extended account rejects any incoming confidential/// transferspub allow_confidential_credits: PodBool,/// If `false`, the base account rejects any incoming transferspub allow_non_confidential_credits: PodBool,/// The total number of `Deposit` and `Transfer` instructions that have/// credited `pending_balance`pub pending_balance_credit_counter: PodU64,/// The maximum number of `Deposit` and `Transfer` instructions that can/// credit `pending_balance` before the `ApplyPendingBalance`/// instruction is executedpub maximum_pending_balance_credit_counter: PodU64,/// The `expected_pending_balance_credit_counter` value that was included in/// the last `ApplyPendingBalance` instructionpub expected_pending_balance_credit_counter: PodU64,/// The actual `pending_balance_credit_counter` when the last/// `ApplyPendingBalance` instruction was executedpub actual_pending_balance_credit_counter: PodU64,}
ConfidentialTransferAccount 包含多个用于管理机密转账的字段:
-
approved:账户的机密转账审批状态。如果 mint account 的
auto_approve_new_accounts配置设置为true,则所有 token account 将自动获得机密转账授权。 -
elgamal_pubkey:用于加密余额和转账金额的 ElGamal 公钥。
-
pending_balance_lo:待处理余额的低 16 位加密值。余额被拆分为高位和低位以提高解密效率。
-
pending_balance_hi:待处理余额的高 48 位加密值。余额被拆分为高位和低位以提高解密效率。
-
available_balance:可用于转账的加密余额。
-
decryptable_available_balance:使用高级加密标准(AES)密钥加密的可用余额,便于账户所有者高效解密。
-
allow_confidential_credits:若为 true,则允许接收机密转账。
-
allow_non_confidential_credits:若为 true,则允许接收非机密转账。
-
pending_balance_credit_counter:统计来自存款和转账指令的待处理余额入账次数。
-
maximum_pending_balance_credit_counter:在需要执行
ApplyPendingBalance指令将待处理余额转换为可用余额之前,允许的待处理入账次数上限。 -
expected_pending_balance_credit_counter:上次处理
ApplyPendingBalance指令时,客户端通过 instruction data 提供的pending_balance_credit_counter值。 -
actual_pending_balance_credit_counter:处理最后一条
ApplyPendingBalance指令时,token account 上的pending_balance_credit_counter值。
待处理余额与可用余额
机密余额分为待处理余额和可用余额,以防止 DoS 攻击。若没有这种分离,攻击者可以反复向 token account 发送代币,阻止 token account 所有者转移代币。由于在提交交易和处理交易之间,加密余额会发生变化,导致交易失败,token account 所有者将无法转移代币。
所有存款和转账金额最初都会添加到待处理余额中。Token account 所有者必须使用
ApplyPendingBalance
指令将待处理余额转换为可用余额。incoming 转账或存款不会影响 token
account 的可用余额。
待处理余额信用计数器
调用 ApplyPendingBalance 指令将待处理余额转换为可用余额时:
-
客户端查询当前待处理余额和可用余额,对其总和进行加密,并提供一个使用 token account 所有者 AES 密钥加密的
decryptable_available_balance。 -
预期信用计数器和实际信用计数器用于追踪从创建
ApplyPendingBalance指令到处理该指令期间计数器值的变化:expected_pending_balance_credit_counter:客户端创建ApplyPendingBalance指令时的pending_balance_credit_counter值actual_pending_balance_credit_counter:处理ApplyPendingBalance指令时,token account 上的pending_balance_credit_counter值
匹配的预期/实际计数器表明 decryptable_available_balance 与 available_balance
相符。
在获取 token account 状态以读取 decryptable_available_balance
时,若预期/实际计数器值不同,客户端需查找与计数器差值匹配的最近存款/转账指令,以计算正确余额。
余额对账流程
当预期与实际待处理余额计数器不一致时,请按以下步骤对账
decryptable_available_balance:
- 从 token account 获取
decryptable_available_balance - 获取最近的交易,包括存款和转账指令,直至计数器差值(实际 - 预期):
- 累加存款指令中的公开金额
- 解密并累加转账指令中的目标密文金额
所需指令
创建和配置用于保密转账的 token account 需使用以下指令,这些指令均可在单笔交易中完成:
-
创建 Token Account:调用 Associated Token Program 的
AssociatedTokenAccountInstruction::Create指令,在其确定性地址处创建token account。 -
重新分配账户空间:调用 Token Extension Program 的
TokenInstruction::Reallocate指令,为ConfidentialTransferAccount状态添加所需空间。 -
验证 Pubkey 有效性证明:创建一个由 ZK ElGamal Proof 程序所有的账户,然后调用其
VerifyPubkeyValidity指令以验证证明并将验证结果存储在该上下文状态账户中。 -
配置保密转账:调用 Token Extension Program 的 ConfidentialTransferInstruction::ConfigureAccount 指令,通过
ProofLocation::ContextStateAccount引用证明上下文状态账户,以初始化ConfidentialTransferAccount状态。
只有 token account 的所有者可以为 token account 配置保密转账功能。
ConfigureAccount 指令需要客户端生成加密密钥及证明,且该证明只能由 token
account 所有者生成。
pubkey 有效性证明用于验证账户的 ElGamal 公钥是否有效。它通过
build_pubkey_validity_proof_data 生成,由链上的 ZK ElGamal
Proof 程序验证并存入上下文状态账户,再通过
ProofLocation::ContextStateAccount 从 ConfigureAccount
中引用,因此 token 指令本身不携带任何证明字节。如需了解实现细节,请参阅:
示例代码
以下代码创建一个 associated token account,并将其配置为针对现有保密铸币的保密转账。
保密转账依赖于 ZK ElGamal Proof 程序,该程序已在主网和开发网上启用。标准的
solana-test-validator 不会启用它,但主网分叉本地 validator(例如
Surfpool)会启用。请使用已充值的付款方账户在上述环境之一中运行示例(代码使用开发网),并将铸币占位符替换为按照
创建铸币
创建的铸币。
Rust
// The native ZK ElGamal Proof program verifies the proof on chain.const ZK_PROOF_PROGRAM_ID: Pubkey =solana_pubkey::pubkey!("ZkE1Gama1Proof11111111111111111111111111111");fn main() -> Result<()> {// Use a cluster whose ZK ElGamal Proof program is enabled (mainnet, devnet).let rpc_client = RpcClient::new_with_commitment(String::from("https://api.devnet.solana.com"),CommitmentConfig::confirmed(),);// The Solana CLI default keypair, used as fee payer, mint authority, and// token account owner.let payer = load_keypair()?;let decimals: u8 = 2;// Setup: create a confidential mint for the token account.let mint = create_confidential_mint(&rpc_client, &payer, decimals)?;let token_account = get_associated_token_address_with_program_id(&payer.pubkey(),&mint,&spl_token_2022::id(),);// 1. Create the associated token account.let create_ata_ix = create_associated_token_account(&payer.pubkey(), // funding account&payer.pubkey(), // token account owner&mint,&spl_token_2022::id(),);// 2. Add space for the ConfidentialTransferAccount extension.let realloc_ix = reallocate(&spl_token_2022::id(),&token_account,&payer.pubkey(), // payer&payer.pubkey(), // owner&[&payer.pubkey()],&[ExtensionType::ConfidentialTransferAccount],)?;// 3. Derive the owner's ElGamal keypair and AES key from a signature over// the token account address. The same signer and address always derive// the same keys, so the owner can recover them from their wallet.let (elgamal_keypair, aes_key) = derive_confidential_keys(&payer, &token_account.to_bytes()).map_err(|e| anyhow::anyhow!("derive confidential keys: {e}"))?;// Initial decryptable available balance of 0, encrypted with the AES key.let decryptable_balance: PodAeCiphertext = aes_key.encrypt(0).into();let maximum_pending_balance_credit_counter: u64 = 65_536;// 4. Generate the pubkey-validity proof, then pre-verify it into a context// state account owned by the ZK ElGamal Proof program. configure_account// references the verified proof by account, so no proof bytes travel in// the token instruction itself.let proof_data = build_pubkey_validity_proof_data(&elgamal_keypair).map_err(|e| anyhow::anyhow!("generate pubkey validity proof: {e}"))?;let proof_account = Keypair::new();let context_state_size = size_of::<ProofContextState<PubkeyValidityProofContext>>();let context_state_rent =rpc_client.get_minimum_balance_for_rent_exemption(context_state_size)?;let create_proof_account_ix = system_instruction::create_account(&payer.pubkey(),&proof_account.pubkey(),context_state_rent,context_state_size as u64,&ZK_PROOF_PROGRAM_ID,);let proof_account_address: Address = proof_account.pubkey().to_bytes().into();let owner_address: Address = payer.pubkey().to_bytes().into();let verify_proof_ix = ProofInstruction::VerifyPubkeyValidity.encode_verify_proof(Some(ContextStateInfo {context_state_account: &proof_account_address,context_state_authority: &owner_address,}),&proof_data,);// 5. Configure the account, pointing at the pre-verified proof account.let proof_location: ProofLocation<PubkeyValidityProofData> =ProofLocation::ContextStateAccount(&proof_account.pubkey());let configure_account_ixs = configure_account(&spl_token_2022::id(),&token_account,&mint,&decryptable_balance,maximum_pending_balance_credit_counter,&payer.pubkey(), // owner&[],proof_location,)?;// Everything fits in a single transaction.let mut instructions = vec![create_ata_ix,realloc_ix,create_proof_account_ix,verify_proof_ix,];instructions.extend(configure_account_ixs);let blockhash = rpc_client.get_latest_blockhash()?;let transaction = Transaction::new_signed_with_payer(&instructions,Some(&payer.pubkey()),&[&payer, &proof_account],blockhash,);let signature = rpc_client.send_and_confirm_transaction(&transaction)?;println!("Configured token account {token_account} for confidential transfers: {signature}");Ok(())}
Typescript
const client = await createClient().use(signerFromFile(join(homedir(), ".config/solana/id.json"))).use(solanaRpc({rpcUrl: "https://api.devnet.solana.com"}));// The Solana CLI default keypair, used as fee payer, mint authority, and// token account owner.const owner = client.payer;const decimals = 2;// Setup: create a confidential mint for the token account.const mint = await createConfidentialMint(client, owner, decimals);const [tokenAccount] = await findAssociatedTokenPda({owner: owner.address,tokenProgram: TOKEN_2022_PROGRAM_ADDRESS,mint});// Derive recoverable ElGamal and AES keys bound to (owner, mint). Re-deriving// from the same wallet always yields the same keys, so the owner can recover// them rather than having to back up a separate secret.const derivedElGamal = await deriveElGamalKeypairForOwnerMint({signer: owner,owner: owner.address,mint});const elgamalKeypair = ElGamalKeypair.fromSecretKey(ElGamalSecretKey.fromBytes(derivedElGamal.secretKey));const aesKey = AeKey.fromBytes(await deriveAeKeyForOwnerMint({ signer: owner, owner: owner.address, mint }));// Build the create-ATA + reallocate + verify-proof + configure plan, then send.// The helper returns an instruction plan because the steps may span more than// one transaction.const plan = await getCreateConfidentialTransferAccountInstructionPlan({rpc: client.rpc,payer: owner,owner,mint,elgamalKeypair,aesKey});const result = await client.sendTransaction(plan);console.log(`Configured token account ${tokenAccount} for confidential transfers: ${result.context.signature}`);
TypeScript 辅助函数位于 @solana-program/token-2022/confidential
子路径中,并基于 @solana/zk-sdk 提供加密原语。owner 和 client 来自您的
@solana/kit 配置;返回的指令计划 通过 @solana/kit
的指令计划支持发送,当证明数据过大无法放入单笔交易时,将自动拆分到多笔交易中执行。
Is this page helpful?