Trasferire token

Come trasferire token in modo confidenziale da un token account a un altro

Per trasferire token in modo confidenziale da un token account a un altro, sia il mittente che il destinatario devono avere token account configurati con lo stato ConfidentialTransferAccount e approvati per i trasferimenti confidenziali. Il token account del mittente deve inoltre disporre di un saldo confidenziale disponibile da cui effettuare il trasferimento.

Per trasferire token in modo confidenziale:

  1. Creare tre prove lato client:

    Proof di Uguaglianza (CiphertextCommitmentEqualityProofData): Verifica che il nuovo testo cifrato del saldo disponibile dopo il trasferimento corrisponda al relativo impegno di Pedersen, assicurando che il nuovo saldo disponibile dell'account sorgente sia calcolato correttamente come new_balance = current_balance - transfer_amount.

    Proof di Validità del Testo Cifrato (BatchedGroupedCiphertext3HandlesValidityProofData): Verifica che i testi cifrati dell'importo del trasferimento siano generati correttamente per tutte e tre le parti (sorgente, destinazione e revisore), assicurando che l'importo del trasferimento sia correttamente cifrato con la chiave pubblica di ciascuna parte.

    Proof di Intervallo (BatchedRangeProofU128Data): Verifica che il nuovo saldo disponibile e l'importo del trasferimento (suddiviso in bit bassi/alti) siano tutti non negativi e compresi in un intervallo specificato.

  2. Per ogni prova:

    • Invocare il programma di prove ZK ElGamal per verificare i dati della prova.
    • Memorizzare i metadati specifici della prova in un account "context state" della prova da utilizzare in altre istruzioni.
  3. Invocare l'istruzione ConfidentialTransferInstruction::Transfer fornendo gli account di context state delle prove.

  4. Chiudere gli account di context state delle prove per recuperare il SOL utilizzato per crearli.

Il seguente diagramma illustra i passaggi necessari per trasferire token dal token account del mittente al token account del destinatario.

Transfer Tokens

Istruzioni Necessarie

Per trasferire token in modo confidenziale da un token account a un altro, è necessario:

  • Generare una proof di uguaglianza, una proof di validità del testo cifrato e una proof di intervallo lato client
  • Invocare il programma di prove Zk ElGamal per verificare le prove e inizializzare gli account "context state"
  • Invocare l'istruzione ConfidentialTransferInstruction::Transfer fornendo i tre account delle prove.
  • Chiudere i tre account delle prove per recuperare il rent.

L'esempio Rust qui sotto genera le prove con il crate spl-token-confidential-transfer-proof-generation, verifica ciascuna in un account di stato del contesto tramite il programma ZK ElGamal Proof, fa riferimento ai tre account nell'istruzione di trasferimento e li chiude in seguito. L'esempio TypeScript utilizza l'helper getConfidentialTransferInstructionPlan da @solana-program/token-2022/confidential, che assembla gli stessi account di prova, il trasferimento e la chiusura come piano di istruzioni multi-transazione.

Codice di Esempio

L'esempio seguente trasferisce token in modo riservato da un account a un altro. Entrambi gli account devono essere già configurati per i trasferimenti riservati e il mittente deve disporre di un saldo riservato disponibile.

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 della mainnet come Surfpool sì. Esegui l'esempio su uno di questi (il codice usa devnet) con un payer finanziato e sostituisci i segnaposto con il tuo mint e gli account del mittente e del destinatario.

Rust

