Crear un token account

Cómo crear un token account con la extensión Confidential Transfer

La extensión Confidential Transfer permite transferencias privadas de tokens añadiendo estado adicional al token account. Esta sección explica cómo crear un token account con esta extensión habilitada.

El siguiente diagrama muestra los pasos necesarios para crear un token account con la extensión Confidential Transfer:

Create Token Account with Confidential Transfer Extension

Estado del Token Account con Confidential Transfer

La extensión añade el estado ConfidentialTransferAccount al 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,
}

El ConfidentialTransferAccount contiene varios campos para gestionar las transferencias confidenciales:

  • approved: El estado de aprobación de la cuenta para transferencias confidenciales. Si la configuración auto_approve_new_accounts del mint account está establecida como true, todos los token accounts son aprobados automáticamente para transferencias confidenciales.

  • elgamal_pubkey: La clave pública ElGamal utilizada para cifrar saldos e importes de transferencias.

  • pending_balance_lo: Los 16 bits inferiores cifrados del saldo pendiente. El saldo se divide en partes alta y baja para una descifrado eficiente.

  • pending_balance_hi: Los 48 bits superiores cifrados del saldo pendiente. El saldo se divide en partes alta y baja para un descifrado eficiente.

  • available_balance: El saldo cifrado disponible para transferencias.

  • decryptable_available_balance: El saldo disponible cifrado con una clave de Estándar de Cifrado Avanzado (AES) para un descifrado eficiente por parte del propietario de la cuenta.

  • allow_confidential_credits: Si es verdadero, permite transferencias confidenciales entrantes.

  • allow_non_confidential_credits: Si es verdadero, permite transferencias no confidenciales entrantes.

  • pending_balance_credit_counter: Cuenta los créditos de saldo pendiente entrantes provenientes de instrucciones de depósito y transferencia.

  • maximum_pending_balance_credit_counter: El límite de créditos pendientes antes de requerir una instrucción ApplyPendingBalance para convertir el saldo pendiente en saldo disponible.

  • expected_pending_balance_credit_counter: El valor pending_balance_credit_counter proporcionado por el cliente a través de la instruction data la última vez que se procesó la instrucción ApplyPendingBalance.

  • actual_pending_balance_credit_counter: El valor pending_balance_credit_counter en el token account en el momento en que se procesó la última instrucción ApplyPendingBalance.

Saldo Pendiente vs Saldo Disponible

Los saldos confidenciales se dividen en saldos pendientes y disponibles para prevenir ataques DoS. Sin esta separación, un atacante podría enviar tokens repetidamente a un token account, bloqueando la capacidad del propietario del token account para transferir tokens. El propietario del token account no podría transferir tokens porque el saldo cifrado cambiaría entre el momento en que se envía la transacción y cuando se procesa, resultando en una transacción fallida.

Todos los depósitos y montos de transferencia se añaden inicialmente al saldo pendiente. Los propietarios del token account deben usar la instrucción ApplyPendingBalance para convertir el saldo pendiente en saldo disponible. Las transferencias o depósitos entrantes no afectan el saldo disponible del token account.

División Alta/Baja del Saldo Pendiente

El saldo pendiente confidencial se divide en pending_balance_lo e pending_balance_hi porque el descifrado ElGamal requiere mayor cómputo para números más grandes. Puedes encontrar la implementación de aritmética de texto cifrado aquí, que se utiliza en la instrucción ApplyPendingBalance aquí.

Contadores de Crédito de Saldo Pendiente

Al llamar a la instrucción ApplyPendingBalance para convertir el saldo pendiente en saldo disponible:

  1. El cliente consulta los saldos pendientes y disponibles actuales, cifra la suma y proporciona un decryptable_available_balance cifrado usando la clave AES del propietario del token account.

  2. Los contadores de crédito pendiente esperado y real rastrean los cambios en el valor del contador entre el momento en que se crea y se procesa la instrucción ApplyPendingBalance:

    • expected_pending_balance_credit_counter: El valor pending_balance_credit_counter cuando el cliente crea la instrucción ApplyPendingBalance
    • actual_pending_balance_credit_counter: El valor pending_balance_credit_counter en el token account en el momento en que se procesa la instrucción ApplyPendingBalance

Los contadores esperado/real coincidentes indican que el decryptable_available_balance corresponde al available_balance.

Al obtener el estado de un token account para leer el decryptable_available_balance, valores diferentes en los contadores esperado/real requieren que el cliente busque instrucciones recientes de depósito/transferencia que coincidan con la diferencia de contador para calcular el saldo correcto.

Proceso de Reconciliación de Saldo

Cuando los contadores de saldo pendiente esperado y real difieren, sigue estos pasos para reconciliar el decryptable_available_balance:

  1. Comienza con el decryptable_available_balance del token account
  2. Obtén las transacciones más recientes que incluyan instrucciones de depósito y transferencia hasta la diferencia de contador (real - esperado):
    • Agrega los montos públicos de las instrucciones de depósito
    • Descifra y agrega los montos de texto cifrado de destino de las instrucciones de transferencia

Instrucciones Requeridas

La creación y configuración de un token account para transferencias confidenciales utiliza las siguientes instrucciones, que caben en una sola transacción:

  1. Crear el Token Account: Invoca la instrucción AssociatedTokenAccountInstruction::Create del Associated Token Program para crear el token account en su dirección determinista.

  2. Reasignar Espacio de Cuenta: Invoca la instrucción TokenInstruction::Reallocate del Token Extensions Program para agregar espacio para el estado ConfidentialTransferAccount.

  3. Verificar la Prueba de Validez del Pubkey: Crea una cuenta propiedad del programa ZK ElGamal Proof, luego invoca su instrucción VerifyPubkeyValidity para verificar la prueba y almacenar el resultado verificado en esa cuenta de estado de contexto.

  4. Configurar Transferencias Confidenciales: Invoca la instrucción ConfidentialTransferInstruction::ConfigureAccount del Token Extensions Program, referenciando la cuenta de estado de contexto de la prueba mediante ProofLocation::ContextStateAccount, para inicializar el estado ConfidentialTransferAccount.

Solo el propietario del token account puede configurar un token account para transferencias confidenciales.

La instrucción ConfigureAccount requiere la generación del lado del cliente de claves de cifrado y una prueba que solo puede ser generada por el propietario del token account.

La prueba de validez del pubkey verifica que la clave pública ElGamal de la cuenta sea válida. Se genera con build_pubkey_validity_proof_data, se verifica en cadena por el programa ZK ElGamal Proof en una cuenta de estado de contexto, y luego se referencia desde ConfigureAccount mediante ProofLocation::ContextStateAccount, de modo que ningún byte de prueba viaja en la propia instrucción del token. Para detalles de implementación, consulta:

Código de Ejemplo

El siguiente código crea una associated token account y la configura para transferencias confidenciales contra un mint confidencial existente.

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 con alguno de estos (el código usa devnet) con un pagador con fondos, y reemplaza el marcador de mint con un mint creado según Crear un 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}`
);

Los helpers de TypeScript se encuentran en el subpath @solana-program/token-2022/confidential y se basan en @solana/zk-sdk para las primitivas de cifrado. owner y client provienen de tu configuración de @solana/kit; el plan de instrucciones devuelto se envía con el soporte de planes de instrucciones de @solana/kit, que distribuye el trabajo entre varias transacciones cuando las pruebas son demasiado grandes para una sola.

Is this page helpful?

Tabla de Contenidos

Editar Página