创建 Token 账户

ZK ElGamal Program 目前在主网和开发网暂时禁用,正在进行安全审计。这意味着 confidential transfers 扩展当前不可用。虽然相关概念依然适用,但代码示例将无法运行。

如何使用 Confidential Transfer 扩展创建 token account

Confidential Transfer 扩展通过为 token account 增加额外状态,实现了私密代币转账。本节将介绍如何启用该扩展并创建 token account。

下图展示了使用 Confidential Transfer 扩展创建 token account 的步骤:

Create Token Account with Confidential Transfer Extension

Confidential Transfer Token Account 状态

该扩展为 token account 增加了 ConfidentialTransferAccount 状态:

Confidential Token Account State
#[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 encryption
pub 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 balance
pub decryptable_available_balance: DecryptableBalance,
/// If `false`, the extended account rejects any incoming confidential
/// transfers
pub allow_confidential_credits: PodBool,
/// If `false`, the base account rejects any incoming transfers
pub 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 executed
pub maximum_pending_balance_credit_counter: PodU64,
/// The `expected_pending_balance_credit_counter` value that was included in
/// the last `ApplyPendingBalance` instruction
pub expected_pending_balance_credit_counter: PodU64,
/// The actual `pending_balance_credit_counter` when the last
/// `ApplyPendingBalance` instruction was executed
pub actual_pending_balance_credit_counter: PodU64,
}

ConfidentialTransferAccount 包含多个字段用于管理 confidential transfers:

  • approved:账户对 confidential transfers 的批准状态。如果 mint account 的 auto_approve_new_accounts 配置设置为 true,所有 token account 都会自动获得 confidential transfers 的批准。

  • 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:客户端上次通过 instruction data 向 ApplyPendingBalance 指令提供的 pending_balance_credit_counter 值。

  • actual_pending_balance_credit_counter:上次处理 ApplyPendingBalance 指令时,token account 上的 pending_balance_credit_counter 值。

待处理余额与可用余额

保密余额分为待处理余额和可用余额,以防止 DoS 攻击。如果没有这种区分,攻击者可能会反复向代币账户发送代币,阻止代币账户所有者转移代币。代币账户所有者将无法转移代币,因为在交易提交和处理之间,加密余额会发生变化,导致交易失败。

所有存款和转账金额最初都会添加到待处理余额中。token account 拥有者必须使用 ApplyPendingBalance 指令将待处理余额转换为可用余额。收到的转账或存款不会影响 token account 的可用余额。

待处理余额高低分割

机密待处理余额被拆分为 pending_balance_lopending_balance_hi,因为 ElGamal 解密在处理较大数字时需要更多计算。你可以在这里找到密文算术的实现,该实现被用于 ApplyPendingBalance 指令这里

待处理余额计数器

调用 ApplyPendingBalance 指令将待处理余额转换为可用余额时:

  1. 客户端查询当前的待处理余额和可用余额,对总和进行加密,并提供一个使用 token account 拥有者 AES 密钥加密的 decryptable_available_balance

  2. 期望和实际的待处理计数器用于跟踪 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_balanceavailable_balance 匹配。

当获取 token account 的状态以读取 decryptable_available_balance 时,如果预期计数器和实际计数器的值不同,客户端需要查找与计数器差值相符的最近存款/转账指令,以计算正确的余额。

余额对账流程

当预期和实际的待处理余额计数器不一致时,请按照以下步骤对 decryptable_available_balance 进行核对:

  1. 从 token account 获取 decryptable_available_balance
  2. 获取包含存款和转账指令的最新交易,直到计数器差值(实际 - 预期):
    • 累加存款指令中的公开金额
    • 解密并累加转账指令中的目标密文金额

所需指令

