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:
Estado do Token Account com Confidential Transfer
A extensão adiciona o estado ConfidentialTransferAccount ao 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,}
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_accountsdo mint account estiver definida comotrue, 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
ApplyPendingBalancepara converter o saldo pendente no saldo disponível. -
expected_pending_balance_credit_counter: O valor
pending_balance_credit_counterfornecido pelo cliente por meio do instruction data na última vez que a instruçãoApplyPendingBalancefoi processada. -
actual_pending_balance_credit_counter: O valor
pending_balance_credit_counterno token account no momento em que a última instruçãoApplyPendingBalancefoi 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:
-
O cliente consulta os saldos pendentes e disponíveis atuais, criptografa a soma e fornece um
decryptable_available_balancecriptografado usando a chave AES do proprietário do token account. -
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 valorpending_balance_credit_counterquando o cliente cria a instruçãoApplyPendingBalanceactual_pending_balance_credit_counter: O valorpending_balance_credit_counterno token account no momento em que a instruçãoApplyPendingBalanceé 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:
- Comece com o
decryptable_available_balancedo token account - 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:
-
Criar o Token Account: Invoque a instrução
AssociatedTokenAccountInstruction::Createdo Associated Token Program para criar o token account em seu endereço determinístico. -
Realocar Espaço da Conta: Invoque a instrução
TokenInstruction::Reallocatedo Token Extensions Program para adicionar espaço ao estadoConfidentialTransferAccount. -
Verificar a Prova de Validade do Pubkey: Crie uma conta de propriedade do programa ZK ElGamal Proof, em seguida invoque sua instrução
VerifyPubkeyValiditypara verificar a prova e armazenar o resultado verificado nessa conta de estado de contexto. -
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 estadoConfidentialTransferAccount.
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?