Créer un token account

Comment créer un token account avec l'extension Confidential Transfer

L'extension Confidential Transfer permet des transferts de jetons privés en ajoutant un état supplémentaire au token account. Cette section explique comment créer un token account avec cette extension activée.

Le diagramme suivant illustre les étapes impliquées dans la création d'un token account avec l'extension Confidential Transfer :

Create Token Account with Confidential Transfer Extension

État du token account avec Confidential Transfer

L'extension ajoute l'état ConfidentialTransferAccount au 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,
}

Le ConfidentialTransferAccount contient plusieurs champs pour gérer les transferts confidentiels :

  • approved : Le statut d'approbation du compte pour les transferts confidentiels. Si la configuration auto_approve_new_accounts du mint account est définie sur true, tous les token accounts sont automatiquement approuvés pour les transferts confidentiels.

  • elgamal_pubkey : La clé publique ElGamal utilisée pour chiffrer les soldes et les montants des transferts.

  • pending_balance_lo : Les 16 bits inférieurs chiffrés du solde en attente. Le solde est divisé en parties haute et basse pour un déchiffrement efficace.

  • pending_balance_hi : Les 48 bits supérieurs chiffrés du solde en attente. Le solde est divisé en parties haute et basse pour un déchiffrement efficace.

  • available_balance : Le solde chiffré disponible pour les transferts.

  • decryptable_available_balance : Le solde disponible chiffré avec une clé Advanced Encryption Standard (AES) pour un déchiffrement efficace par le propriétaire du compte.

  • allow_confidential_credits : Si vrai, autorise les transferts confidentiels entrants.

  • allow_non_confidential_credits : Si vrai, autorise les transferts non confidentiels entrants.

  • pending_balance_credit_counter : Comptabilise les crédits de solde en attente entrants provenant des instructions de dépôt et de transfert.

  • maximum_pending_balance_credit_counter : La limite du nombre de crédits en attente avant d'exiger une instruction ApplyPendingBalance pour convertir le solde en attente en solde disponible.

  • expected_pending_balance_credit_counter : La valeur pending_balance_credit_counter fournie par le client via l'instruction data lors du dernier traitement de l'instruction ApplyPendingBalance.

  • actual_pending_balance_credit_counter : La valeur pending_balance_credit_counter sur le token account au moment où la dernière instruction ApplyPendingBalance a été traitée.

Solde en attente vs solde disponible

Les soldes confidentiels sont séparés en soldes en attente et soldes disponibles afin de prévenir les attaques DoS. Sans cette séparation, un attaquant pourrait envoyer des tokens de manière répétée vers un token account, bloquant ainsi la capacité du propriétaire du token account à transférer des tokens. Le propriétaire du token account serait dans l'impossibilité de transférer des tokens, car le solde chiffré changerait entre le moment où la transaction est soumise et celui où elle est traitée, entraînant l'échec de la transaction.

Tous les dépôts et montants de transfert sont initialement ajoutés au solde en attente. Les propriétaires de token account doivent utiliser l'instruction ApplyPendingBalance pour convertir le solde en attente en solde disponible. Les transferts entrants ou les dépôts n'affectent pas le solde disponible d'un token account.

Séparation haute/basse du solde en attente

Le solde en attente confidentiel est divisé en pending_balance_lo et pending_balance_hi, car le déchiffrement ElGamal nécessite davantage de calculs pour les grands nombres. Vous pouvez trouver l'implémentation de l'arithmétique sur les textes chiffrés ici, utilisée dans l'instruction ApplyPendingBalance ici.

Compteurs de crédit du solde en attente

Lors de l'appel de l'instruction ApplyPendingBalance pour convertir le solde en attente en solde disponible :

  1. Le client récupère les soldes en attente et disponibles actuels, chiffre leur somme et fournit un decryptable_available_balance chiffré à l'aide de la clé AES du propriétaire du token account.

  2. Les compteurs de crédit en attente attendu et réel suivent les modifications de la valeur du compteur entre le moment où l'instruction ApplyPendingBalance est créée et celui où elle est traitée :

    • expected_pending_balance_credit_counter : La valeur pending_balance_credit_counter au moment où le client crée l'instruction ApplyPendingBalance
    • actual_pending_balance_credit_counter : La valeur pending_balance_credit_counter sur le token account au moment où l'instruction ApplyPendingBalance est traitée

