Come creare un token account con l'estensione Confidential Transfer
L'estensione Confidential Transfer abilita i trasferimenti privati di token aggiungendo stato aggiuntivo al token account. Questa sezione spiega come creare un token account con questa estensione abilitata.
Il seguente diagramma mostra i passaggi necessari per creare un token account con l'estensione Confidential Transfer:
Stato del Token Account con Confidential Transfer
L'estensione aggiunge lo stato ConfidentialTransferAccount al 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,}
Il ConfidentialTransferAccount contiene diversi campi per gestire i
trasferimenti riservati:
-
approved: Lo stato di approvazione dell'account per i trasferimenti riservati. Se la configurazione
auto_approve_new_accountsdel mint account è impostata cometrue, tutti i token account vengono approvati automaticamente per i trasferimenti riservati. -
elgamal_pubkey: La chiave pubblica ElGamal utilizzata per cifrare i saldi e gli importi dei trasferimenti.
-
pending_balance_lo: I 16 bit inferiori cifrati del saldo in sospeso. Il saldo è suddiviso in parti alta e bassa per una decrittazione efficiente.
-
pending_balance_hi: I 48 bit superiori cifrati del saldo in sospeso. Il saldo è suddiviso in parti alta e bassa per una decrittazione efficiente.
-
available_balance: Il saldo cifrato disponibile per i trasferimenti.
-
decryptable_available_balance: Il saldo disponibile cifrato con una chiave AES (Advanced Encryption Standard) per una decrittazione efficiente da parte del titolare dell'account.
-
allow_confidential_credits: Se true, consente i trasferimenti riservati in entrata.
-
allow_non_confidential_credits: Se true, consente i trasferimenti non riservati in entrata.
-
pending_balance_credit_counter: Conteggia i crediti del saldo in sospeso in entrata dalle istruzioni di deposito e trasferimento.
-
maximum_pending_balance_credit_counter: Il limite di conteggio dei crediti in sospeso prima di richiedere un'istruzione
ApplyPendingBalanceper convertire il saldo in sospeso nel saldo disponibile. -
expected_pending_balance_credit_counter: Il valore
pending_balance_credit_counterfornito dal client tramite instruction data l'ultima volta che l'istruzioneApplyPendingBalanceè stata elaborata. -
actual_pending_balance_credit_counter: Il valore
pending_balance_credit_countersul token account al momento in cui l'ultima istruzioneApplyPendingBalanceè stata elaborata.
Saldo in Sospeso vs Saldo Disponibile
I saldi riservati sono suddivisi in saldi in sospeso e saldi disponibili per prevenire attacchi DoS. Senza questa separazione, un attaccante potrebbe inviare ripetutamente token a un token account, bloccando la capacità del proprietario del token account di trasferire token. Il proprietario del token account non sarebbe in grado di trasferire token perché il saldo crittografato cambierebbe tra il momento in cui la transazione viene inviata e quello in cui viene elaborata, causando il fallimento della transazione.
Tutti i depositi e gli importi dei trasferimenti vengono inizialmente aggiunti
al saldo in sospeso. I proprietari del token account devono utilizzare
l'istruzione ApplyPendingBalance per convertire il saldo in sospeso in
saldo disponibile. I trasferimenti in entrata o i depositi non influiscono sul
saldo disponibile di un token account.
Suddivisione Alto/Basso del Saldo in Sospeso
Il saldo in sospeso riservato è suddiviso in pending_balance_lo e
pending_balance_hi perché la decrittazione ElGamal richiede un maggiore
calcolo per numeri più grandi. L'implementazione dell'aritmetica sui testi
cifrati è disponibile
qui,
utilizzata nell'istruzione ApplyPendingBalance
qui.
Contatori di Credito del Saldo in Sospeso
Quando si chiama l'istruzione ApplyPendingBalance per convertire il saldo
in sospeso in saldo disponibile:
-
Il client recupera i saldi in sospeso e disponibili correnti, ne crittografa la somma e fornisce un
decryptable_available_balancecrittografato utilizzando la chiave AES del proprietario del token account. -
I contatori di credito in sospeso attesi e effettivi tracciano le modifiche al valore del contatore tra il momento in cui l'istruzione
ApplyPendingBalanceviene creata e quello in cui viene elaborata:expected_pending_balance_credit_counter: Il valorepending_balance_credit_counterquando il client crea l'istruzioneApplyPendingBalanceactual_pending_balance_credit_counter: Il valorepending_balance_credit_countersul token account al momento in cui l'istruzioneApplyPendingBalanceviene elaborata
Contatori expected/actual corrispondenti indicano che
decryptable_available_balance corrisponde a available_balance.
Quando si recupera lo stato di un token account per leggere
decryptable_available_balance, valori diversi dei contatori expected/actual
richiedono al client di cercare le istruzioni recenti di deposito/trasferimento
corrispondenti alla differenza dei contatori per calcolare il saldo corretto.
Processo di Riconciliazione del Saldo
Quando i contatori del saldo in attesa expected e actual differiscono, seguire
questi passaggi per riconciliare decryptable_available_balance:
- Iniziare con
decryptable_available_balancedal token account - Recuperare le transazioni più recenti incluse le istruzioni di deposito e
trasferimento fino alla differenza dei contatori (actual - expected):
- Aggiungere gli importi pubblici dalle istruzioni di deposito
- Decifrare e aggiungere gli importi del ciphertext di destinazione dalle istruzioni di trasferimento
Istruzioni Richieste
La creazione e la configurazione di un token account per i trasferimenti riservati utilizza le seguenti istruzioni, che rientrano tutte in una singola transazione:
-
Creare il Token Account: Invocare l'istruzione
AssociatedTokenAccountInstruction::Createdell'Associated Token Program per creare il token account al suo indirizzo deterministico. -
Riallocare lo Spazio dell'Account: Invocare l'istruzione
TokenInstruction::Reallocatedel Token Extension Program per aggiungere spazio per lo statoConfidentialTransferAccount. -
Verificare la Prova di Validità del Pubkey: Creare un account di proprietà del programma ZK ElGamal Proof, quindi invocare la sua istruzione
VerifyPubkeyValidityper verificare la prova e memorizzare il risultato verificato in quell'account di stato di contesto. -
Configurare i Trasferimenti Riservati: Invocare l'istruzione ConfidentialTransferInstruction::ConfigureAccount del Token Extension Program, facendo riferimento all'account di stato di contesto della prova tramite
ProofLocation::ContextStateAccount, per inizializzare lo statoConfidentialTransferAccount.
Solo il proprietario del token account può configurare un token account per trasferimenti confidenziali.
L'istruzione ConfigureAccount richiede la generazione lato client di
chiavi di cifratura e una prova che può essere generata solo dal proprietario
del token account.
La prova di validità del pubkey verifica che la chiave pubblica ElGamal
dell'account sia valida. Viene generata con
build_pubkey_validity_proof_data, verificata on-chain dal programma ZK
ElGamal Proof in un account di stato di contesto, e poi referenziata da
ConfigureAccount tramite ProofLocation::ContextStateAccount, in modo
che nessun byte di prova transiti nell'istruzione token stessa. Per i dettagli
di implementazione, vedere:
Codice di Esempio
Il seguente codice crea un associated token account e lo configura per i trasferimenti riservati su un mint riservato esistente.
I trasferimenti riservati dipendono dal programma ZK ElGamal Proof, abilitato su
mainnet e devnet. Un solana-test-validator standard non lo abilita, ma un
validator locale con fork di mainnet come Surfpool sì.
Esegui l'esempio su uno di questi (il codice usa devnet) con un payer
finanziato, e sostituisci il segnaposto del mint con un mint creato seguendo
Crea 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}`);
Gli helper TypeScript si trovano nel sottopath
@solana-program/token-2022/confidential e si basano su @solana/zk-sdk per
le primitive di cifratura. owner e client provengono dalla tua
configurazione @solana/kit; il piano di istruzioni restituito viene inviato
tramite il supporto ai piani di istruzioni di @solana/kit, che suddivide il
lavoro su più transazioni quando le prove sono troppo grandi per una sola.
Is this page helpful?