创建 Token 账户

如何使用机密转账扩展创建 token account

机密转账扩展通过向 token account 添加额外状态来实现私密代币转账。本节将介绍如何创建启用此扩展的 token account。

下图展示了使用机密转账扩展创建 token account 所涉及的步骤:

Create Token Account with Confidential Transfer Extension

机密转账 Token Account 状态

该扩展将 ConfidentialTransferAccount 状态添加到 token account:

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 包含多个用于管理机密转账的字段:

  • 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 的可用余额。

待处理余额高位/低位拆分

机密待处理余额被拆分为 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. 获取最近的交易,包括存款和转账指令,直至计数器差值(实际 - 预期):
    • 累加存款指令中的公开金额
    • 解密并累加转账指令中的目标密文金额

所需指令

创建和配置用于保密转账的 token account 需使用以下指令,这些指令均可在单笔交易中完成:

  1. 创建 Token Account:调用 Associated Token Program 的 AssociatedTokenAccountInstruction::Create 指令,在其确定性地址处创建token account。

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

  3. 验证 Pubkey 有效性证明:创建一个由 ZK ElGamal Proof 程序所有的账户,然后调用其 VerifyPubkeyValidity 指令以验证证明并将验证结果存储在该上下文状态账户中。

  4. 配置保密转账:调用 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::ContextStateAccountConfigureAccount 中引用,因此 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 提供加密原语。ownerclient 来自您的 @solana/kit 配置;返回的指令计划 通过 @solana/kit 的指令计划支持发送,当证明数据过大无法放入单笔交易时,将自动拆分到多笔交易中执行。

Is this page helpful?

Table of Contents

Edit Page
©️ 2026 Solana 基金会版权所有