Переказ токенів

Як конфіденційно перенести токени з одного token account на інший

Щоб конфіденційно перенести токени з одного token account на інший, як відправник, так і одержувач повинні мати token account, налаштовані зі станом ConfidentialTransferAccount та затверджені для конфіденційних переказів. Також token account відправника повинен мати доступний конфіденційний баланс для переказу.

Щоб перенести токени конфіденційно:

  1. Створіть три докази на стороні клієнта:

    Доказ рівності (CiphertextCommitmentEqualityProofData): Перевіряє, що новий шифротекст доступного балансу після переказу відповідає відповідному зобов'язанню Педерсена, гарантуючи, що новий доступний баланс вихідного рахунку правильно обчислено як new_balance = current_balance - transfer_amount.

    Доказ дійсності шифротексту (BatchedGroupedCiphertext3HandlesValidityProofData): Перевіряє, що шифротексти суми переказу правильно сформовані для всіх трьох сторін (відправника, одержувача та аудитора), гарантуючи, що сума переказу правильно зашифрована під публічним ключем кожної сторони.

    Доказ діапазону (BatchedRangeProofU128Data): Перевіряє, що новий доступний баланс і сума переказу (розбита на молодші/старші біти) є невід'ємними та знаходяться в заданому діапазоні.

  2. Для кожного доказу:

    • Викличте програму ZK ElGamal proof для перевірки даних доказу.
    • Збережіть специфічні для доказу метадані в обліковому записі "context state" для використання в інших інструкціях.
  3. Викличте інструкцію ConfidentialTransferInstruction::Transfer, надавши облікові записи контексту стану доказів.

  4. Закрийте облікові записи контексту стану доказів, щоб повернути SOL, витрачений на їх створення.

Наступна діаграма показує кроки, пов'язані з переказом токенів з token account відправника на token account одержувача.

Transfer Tokens

Необхідні інструкції

Щоб конфіденційно перенести токени з одного token account на інший, необхідно:

  • Згенерувати доказ рівності, доказ дійсності шифротексту та доказ діапазону на стороні клієнта
  • Викликати програму Zk ElGamal proof для перевірки доказів та ініціалізації облікових записів "context state"
  • Викликати інструкцію ConfidentialTransferInstruction::Transfer, надавши три облікові записи доказів.
  • Закрити три облікові записи доказів для повернення rent.

Наведений нижче приклад на Rust генерує докази за допомогою крейту spl-token-confidential-transfer-proof-generation, перевіряє кожен із них в обліковий запис стану контексту через програму ZK ElGamal Proof, посилається на три облікові записи в інструкції переказу та закриває їх після завершення. Приклад на TypeScript використовує допоміжну функцію getConfidentialTransferInstructionPlan з @solana-program/token-2022/confidential, яка формує ті самі облікові записи доказів, переказ і закриття як план інструкцій у кількох транзакціях.

Приклад коду

Наведений нижче приклад конфіденційно переказує токени з одного облікового запису на інший. Обидва облікові записи вже мають бути налаштовані для конфіденційних переказів, а відправник повинен мати доступний конфіденційний баланс.

Конфіденційні перекази залежать від програми ZK ElGamal Proof, яка увімкнена в mainnet і devnet. Стандартний solana-test-validator не вмикає її, але локальний validator із форком mainnet, наприклад Surfpool, вмикає. Запустіть приклад проти одного з них (у коді використовується devnet) з профінансованим платником та замініть заповнювачі своїм mint і обліковими записами відправника та одержувача.

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?

Зміст

Редагувати сторінку