创建一个 Token 账户
如何使用机密转账扩展创建一个 Token 账户
机密转账扩展通过向 Token 账户添加额外状态来实现私密的 Token 转账。本节将解释如何启用此扩展来创建一个 Token 账户。
下图展示了使用机密转账扩展创建 Token 账户的步骤:
机密转账 Token 账户状态
该扩展向 Token 账户添加了 ConfidentialTransferAccount 状态:
#[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 账户的
auto_approve_new_accounts
配置设置为true
,则所有 Token 账户会自动获得机密转账的审批。 -
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:客户端通过指令数据提供的
pending_balance_credit_counter
值,上次处理ApplyPendingBalance
指令时的值。 -
actual_pending_balance_credit_counter:在上次处理
ApplyPendingBalance
指令时,token account 上的pending_balance_credit_counter
值。
待处理余额与可用余额
机密余额分为待处理余额和可用余额,以防止 DoS 攻击。如果没有这种区分,攻击者可以反复向 token account 发送代币,从而阻止 token account 所有者转移代币。由于在交易提交和处理之间加密余额会发生变化,导致交易失败,token account 所有者将无法转移代币。
所有存款和转账金额最初都会添加到待处理余额中。token account 所有者必须使用
ApplyPendingBalance
指令将待处理余额转换为可用余额。传入的转账或存款不会影响 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
状态添加空间。 -
配置机密转账:调用 Token Extension Program 的 ConfidentialTransferInstruction::ConfigureAccount 指令以初始化
ConfidentialTransferAccount
状态。
只有 token account 的所有者可以为机密转账配置 token account。
ConfigureAccount
指令需要客户端生成加密密钥和证明数据,这些只能由 token
account 的所有者生成。
PubkeyValidityProofData
创建一个证明,用于验证 ElGamal 密钥的有效性。有关实现细节,请参阅:
示例代码
以下代码演示了如何使用机密转账扩展创建一个 Associated Token Account,
要运行此示例,请使用以下命令启动一个从主网克隆的 Token Extension Program 的本地 validator。您必须安装 Solana CLI 才能启动本地 validator。
$solana-test-validator --clone-upgradeable-program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb --url https://api.mainnet-beta.solana.com -r
在撰写本文时,机密转账功能尚未在默认的本地 validator 上启用。您需要克隆主网的 Token Extension Program 才能运行示例代码。
use anyhow::{Context, Result};use solana_client::nonblocking::rpc_client::RpcClient;use solana_sdk::{commitment_config::CommitmentConfig,signature::{Keypair, Signer},transaction::Transaction,};use spl_associated_token_account::{get_associated_token_address_with_program_id, instruction::create_associated_token_account,};use spl_token_client::{client::{ProgramRpcClient, ProgramRpcClientSendTransaction},spl_token_2022::{extension::{confidential_transfer::instruction::{configure_account, PubkeyValidityProofData},ExtensionType,},id as token_2022_program_id,instruction::reallocate,solana_zk_sdk::encryption::{auth_encryption::*, elgamal::*},},token::{ExtensionInitializationParams, Token},};use spl_token_confidential_transfer_proof_extraction::instruction::{ProofData, ProofLocation};use std::sync::Arc;#[tokio::main]async fn main() -> Result<()> {// Create connection to local test validatorlet rpc_client = Arc::new(RpcClient::new_with_commitment(String::from("http://localhost:8899"),CommitmentConfig::confirmed(),));// Load the default Solana CLI keypair to use as the fee payer// This will be the wallet paying for the transaction fees// Use Arc to prevent multiple clones of the keypairlet payer = Arc::new(load_keypair()?);println!("Using payer: {}", payer.pubkey());// Generate a new keypair to use as the address of the token mintlet mint = Keypair::new();println!("Mint keypair generated: {}", mint.pubkey());// Set up program client for Token clientlet program_client = ProgramRpcClient::new(rpc_client.clone(), ProgramRpcClientSendTransaction);// Number of decimals for the mintlet decimals = 9;// Create a token client for the Token-2022 program// This provides high-level methods for token operationslet token = Token::new(Arc::new(program_client),&token_2022_program_id(), // Use the Token-2022 program (newer version with extensions)&mint.pubkey(), // Address of the new token mintSome(decimals), // Number of decimal placespayer.clone(), // Fee payer for transactions (cloning Arc, not keypair));// Create extension initialization parameters for the mint// The ConfidentialTransferMint extension enables confidential (private) transfers of tokenslet extension_initialization_params =vec![ExtensionInitializationParams::ConfidentialTransferMint {authority: Some(payer.pubkey()), // Authority that can modify confidential transfer settingsauto_approve_new_accounts: true, // Automatically approve new confidential accountsauditor_elgamal_pubkey: None, // Optional auditor ElGamal public key}];// Create and initialize the mint with the ConfidentialTransferMint extension// This sends a transaction to create the new token mintlet transaction_signature = token.create_mint(&payer.pubkey(), // Mint authority - can mint new tokensSome(&payer.pubkey()), // Freeze authority - can freeze token accountsextension_initialization_params, // Add the ConfidentialTransferMint extension&[&mint], // Mint keypair needed as signer).await?;println!("Mint Address: {}", mint.pubkey());println!("Mint Creation Transaction Signature: {}",transaction_signature);// ===== Create and configure token account for confidential transfers =====println!("\nCreate and configure token account for confidential transfers");// Get the associated token account address for the ownerlet token_account_pubkey = get_associated_token_address_with_program_id(&payer.pubkey(), // Token account owner&mint.pubkey(), // Mint&token_2022_program_id(), // Token program ID);println!("Token Account Address: {}", token_account_pubkey);// Step 1: Create the associated token accountlet create_associated_token_account_instruction = create_associated_token_account(&payer.pubkey(), // Funding account&payer.pubkey(), // Token account owner&mint.pubkey(), // Mint&token_2022_program_id(), // Token program ID);// Step 2: Reallocate the token account to include space for the ConfidentialTransferAccount extensionlet reallocate_instruction = reallocate(&token_2022_program_id(), // Token program ID&token_account_pubkey, // Token account&payer.pubkey(), // Payer&payer.pubkey(), // Token account owner&[&payer.pubkey()], // Signers&[ExtensionType::ConfidentialTransferAccount], // Extension to reallocate space for)?;// Step 3: Generate the ElGamal keypair and AES key for token accountlet elgamal_keypair = ElGamalKeypair::new_from_signer(&payer, &token_account_pubkey.to_bytes()).expect("Failed to create ElGamal keypair");let aes_key = AeKey::new_from_signer(&payer, &token_account_pubkey.to_bytes()).expect("Failed to create AES key");// The maximum number of Deposit and Transfer instructions that can// credit pending_balance before the ApplyPendingBalance instruction is executedlet maximum_pending_balance_credit_counter = 65536;// Initial token balance is 0let decryptable_balance = aes_key.encrypt(0);// Generate the proof data client-sidelet proof_data = PubkeyValidityProofData::new(&elgamal_keypair).map_err(|_| anyhow::anyhow!("Failed to generate proof data"))?;// Indicate that proof is included in the same transactionlet proof_location =ProofLocation::InstructionOffset(1.try_into()?, ProofData::InstructionData(&proof_data));// Step 4: Create instructions to configure the account for confidential transferslet configure_account_instructions = configure_account(&token_2022_program_id(), // Program ID&token_account_pubkey, // Token account&mint.pubkey(), // Mint&decryptable_balance.into(), // Initial balancemaximum_pending_balance_credit_counter, // Maximum pending balance credit counter&payer.pubkey(), // Token Account Owner&[], // Additional signersproof_location, // Proof location)?;// Combine all instructionslet mut instructions = vec![create_associated_token_account_instruction,reallocate_instruction,];instructions.extend(configure_account_instructions);// Create and send the transactionlet recent_blockhash = rpc_client.get_latest_blockhash().await?;let transaction = Transaction::new_signed_with_payer(&instructions,Some(&payer.pubkey()),&[&payer],recent_blockhash,);let transaction_signature = rpc_client.send_and_confirm_transaction(&transaction).await?;println!("Create Token Account Transaction Signature: {}",transaction_signature);Ok(())}// Load the keypair from the default Solana CLI keypair path (~/.config/solana/id.json)// This enables using the same wallet as the Solana CLI toolsfn load_keypair() -> Result<Keypair> {// Get the default keypair pathlet keypair_path = dirs::home_dir().context("Could not find home directory")?.join(".config/solana/id.json");// Read the keypair file directly into bytes using serde_json// The keypair file is a JSON array of byteslet file = std::fs::File::open(&keypair_path)?;let keypair_bytes: Vec<u8> = serde_json::from_reader(file)?;// Create keypair from the loaded bytes// This converts the byte array into a keypairlet keypair = Keypair::from_bytes(&keypair_bytes)?;Ok(keypair)}
Is this page helpful?