Des compteurs attendus/réels identiques indiquent que le decryptable_available_balance correspond au available_balance.

Lors de la récupération de l'état d'un token account pour lire le decryptable_available_balance, des valeurs différentes entre les compteurs attendus/réels obligent le client à rechercher les instructions récentes de dépôt/transfert correspondant à la différence de compteur afin de calculer le solde correct.

Processus de réconciliation du solde

Lorsque les compteurs de solde en attente attendus et réels diffèrent, suivez ces étapes pour réconcilier le decryptable_available_balance :

  1. Commencer par le decryptable_available_balance du token account
  2. Récupérer les transactions les plus récentes incluant les instructions de dépôt et de transfert jusqu'à la différence de compteur (réel - attendu) :
    • Ajouter les montants publics des instructions de dépôt
    • Déchiffrer et ajouter les montants du texte chiffré de destination des instructions de transfert

Instructions requises

La création et la configuration d'un token account pour les transferts confidentiels utilisent les instructions suivantes, qui tiennent toutes dans une seule transaction :

  1. Créer le token account : Invoquer l'instruction AssociatedTokenAccountInstruction::Create de l'Associated Token Program pour créer le token account à son adresse déterministe.

  2. Réallouer l'espace du compte : Invoquer l'instruction TokenInstruction::Reallocate du Token Extension Program pour ajouter de l'espace à l'état ConfidentialTransferAccount.

  3. Vérifier la preuve de validité du Pubkey : Créer un compte appartenant au programme ZK ElGamal Proof, puis invoquer son instruction VerifyPubkeyValidity pour vérifier la preuve et stocker le résultat vérifié dans ce compte d'état de contexte.

  4. Configurer les transferts confidentiels : Invoquer l'instruction ConfidentialTransferInstruction::ConfigureAccount du Token Extension Program, en référençant le compte d'état de contexte de preuve via ProofLocation::ContextStateAccount, pour initialiser l'état ConfidentialTransferAccount.

Seul le propriétaire du token account peut configurer un token account pour les transferts confidentiels.

L'instruction ConfigureAccount nécessite la génération côté client de clés de chiffrement et d'une preuve qui ne peut être générée que par le propriétaire du token account.

La preuve de validité du pubkey vérifie que la clé publique ElGamal du compte est valide. Elle est générée avec build_pubkey_validity_proof_data, vérifiée on-chain par le programme ZK ElGamal Proof dans un compte d'état de contexte, puis référencée depuis ConfigureAccount via ProofLocation::ContextStateAccount, de sorte qu'aucun octet de preuve ne transite dans l'instruction du token elle-même. Pour les détails d'implémentation, voir :

Exemple de code

Le code suivant crée un associated token account et le configure pour les transferts confidentiels sur un mint confidentiel existant.

Les transferts confidentiels reposent sur le programme ZK ElGamal Proof, qui est activé sur le mainnet et le devnet. Un solana-test-validator standard ne l'active pas, mais un validator local avec fork du mainnet tel que Surfpool le fait. Exécutez l'exemple sur l'un d'eux (le code utilise le devnet) avec un payeur approvisionné, et remplacez le placeholder du mint par un mint créé conformément à Créer 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}`
);

Les utilitaires TypeScript se trouvent dans le sous-chemin @solana-program/token-2022/confidential et s'appuient sur @solana/zk-sdk pour les primitives de chiffrement. owner et client proviennent de votre configuration @solana/kit ; le plan d'instructions retourné est envoyé via la prise en charge des plans d'instructions de @solana/kit, qui répartit le travail sur plusieurs transactions lorsque les preuves sont trop volumineuses pour une seule.

Is this page helpful?

Table des matières

Modifier la page
© 2026 Fondation Solana. Tous droits réservés.