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 :
État du token account avec Confidential Transfer
L'extension ajoute l'état ConfidentialTransferAccount au 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,}
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_accountsdu mint account est définie surtrue, 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
ApplyPendingBalancepour convertir le solde en attente en solde disponible. -
expected_pending_balance_credit_counter : La valeur
pending_balance_credit_counterfournie par le client via l'instruction data lors du dernier traitement de l'instructionApplyPendingBalance. -
actual_pending_balance_credit_counter : La valeur
pending_balance_credit_countersur le token account au moment où la dernière instructionApplyPendingBalancea é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 :
-
Le client récupère les soldes en attente et disponibles actuels, chiffre leur somme et fournit un
decryptable_available_balancechiffré à l'aide de la clé AES du propriétaire du token account. -
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
ApplyPendingBalanceest créée et celui où elle est traitée :expected_pending_balance_credit_counter: La valeurpending_balance_credit_counterau moment où le client crée l'instructionApplyPendingBalanceactual_pending_balance_credit_counter: La valeurpending_balance_credit_countersur le token account au moment où l'instructionApplyPendingBalanceest 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 :
- Commencer par le
decryptable_available_balancedu token account - 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 :
-
Créer le token account : Invoquer l'instruction
AssociatedTokenAccountInstruction::Createde l'Associated Token Program pour créer le token account à son adresse déterministe. -
Réallouer l'espace du compte : Invoquer l'instruction
TokenInstruction::Reallocatedu Token Extension Program pour ajouter de l'espace à l'étatConfidentialTransferAccount. -
Vérifier la preuve de validité du Pubkey : Créer un compte appartenant au programme ZK ElGamal Proof, puis invoquer son instruction
VerifyPubkeyValiditypour vérifier la preuve et stocker le résultat vérifié dans ce compte d'état de contexte. -
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'étatConfidentialTransferAccount.
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?