const ZK_PROOF_PROGRAM_ID: Pubkey =
solana_pubkey::pubkey!("ZkE1Gama1Proof11111111111111111111111111111");
fn main() -> Result<()> {
let rpc_client = RpcClient::new_with_commitment(
String::from("https://api.devnet.solana.com"),
CommitmentConfig::confirmed(),
);
// Sender = fee payer = token account owner. Both the sender and recipient
// accounts must already be configured for confidential transfers, and the
// sender must have an available confidential balance (deposit then apply
// pending balance beforehand).
let sender = load_keypair()?;
let amount: u64 = 100;
// Setup: create confidential accounts and fund the sender.
let recipient_keypair = Keypair::new();
let (mint, sender_token_account, recipient_token_account) =
setup_transfer_accounts(&rpc_client, &sender, &recipient_keypair, amount)?;
// Read the recipient's ElGamal public key from their confidential account.
let recipient_acc = rpc_client.get_account(&recipient_token_account)?;
let recipient_state = StateWithExtensions::<TokenAccount>::unpack(&recipient_acc.data)?;
let recipient_elgamal_pubkey: ElGamalPubkey = recipient_state
.get_extension::<ConfidentialTransferAccount>()?
.elgamal_pubkey
.try_into()
.map_err(|e| anyhow::anyhow!("recipient ElGamal pubkey: {e:?}"))?;
// Read the optional auditor ElGamal public key from the mint.
let mint_acc = rpc_client.get_account(&mint)?;
let mint_state = StateWithExtensions::<Mint>::unpack(&mint_acc.data)?;
let mint_ext = mint_state.get_extension::<ConfidentialTransferMint>()?;
let auditor_elgamal_pubkey: Option<ElGamalPubkey> =
Option::<PodElGamalPubkey>::from(mint_ext.auditor_elgamal_pubkey)
.map(|pod| {
ElGamalPubkey::try_from(pod).map_err(|e| anyhow::anyhow!("auditor pubkey: {e:?}"))
})
.transpose()?;
// Derive the sender's keys and read their current confidential balance.
let (sender_elgamal, sender_aes) =
derive_confidential_keys(&sender, &sender_token_account.to_bytes())
.map_err(|e| anyhow::anyhow!("derive confidential keys: {e}"))?;
let sender_acc = rpc_client.get_account(&sender_token_account)?;
let sender_state = StateWithExtensions::<TokenAccount>::unpack(&sender_acc.data)?;
let sender_ext = sender_state.get_extension::<ConfidentialTransferAccount>()?;
let current_available: ElGamalCiphertext = sender_ext
.available_balance
.try_into()
.map_err(|e| anyhow::anyhow!("available balance: {e:?}"))?;
let current_decryptable: AeCiphertext = sender_ext
.decryptable_available_balance
.try_into()
.map_err(|e| anyhow::anyhow!("decryptable balance: {e:?}"))?;
// Generate the three transfer proofs (equality, ciphertext-validity, range).
let proof_data = transfer_split_proof_data(
&current_available,
&current_decryptable,
amount,
&sender_elgamal,
&sender_aes,
&recipient_elgamal_pubkey,
auditor_elgamal_pubkey.as_ref(),
)
.map_err(|e| anyhow::anyhow!("transfer_split_proof_data: {e}"))?;
// Create one context state account per proof, owned by the ZK program.
let equality_account = Keypair::new();
let validity_account = Keypair::new();
let range_account = Keypair::new();
let equality_size = size_of::<ProofContextState<CiphertextCommitmentEqualityProofContext>>();
let validity_size =
size_of::<ProofContextState<BatchedGroupedCiphertext3HandlesValidityProofContext>>();
let range_size = size_of::<ProofContextState<BatchedRangeProofContext>>();
let create = |account: &Keypair, space: usize| -> Result<Instruction> {
Ok(system_instruction::create_account(
&sender.pubkey(),
&account.pubkey(),
rpc_client.get_minimum_balance_for_rent_exemption(space)?,
space as u64,
&ZK_PROOF_PROGRAM_ID,
))
};
let equality_create_ix = create(&equality_account, equality_size)?;
let validity_create_ix = create(&validity_account, validity_size)?;
let range_create_ix = create(&range_account, range_size)?;
// The sender is the context-state authority for all three proof accounts.
let authority: Address = sender.pubkey().to_bytes().into();
let equality_verify_ix = ProofInstruction::VerifyCiphertextCommitmentEquality
.encode_verify_proof(
Some(ContextStateInfo {
context_state_account: &Address::from(equality_account.pubkey().to_bytes()),
context_state_authority: &authority,
}),
&proof_data.equality_proof_data,
);
let validity_verify_ix = ProofInstruction::VerifyBatchedGroupedCiphertext3HandlesValidity
.encode_verify_proof(
Some(ContextStateInfo {
context_state_account: &Address::from(validity_account.pubkey().to_bytes()),
context_state_authority: &authority,
}),
&proof_data
.ciphertext_validity_proof_data_with_ciphertext
.proof_data,
);
let range_verify_ix = ProofInstruction::VerifyBatchedRangeProofU128.encode_verify_proof(
Some(ContextStateInfo {
context_state_account: &Address::from(range_account.pubkey().to_bytes()),
context_state_authority: &authority,
}),
&proof_data.range_proof_data,
);
// Transaction 1: create all three accounts and verify the validity proof.
send_tx(
&rpc_client,
&[
equality_create_ix,
validity_create_ix,
range_create_ix,
validity_verify_ix,
],
&[&sender, &equality_account, &validity_account, &range_account],
)?;
// Transaction 2: verify the range proof (the largest, on its own).
send_tx(&rpc_client, &[range_verify_ix], &[&sender])?;
// Compute the sender's new decryptable available balance after the transfer.
let current_plaintext = current_decryptable
.decrypt(&sender_aes)
.context("decrypt available balance")?;
let new_plaintext = current_plaintext
.checked_sub(amount)
.context("insufficient available balance")?;
let new_decryptable: PodAeCiphertext = sender_aes.encrypt(new_plaintext).into();
let auditor_lo = proof_data
.ciphertext_validity_proof_data_with_ciphertext
.ciphertext_lo;
let auditor_hi = proof_data
.ciphertext_validity_proof_data_with_ciphertext
.ciphertext_hi;
let transfer_ix = inner_transfer(
&spl_token_2022::id(),
&sender_token_account,
&mint,
&recipient_token_account,
&new_decryptable,
&auditor_lo,
&auditor_hi,
&sender.pubkey(),
&[],
ProofLocation::ContextStateAccount(&equality_account.pubkey()),
ProofLocation::ContextStateAccount(&validity_account.pubkey()),
ProofLocation::ContextStateAccount(&range_account.pubkey()),
)?;
// Transaction 3: verify the equality proof, run the transfer, and close the
// three proof accounts to reclaim their rent.
let close = |account: &Keypair| {
close_context_state(
ContextStateInfo {
context_state_account: &Address::from(account.pubkey().to_bytes()),
context_state_authority: &authority,
},
&authority,
)
};
let instructions = [
equality_verify_ix,
transfer_ix,
close(&equality_account),
close(&validity_account),
close(&range_account),
];
let blockhash = rpc_client.get_latest_blockhash()?;
let transaction = Transaction::new_signed_with_payer(
&instructions,
Some(&sender.pubkey()),
&[&sender],
blockhash,
);
let signature = rpc_client.send_and_confirm_transaction(&transaction)?;
println!("Transferred {amount} tokens confidentially: {signature}");
Ok(())
}

