Criar uma Token Account

Como criar um token account com a extensão Confidential Transfer

A extensão Confidential Transfer permite transferências privadas de tokens ao adicionar estado extra ao token account. Esta seção explica como criar um token account com essa extensão habilitada.

O diagrama a seguir mostra as etapas envolvidas na criação de um token account com a extensão Confidential Transfer:

Create Token Account with Confidential Transfer Extension

Estado do Token Account com Confidential Transfer

A extensão adiciona o estado ConfidentialTransferAccount ao 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,
}

O ConfidentialTransferAccount contém vários campos para gerenciar transferências confidenciais:

  • approved: O status de aprovação da conta para transferências confidenciais. Se a configuração auto_approve_new_accounts do mint account estiver definida como true, todos os token accounts são automaticamente aprovados para transferências confidenciais.

  • elgamal_pubkey: A chave pública ElGamal usada para criptografar saldos e valores de transferência.

  • pending_balance_lo: Os 16 bits inferiores criptografados do saldo pendente. O saldo é dividido em partes alta e baixa para decriptação eficiente.

  • pending_balance_hi: Os 48 bits superiores criptografados do saldo pendente. O saldo é dividido em partes alta e baixa para decriptação eficiente.

  • available_balance: O saldo criptografado disponível para transferências.

  • decryptable_available_balance: O saldo disponível criptografado com uma chave AES (Advanced Encryption Standard) para decriptação eficiente pelo proprietário da conta.

  • allow_confidential_credits: Se verdadeiro, permite transferências confidenciais recebidas.

  • allow_non_confidential_credits: Se verdadeiro, permite transferências não confidenciais recebidas.

  • pending_balance_credit_counter: Contabiliza os créditos de saldo pendente recebidos de instruções de depósito e transferência.

  • maximum_pending_balance_credit_counter: O limite de contagem de créditos pendentes antes de exigir uma instrução ApplyPendingBalance para converter o saldo pendente no saldo disponível.

  • expected_pending_balance_credit_counter: O valor pending_balance_credit_counter fornecido pelo cliente por meio do instruction data na última vez que a instrução ApplyPendingBalance foi processada.

  • actual_pending_balance_credit_counter: O valor pending_balance_credit_counter no token account no momento em que a última instrução ApplyPendingBalance foi processada.

Saldo Pendente vs Saldo Disponível

Os saldos confidenciais são divididos em saldos pendentes e disponíveis para prevenir ataques DoS. Sem essa separação, um atacante poderia enviar tokens repetidamente para um token account, bloqueando a capacidade do proprietário do token account de transferir tokens. O proprietário do token account ficaria impossibilitado de transferir tokens, pois o saldo criptografado mudaria entre o momento em que a transação é enviada e quando é processada, resultando em uma transação com falha.

Todos os depósitos e valores de transferência são inicialmente adicionados ao saldo pendente. Os proprietários de token account devem usar a instrução ApplyPendingBalance para converter o saldo pendente em saldo disponível. Transferências ou depósitos recebidos não afetam o saldo disponível de um token account.

Divisão Alta/Baixa do Saldo Pendente

O saldo pendente confidencial é dividido em pending_balance_lo e pending_balance_hi porque a descriptografia ElGamal requer mais processamento para números maiores. Você pode encontrar a implementação aritmética de texto cifrado aqui, que é utilizada na instrução ApplyPendingBalance aqui.

Contadores de Crédito de Saldo Pendente

Ao chamar a instrução ApplyPendingBalance para converter o saldo pendente em saldo disponível:

  1. O cliente consulta os saldos pendentes e disponíveis atuais, criptografa a soma e fornece um decryptable_available_balance criptografado usando a chave AES do proprietário do token account.

  2. Os contadores de crédito pendente esperado e real rastreiam alterações no valor do contador entre o momento em que a instrução ApplyPendingBalance é criada e processada:

    • expected_pending_balance_credit_counter: O valor pending_balance_credit_counter quando o cliente cria a instrução ApplyPendingBalance
    • actual_pending_balance_credit_counter: O valor pending_balance_credit_counter no token account no momento em que a instrução ApplyPendingBalance é processada

Contadores esperados/reais correspondentes indicam que o decryptable_available_balance corresponde ao available_balance.

Ao buscar o estado de um token account para ler o decryptable_available_balance, valores diferentes nos contadores esperados/reais exigem que o cliente consulte instruções recentes de depósito/transferência correspondentes à diferença de contadores para calcular o saldo correto.

Processo de Reconciliação de Saldo

Quando os contadores de saldo pendente esperado e real diferem, siga estas etapas para reconciliar o decryptable_available_balance:

  1. Comece com o decryptable_available_balance do token account
  2. Busque as transações mais recentes incluindo instruções de depósito e transferência até a diferença de contadores (real - esperado):
    • Adicione os valores públicos das instruções de depósito
    • Descriptografe e adicione os valores de texto cifrado de destino das instruções de transferência

Instruções Necessárias

A criação e configuração de um token account para transferências confidenciais utiliza as seguintes instruções, que cabem em uma única transação:

  1. Criar o Token Account: Invoque a instrução AssociatedTokenAccountInstruction::Create do Associated Token Program para criar o token account em seu endereço determinístico.

  2. Realocar Espaço da Conta: Invoque a instrução TokenInstruction::Reallocate do Token Extensions Program para adicionar espaço ao estado ConfidentialTransferAccount.

  3. Verificar a Prova de Validade do Pubkey: Crie uma conta de propriedade do programa ZK ElGamal Proof, em seguida invoque sua instrução VerifyPubkeyValidity para verificar a prova e armazenar o resultado verificado nessa conta de estado de contexto.

  4. Configurar Transferências Confidenciais: Invoque a instrução ConfidentialTransferInstruction::ConfigureAccount do Token Extensions Program, referenciando a conta de estado de contexto da prova via ProofLocation::ContextStateAccount, para inicializar o estado ConfidentialTransferAccount.

Apenas o proprietário da token account pode configurar uma token account para transferências confidenciais.

A instrução ConfigureAccount requer a geração no lado do cliente de chaves de criptografia e uma prova que só pode ser gerada pelo proprietário do token account.

A prova de validade do pubkey verifica que a chave pública ElGamal da conta é válida. Ela é gerada com build_pubkey_validity_proof_data, verificada na cadeia pelo programa ZK ElGamal Proof em uma conta de estado de contexto, e depois referenciada a partir de ConfigureAccount via ProofLocation::ContextStateAccount, de modo que nenhum byte de prova percorre a própria instrução do token. Para detalhes de implementação, consulte:

Código de Exemplo

O código a seguir cria uma associated token account e a configura para transferências confidenciais contra um mint confidencial existente.

As transferências confidenciais dependem do programa ZK ElGamal Proof, que está habilitado na mainnet e devnet. Um solana-test-validator padrão não o habilita, mas um validator local com fork da mainnet, como o Surfpool, sim. Execute o exemplo em um desses ambientes (o código usa a devnet) com um pagador com saldo, e substitua o placeholder do mint por um mint criado conforme Criar um Mint.

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}`
);

Os helpers TypeScript residem no subcaminho @solana-program/token-2022/confidential e são construídos sobre @solana/zk-sdk para as primitivas de criptografia. owner e client vêm da sua configuração @solana/kit; o plano de instruções retornado é enviado com o suporte a planos de instruções do @solana/kit, que divide o trabalho entre transações quando as provas são grandes demais para uma única.

Is this page helpful?

Índice

Editar Página
© 2026 Fundação Solana. Todos os direitos reservados.