Как вывести токены из конфиденциального доступного баланса
Чтобы вывести токены из конфиденциального доступного баланса на публичный баланс:
-
Создайте два доказательства на стороне клиента:
Доказательство равенства (CiphertextCommitmentEqualityProofData): Проверяет, что зашифрованный текст оставшегося доступного баланса после вывода соответствует соответствующему обязательству Педерсена, гарантируя, что новый доступный баланс аккаунта корректно вычислен как
remaining_balance = current_balance - withdraw_amount.Доказательство диапазона (BatchedRangeProofU64Data): Проверяет, что оставшийся доступный баланс после вывода является неотрицательным и находится в заданном диапазоне.
-
Для каждого доказательства:
- Вызовите программу ZK ElGamal для проверки данных доказательства.
- Сохраните специфичные метаданные доказательства в аккаунте «контекстного состояния» для использования в других инструкциях.
-
Вызовите инструкцию ConfidentialTransferInstruction::Withdraw, передав два аккаунта доказательств.
-
Закройте аккаунты доказательств, чтобы вернуть SOL, использованный для их создания.
Следующая диаграмма показывает шаги, связанные с выводом токенов из конфиденциального доступного баланса на публичный баланс:
Необходимые инструкции
Чтобы вывести токены из конфиденциального доступного баланса на публичный баланс, необходимо:
- Сгенерировать доказательство равенства и доказательство диапазона на стороне клиента
- Вызвать программу ZK ElGamal для проверки доказательств и инициализации аккаунтов «контекстного состояния»
- Вызвать инструкцию ConfidentialTransferInstruction::Withdraw, передав два аккаунта доказательств.
- Закрыть два аккаунта доказательств, чтобы вернуть rent.
Пример на Rust, приведённый ниже, генерирует доказательства с помощью крейта
spl-token-confidential-transfer-proof-generation, проверяет каждое из них в
аккаунте контекстного состояния через программу ZK ElGamal Proof, ссылается на
оба аккаунта в инструкции вывода и закрывает их после завершения. Пример на
TypeScript использует вспомогательную функцию
getConfidentialWithdrawInstructionPlan из
@solana-program/token-2022/confidential, которая формирует те же аккаунты
доказательств, вывод и закрытие в виде плана инструкций с несколькими
транзакциями.
Пример кода
В следующем примере токены выводятся из конфиденциального доступного баланса обратно в публичный баланс. Аккаунт должен быть уже настроен для конфиденциальных переводов и иметь доступный конфиденциальный баланс.
Конфиденциальные переводы зависят от программы ZK ElGamal Proof, которая
включена в mainnet и devnet. Стандартный solana-test-validator не активирует
её, однако локальный validator с форком mainnet, например
Surfpool, поддерживает её. Запустите пример на одной из
этих сред (в коде используется devnet) с пополненным счётом плательщика и
замените заполнители своим минтом и token account.
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(),);// Owner = fee payer = token account owner. The account must already be// configured for confidential transfers with an available confidential// balance to withdraw from.let owner = load_keypair()?;let amount: u64 = 100;let decimals: u8 = 2;// Setup: create an available confidential balance to withdraw.let (mint, token_account) = setup_withdrawable_account(&rpc_client, &owner, amount, decimals)?;// Derive the owner's keys and read the current confidential available balance.let (elgamal_keypair, aes_key) = derive_confidential_keys(&owner, &token_account.to_bytes()).map_err(|e| anyhow::anyhow!("derive confidential keys: {e}"))?;let account_data = rpc_client.get_account(&token_account)?;let account = StateWithExtensions::<TokenAccount>::unpack(&account_data.data)?;let ct_extension = account.get_extension::<ConfidentialTransferAccount>()?;// The ElGamal available-balance ciphertext is required to build the proof.let available_balance: ElGamalCiphertext = ct_extension.available_balance.try_into().map_err(|e| anyhow::anyhow!("decode available balance: {e:?}"))?;// Read the plaintext balance from the AES-encrypted decryptable balance.// ElGamal's decrypt_u32 only recovers values up to 2^32 raw units, so it// fails for realistic balances; the AES field has no such limit.let decryptable_balance: AeCiphertext = ct_extension.decryptable_available_balance.try_into().map_err(|e| anyhow::anyhow!("decode decryptable balance: {e:?}"))?;let current_available = decryptable_balance.decrypt(&aes_key).context("decrypt available balance")?;// Generate the equality and range proofs for the withdrawal.let proof_data = withdraw_proof_data(&available_balance, current_available, amount, &elgamal_keypair).map_err(|e| anyhow::anyhow!("withdraw_proof_data: {e}"))?;let new_available = current_available.checked_sub(amount).context("insufficient confidential balance")?;let new_decryptable: PodAeCiphertext = aes_key.encrypt(new_available).into();// The owner is the context-state authority for both proof accounts.let authority: Address = owner.pubkey().to_bytes().into();// Equality proof context state account.let equality_account = Keypair::new();let equality_size = size_of::<ProofContextState<CiphertextCommitmentEqualityProofContext>>();let equality_create_ix = system_instruction::create_account(&owner.pubkey(),&equality_account.pubkey(),rpc_client.get_minimum_balance_for_rent_exemption(equality_size)?,equality_size as u64,&ZK_PROOF_PROGRAM_ID,);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,);send_tx(&rpc_client, &[equality_create_ix], &[&owner, &equality_account])?;send_tx(&rpc_client, &[equality_verify_ix], &[&owner])?;// Range proof context state account.let range_account = Keypair::new();let range_size = size_of::<ProofContextState<BatchedRangeProofContext>>();let range_create_ix = system_instruction::create_account(&owner.pubkey(),&range_account.pubkey(),rpc_client.get_minimum_balance_for_rent_exemption(range_size)?,range_size as u64,&ZK_PROOF_PROGRAM_ID,);let range_verify_ix = ProofInstruction::VerifyBatchedRangeProofU64.encode_verify_proof(Some(ContextStateInfo {context_state_account: &Address::from(range_account.pubkey().to_bytes()),context_state_authority: &authority,}),&proof_data.range_proof_data,);send_tx(&rpc_client, &[range_create_ix], &[&owner, &range_account])?;send_tx(&rpc_client, &[range_verify_ix], &[&owner])?;// Withdraw, referencing both pre-verified proof accounts.let withdraw_ixs = withdraw(&spl_token_2022::id(),&token_account,&mint,amount,decimals,&new_decryptable,&owner.pubkey(),&[&owner.pubkey()],ProofLocation::ContextStateAccount(&equality_account.pubkey()),ProofLocation::ContextStateAccount(&range_account.pubkey()),)?;let blockhash = rpc_client.get_latest_blockhash()?;let transaction =Transaction::new_signed_with_payer(&withdraw_ixs, Some(&owner.pubkey()), &[&owner], blockhash);let signature = rpc_client.send_and_confirm_transaction(&transaction)?;// Close both proof accounts to reclaim their rent.for account in [&equality_account, &range_account] {let close_ix = close_context_state(ContextStateInfo {context_state_account: &Address::from(account.pubkey().to_bytes()),context_state_authority: &authority,},&authority,);send_tx(&rpc_client, &[close_ix], &[&owner])?;}println!("Withdrew {amount} tokens to the public balance: {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}));// The Solana CLI default keypair, used as fee payer, mint authority, and// token account owner.const owner = client.payer;const depositAmount = 100n;const amount = 25n;const decimals = 2;// Setup: create a confidential account with an available confidential balance.const mint = await createConfidentialMint(client, owner, decimals);const token = await createConfidentialTokenAccount(client, owner, mint);await mintPublicTokens(client, owner, mint, token, depositAmount);await depositTokens(client, owner, mint, token, depositAmount, decimals);await applyPendingBalance(client, owner, mint, token);// Derive the owner's recoverable ElGamal and AES keys, bound to (owner, mint).const { elgamalKeypair, aesKey } = await deriveConfidentialKeys(owner, mint);const tokenAccount = (await fetchToken(client.rpc, token)).data;// Builds the equality + range proof accounts, the withdraw, and the closes as a// multi-transaction instruction plan.const plan = await getConfidentialWithdrawInstructionPlan({rpc: client.rpc,payer: owner,authority: owner,token,mint,tokenAccount,amount,decimals,elgamalKeypair,aesKey});const result = await client.sendTransactions(plan);const summary = summarizeTransactionPlanResult(result);const signature =summary.successfulTransactions[summary.successfulTransactions.length - 1].context.signature;console.log(`Withdrew ${amount} tokens to the public balance: ${signature}`);
Is this page helpful?