Creare un token account

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:

Create Token Account with Confidential Transfer Extension

Stato del Token Account con Confidential Transfer

L'estensione aggiunge lo stato 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,
}

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_accounts del mint account è impostata come true, 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 ApplyPendingBalance per convertire il saldo in sospeso nel saldo disponibile.

  • expected_pending_balance_credit_counter: Il valore pending_balance_credit_counter fornito dal client tramite instruction data l'ultima volta che l'istruzione ApplyPendingBalance è stata elaborata.

  • actual_pending_balance_credit_counter: Il valore pending_balance_credit_counter sul token account al momento in cui l'ultima istruzione ApplyPendingBalance è 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:

  1. Il client recupera i saldi in sospeso e disponibili correnti, ne crittografa la somma e fornisce un decryptable_available_balance crittografato utilizzando la chiave AES del proprietario del token account.

  2. I contatori di credito in sospeso attesi e effettivi tracciano le modifiche al valore del contatore tra il momento in cui l'istruzione ApplyPendingBalance viene creata e quello in cui viene elaborata:

    • expected_pending_balance_credit_counter: Il valore pending_balance_credit_counter quando il client crea l'istruzione ApplyPendingBalance
    • actual_pending_balance_credit_counter: Il valore pending_balance_credit_counter sul token account al momento in cui l'istruzione ApplyPendingBalance viene 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:

  1. Iniziare con decryptable_available_balance dal token account
  2. 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:

  1. Creare il Token Account: Invocare l'istruzione AssociatedTokenAccountInstruction::Create dell'Associated Token Program per creare il token account al suo indirizzo deterministico.

  2. Riallocare lo Spazio dell'Account: Invocare l'istruzione TokenInstruction::Reallocate del Token Extension Program per aggiungere spazio per lo stato ConfidentialTransferAccount.

  3. Verificare la Prova di Validità del Pubkey: Creare un account di proprietà del programma ZK ElGamal Proof, quindi invocare la sua istruzione VerifyPubkeyValidity per verificare la prova e memorizzare il risultato verificato in quell'account di stato di contesto.

  4. 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 stato ConfidentialTransferAccount.

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?

Indice dei contenuti

Modifica pagina
© 2026 Solana Foundation. Tutti i diritti riservati.