Typescript

const client = await createClient()
.use(signerFromFile(join(homedir(), ".config/solana/id.json")))
.use(
solanaRpc({
rpcUrl: "https://api.devnet.solana.com",
maxConcurrency: 1
})
)
// Temporary custom plugin to skip the default compute-budget estimate
// so proof instructions fit within the transaction message cap.
// The Solana CLI default keypair, used as fee payer, mint authority, and sender.
const owner = client.payer;
const recipient = await generateKeyPairSigner();
const depositAmount = 100n;
const amount = 25n;
const decimals = 2;
// Setup: create source and destination confidential accounts, then fund source.
const mint = await createConfidentialMint(client, owner, decimals);
const auditorElgamalPubkey = await getAuditorElgamalPubkey(client, mint);
const sourceToken = await createConfidentialTokenAccount(client, owner, mint);
const destinationToken = await createConfidentialTokenAccount(
client,
recipient,
mint
);
await mintPublicTokens(client, owner, mint, sourceToken, depositAmount);
await depositTokens(client, owner, mint, sourceToken, depositAmount, decimals);
await applyPendingBalance(client, owner, mint, sourceToken);
// Derive the sender's recoverable ElGamal and AES keys, bound to (owner, mint).
const { elgamalKeypair: sourceElgamalKeypair, aesKey } =
await deriveConfidentialKeys(owner, mint);
// The helper reads the recipient key from the destination account; pass the
// configured auditor key so the proof matches the mint configuration.
const sourceTokenAccount = (await fetchToken(client.rpc, sourceToken)).data;
const destinationTokenAccount = (await fetchToken(client.rpc, destinationToken))
.data;
// Builds the proof context-state accounts, the transfer, and the closes as a
// multi-transaction plan (the three proofs are too large for one transaction).
const plan = await getConfidentialTransferInstructionPlan({
rpc: client.rpc,
payer: owner,
authority: owner,
mint,
sourceToken,
sourceTokenAccount,
destinationToken,
destinationTokenAccount,
auditorElgamalPubkey,
amount,
sourceElgamalKeypair,
aesKey
});
const result = await client.sendTransactions(plan);
const summary = summarizeTransactionPlanResult(result);
const signature =
summary.successfulTransactions[summary.successfulTransactions.length - 1]
.context.signature;
console.log(`Transferred ${amount} tokens confidentially: ${signature}`);

Is this page helpful?

Indice dei contenuti

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