使用机密转账扩展创建代币账户需要三个指令:

  1. 创建 Token Account:调用 Associated Token Program 的 AssociatedTokenAccountInstruction:Create 指令以创建 token account。

  2. 重新分配账户空间:调用 Token Extension Program 的 TokenInstruction::Reallocate 指令,为 ConfidentialTransferAccount 状态增加空间。

  3. 配置机密转账:调用 Token Extension Program 的 ConfidentialTransferInstruction::ConfigureAccount 指令,初始化 ConfidentialTransferAccount 状态。

只有 token account 的所有者可以为 token account 配置保密转账功能

ConfigureAccount 指令需要客户端生成加密密钥和证明数据,这些只能由 token account 的所有者生成。

PubkeyValidityProofData 会创建一个证明,用于验证 ElGamal 密钥的有效性。实现细节请参见:

示例代码

以下代码演示了如何使用 Confidential Transfer 扩展创建一个 Associated Token Account,

要运行此示例,请使用以下命令启动一个从主网克隆的 Token Extension Program 的本地 validator。您必须安装 Solana CLI 才能启动本地 validator。

Terminal
$
solana-test-validator --clone-upgradeable-program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb --url https://api.mainnet.solana.com -r

在撰写本文时,默认的本地 validator 尚未启用 Confidential Transfers。您必须克隆主网的 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 validator
let 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 keypair
let payer = Arc::new(load_keypair()?);
println!("Using payer: {}", payer.pubkey());
// Generate a new keypair to use as the address of the token mint
let mint = Keypair::new();
println!("Mint keypair generated: {}", mint.pubkey());
// Set up program client for Token client
let program_client = ProgramRpcClient::new(rpc_client.clone(), ProgramRpcClientSendTransaction);
// Number of decimals for the mint
let decimals = 9;
// Create a token client for the Token-2022 program
// This provides high-level methods for token operations
let 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 mint
Some(decimals), // Number of decimal places
payer.clone(), // Fee payer for transactions (cloning Arc, not keypair)
);
// Create extension initialization parameters for the mint
// The ConfidentialTransferMint extension enables confidential (private) transfers of tokens
let extension_initialization_params =
vec![ExtensionInitializationParams::ConfidentialTransferMint {
authority: Some(payer.pubkey()), // Authority that can modify confidential transfer settings
auto_approve_new_accounts: true, // Automatically approve new confidential accounts
auditor_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 mint
let transaction_signature = token
.create_mint(
&payer.pubkey(), // Mint authority - can mint new tokens
Some(&payer.pubkey()), // Freeze authority - can freeze token accounts
extension_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 owner
let 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 account
let 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 extension
let 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 account
let 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 executed
let maximum_pending_balance_credit_counter = 65536;
// Initial token balance is 0
let decryptable_balance = aes_key.encrypt(0);
// Generate the proof data client-side
let proof_data = PubkeyValidityProofData::new(&elgamal_keypair)
.map_err(|_| anyhow::anyhow!("Failed to generate proof data"))?;
// Indicate that proof is included in the same transaction
let proof_location =
ProofLocation::InstructionOffset(1.try_into()?, ProofData::InstructionData(&proof_data));
// Step 4: Create instructions to configure the account for confidential transfers
let configure_account_instructions = configure_account(
&token_2022_program_id(), // Program ID
&token_account_pubkey, // Token account
&mint.pubkey(), // Mint
&decryptable_balance.into(), // Initial balance
maximum_pending_balance_credit_counter, // Maximum pending balance credit counter
&payer.pubkey(), // Token Account Owner
&[], // Additional signers
proof_location, // Proof location
)?;
// Combine all instructions
let mut instructions = vec![
create_associated_token_account_instruction,
reallocate_instruction,
];
instructions.extend(configure_account_instructions);
// Create and send the transaction
let 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 tools
fn load_keypair() -> Result<Keypair> {
// Get the default keypair path
let 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 bytes
let 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 keypair
let keypair = Keypair::from_bytes(&keypair_bytes)?;
Ok(keypair)
}

Is this page helpful?

Table of Contents

Edit Page

管理者

©️ 2026 Solana 基金会版权所有
取得联系