Cómo transferir tokens de forma confidencial de un token account a otro
Para transferir tokens de forma confidencial de un token account a otro, tanto
el remitente como el destinatario deben tener token accounts configuradas con el
estado ConfidentialTransferAccount y aprobadas para transferencias
confidenciales. El token account del remitente también debe tener un saldo
confidencial disponible del que transferir.
Para transferir tokens de forma confidencial:
-
Crea tres pruebas en el lado del cliente:
Prueba de igualdad (CiphertextCommitmentEqualityProofData): Verifica que el nuevo texto cifrado del saldo disponible tras la transferencia coincida con su correspondiente compromiso de Pedersen, garantizando que el nuevo saldo disponible de la cuenta de origen se calcule correctamente como
new_balance = current_balance - transfer_amount.Prueba de validez del texto cifrado (BatchedGroupedCiphertext3HandlesValidityProofData): Verifica que los textos cifrados del monto de transferencia estén generados correctamente para las tres partes (origen, destino y auditor), garantizando que el monto de transferencia esté correctamente cifrado bajo la clave pública de cada parte.
Prueba de rango (BatchedRangeProofU128Data): Verifica que el nuevo saldo disponible y el monto de transferencia (dividido en bits bajos/altos) sean no negativos y estén dentro de un rango especificado.
-
Para cada prueba:
- Invoca el programa de pruebas ZK ElGamal para verificar los datos de la prueba.
- Almacena los metadatos específicos de la prueba en una cuenta de "estado de contexto" de prueba para usarla en otras instrucciones.
-
Invoca la instrucción ConfidentialTransferInstruction::Transfer proporcionando las cuentas de estado de contexto de prueba.
-
Cierra las cuentas de estado de contexto de prueba para recuperar el SOL utilizado para crearlas.
El siguiente diagrama muestra los pasos involucrados en la transferencia de tokens desde el token account del remitente al token account del destinatario.
Instrucciones requeridas
Para transferir tokens de forma confidencial de un token account a otro, debes:
- Generar una prueba de igualdad, una prueba de validez del texto cifrado y una prueba de rango en el lado del cliente
- Invocar el programa de pruebas Zk ElGamal para verificar las pruebas e inicializar las cuentas de "estado de contexto"
- Invocar la instrucción ConfidentialTransferInstruction::Transfer proporcionando las tres cuentas de prueba.
- Cerrar las tres cuentas de prueba para recuperar el rent.
El ejemplo de Rust a continuación genera las pruebas con el crate
spl-token-confidential-transfer-proof-generation, verifica cada una en una
cuenta de estado de contexto a través del programa ZK ElGamal Proof, referencia
las tres cuentas en la instrucción de transferencia y las cierra posteriormente.
El ejemplo de TypeScript utiliza el helper
getConfidentialTransferInstructionPlan de
@solana-program/token-2022/confidential, que ensambla las mismas cuentas de
prueba, la transferencia y el cierre como un plan de instrucciones de múltiples
transacciones.
Código de ejemplo
El siguiente ejemplo transfiere tokens de forma confidencial de una cuenta a otra. Ambas cuentas deben estar configuradas previamente para transferencias confidenciales, y el remitente debe contar con un saldo confidencial disponible.
Las transferencias confidenciales dependen del programa ZK ElGamal Proof, que
está habilitado en mainnet y devnet. Un solana-test-validator estándar no lo
habilita, pero un validator local con bifurcación de mainnet como
Surfpool sí lo hace. Ejecuta el ejemplo en uno de ellos
(el código usa devnet) con un pagador con fondos, y reemplaza los marcadores de
posición con tu mint y las cuentas del remitente y destinatario.
Rust
const ZK_PROOF_PROGRAM_ID: Pubkey =solana_pubkey::pubkey!("ZkE1Gama1Proof11111111111111111111111111111");fn main() -> Result<()> {let rpc_client = RpcClient::new_with_commitment(String::from("https://api.devnet.solana.com"),CommitmentConfig::confirmed(),);// Sender = fee payer = token account owner. Both the sender and recipient// accounts must already be configured for confidential transfers, and the// sender must have an available confidential balance (deposit then apply// pending balance beforehand).let sender = load_keypair()?;let amount: u64 = 100;// Setup: create confidential accounts and fund the sender.let recipient_keypair = Keypair::new();let (mint, sender_token_account, recipient_token_account) =setup_transfer_accounts(&rpc_client, &sender, &recipient_keypair, amount)?;// Read the recipient's ElGamal public key from their confidential account.let recipient_acc = rpc_client.get_account(&recipient_token_account)?;let recipient_state = StateWithExtensions::<TokenAccount>::unpack(&recipient_acc.data)?;let recipient_elgamal_pubkey: ElGamalPubkey = recipient_state.get_extension::<ConfidentialTransferAccount>()?.elgamal_pubkey.try_into().map_err(|e| anyhow::anyhow!("recipient ElGamal pubkey: {e:?}"))?;// Read the optional auditor ElGamal public key from the mint.let mint_acc = rpc_client.get_account(&mint)?;let mint_state = StateWithExtensions::<Mint>::unpack(&mint_acc.data)?;let mint_ext = mint_state.get_extension::<ConfidentialTransferMint>()?;let auditor_elgamal_pubkey: Option<ElGamalPubkey> =Option::<PodElGamalPubkey>::from(mint_ext.auditor_elgamal_pubkey).map(|pod| {ElGamalPubkey::try_from(pod).map_err(|e| anyhow::anyhow!("auditor pubkey: {e:?}"))}).transpose()?;// Derive the sender's keys and read their current confidential balance.let (sender_elgamal, sender_aes) =derive_confidential_keys(&sender, &sender_token_account.to_bytes()).map_err(|e| anyhow::anyhow!("derive confidential keys: {e}"))?;let sender_acc = rpc_client.get_account(&sender_token_account)?;let sender_state = StateWithExtensions::<TokenAccount>::unpack(&sender_acc.data)?;let sender_ext = sender_state.get_extension::<ConfidentialTransferAccount>()?;let current_available: ElGamalCiphertext = sender_ext.available_balance.try_into().map_err(|e| anyhow::anyhow!("available balance: {e:?}"))?;let current_decryptable: AeCiphertext = sender_ext.decryptable_available_balance.try_into().map_err(|e| anyhow::anyhow!("decryptable balance: {e:?}"))?;// Generate the three transfer proofs (equality, ciphertext-validity, range).let proof_data = transfer_split_proof_data(¤t_available,¤t_decryptable,amount,&sender_elgamal,&sender_aes,&recipient_elgamal_pubkey,auditor_elgamal_pubkey.as_ref(),).map_err(|e| anyhow::anyhow!("transfer_split_proof_data: {e}"))?;// Create one context state account per proof, owned by the ZK program.let equality_account = Keypair::new();let validity_account = Keypair::new();let range_account = Keypair::new();let equality_size = size_of::<ProofContextState<CiphertextCommitmentEqualityProofContext>>();let validity_size =size_of::<ProofContextState<BatchedGroupedCiphertext3HandlesValidityProofContext>>();let range_size = size_of::<ProofContextState<BatchedRangeProofContext>>();let create = |account: &Keypair, space: usize| -> Result<Instruction> {Ok(system_instruction::create_account(&sender.pubkey(),&account.pubkey(),rpc_client.get_minimum_balance_for_rent_exemption(space)?,space as u64,&ZK_PROOF_PROGRAM_ID,))};let equality_create_ix = create(&equality_account, equality_size)?;let validity_create_ix = create(&validity_account, validity_size)?;let range_create_ix = create(&range_account, range_size)?;// The sender is the context-state authority for all three proof accounts.let authority: Address = sender.pubkey().to_bytes().into();let equality_verify_ix = ProofInstruction::VerifyCiphertextCommitmentEquality.encode_verify_proof(Some(ContextStateInfo {context_state_account: &Address::from(equality_account.pubkey().to_bytes()),context_state_authority: &authority,}),&proof_data.equality_proof_data,);let validity_verify_ix = ProofInstruction::VerifyBatchedGroupedCiphertext3HandlesValidity.encode_verify_proof(Some(ContextStateInfo {context_state_account: &Address::from(validity_account.pubkey().to_bytes()),context_state_authority: &authority,}),&proof_data.ciphertext_validity_proof_data_with_ciphertext.proof_data,);let range_verify_ix = ProofInstruction::VerifyBatchedRangeProofU128.encode_verify_proof(Some(ContextStateInfo {context_state_account: &Address::from(range_account.pubkey().to_bytes()),context_state_authority: &authority,}),&proof_data.range_proof_data,);// Transaction 1: create all three accounts and verify the validity proof.send_tx(&rpc_client,&[equality_create_ix,validity_create_ix,range_create_ix,validity_verify_ix,],&[&sender, &equality_account, &validity_account, &range_account],)?;// Transaction 2: verify the range proof (the largest, on its own).send_tx(&rpc_client, &[range_verify_ix], &[&sender])?;// Compute the sender's new decryptable available balance after the transfer.let current_plaintext = current_decryptable.decrypt(&sender_aes).context("decrypt available balance")?;let new_plaintext = current_plaintext.checked_sub(amount).context("insufficient available balance")?;let new_decryptable: PodAeCiphertext = sender_aes.encrypt(new_plaintext).into();let auditor_lo = proof_data.ciphertext_validity_proof_data_with_ciphertext.ciphertext_lo;let auditor_hi = proof_data.ciphertext_validity_proof_data_with_ciphertext.ciphertext_hi;let transfer_ix = inner_transfer(&spl_token_2022::id(),&sender_token_account,&mint,&recipient_token_account,&new_decryptable,&auditor_lo,&auditor_hi,&sender.pubkey(),&[],ProofLocation::ContextStateAccount(&equality_account.pubkey()),ProofLocation::ContextStateAccount(&validity_account.pubkey()),ProofLocation::ContextStateAccount(&range_account.pubkey()),)?;// Transaction 3: verify the equality proof, run the transfer, and close the// three proof accounts to reclaim their rent.let close = |account: &Keypair| {close_context_state(ContextStateInfo {context_state_account: &Address::from(account.pubkey().to_bytes()),context_state_authority: &authority,},&authority,)};let instructions = [equality_verify_ix,transfer_ix,close(&equality_account),close(&validity_account),close(&range_account),];let blockhash = rpc_client.get_latest_blockhash()?;let transaction = Transaction::new_signed_with_payer(&instructions,Some(&sender.pubkey()),&[&sender],blockhash,);let signature = rpc_client.send_and_confirm_transaction(&transaction)?;println!("Transferred {amount} tokens confidentially: {signature}");Ok(())}
Typescript
const client = await createClient().use(signerFromFile(join(homedir(), ".config/solana/id.json"))).use(solanaRpc({rpcUrl: "https://api.devnet.solana.com",maxConcurrency: 1}))// Temporary custom plugin to skip the default compute-budget estimate// so proof instructions fit within the transaction message cap.// The Solana CLI default keypair, used as fee payer, mint authority, and sender.const owner = client.payer;const recipient = await generateKeyPairSigner();const depositAmount = 100n;const amount = 25n;const decimals = 2;// Setup: create source and destination confidential accounts, then fund source.const mint = await createConfidentialMint(client, owner, decimals);const auditorElgamalPubkey = await getAuditorElgamalPubkey(client, mint);const sourceToken = await createConfidentialTokenAccount(client, owner, mint);const destinationToken = await createConfidentialTokenAccount(client,recipient,mint);await mintPublicTokens(client, owner, mint, sourceToken, depositAmount);await depositTokens(client, owner, mint, sourceToken, depositAmount, decimals);await applyPendingBalance(client, owner, mint, sourceToken);// Derive the sender's recoverable ElGamal and AES keys, bound to (owner, mint).const { elgamalKeypair: sourceElgamalKeypair, aesKey } =await deriveConfidentialKeys(owner, mint);// The helper reads the recipient key from the destination account; pass the// configured auditor key so the proof matches the mint configuration.const sourceTokenAccount = (await fetchToken(client.rpc, sourceToken)).data;const destinationTokenAccount = (await fetchToken(client.rpc, destinationToken)).data;// Builds the proof context-state accounts, the transfer, and the closes as a// multi-transaction plan (the three proofs are too large for one transaction).const plan = await getConfidentialTransferInstructionPlan({rpc: client.rpc,payer: owner,authority: owner,mint,sourceToken,sourceTokenAccount,destinationToken,destinationTokenAccount,auditorElgamalPubkey,amount,sourceElgamalKeypair,aesKey});const result = await client.sendTransactions(plan);const summary = summarizeTransactionPlanResult(result);const signature =summary.successfulTransactions[summary.successfulTransactions.length - 1].context.signature;console.log(`Transferred ${amount} tokens confidentially: ${signature}`);
Is this page helpful?