كيفية نقل الرموز بشكل سري من token account إلى آخر
لنقل الرموز بشكل سري من token account إلى آخر، يجب على كلٍّ من المُرسِل والمستلم
امتلاك token accounts مُهيَّأة بحالة ConfidentialTransferAccount ومُعتمَدة
لعمليات النقل السرية. كما يجب أن يمتلك token account الخاص بالمُرسِل رصيداً
سرياً متاحاً للتحويل منه.
لنقل الرموز بشكل سري:
-
أنشئ ثلاثة إثباتات على جانب العميل:
إثبات المساواة (CiphertextCommitmentEqualityProofData): يتحقق من أن نص التشفير الخاص بالرصيد المتاح الجديد بعد عملية النقل يتطابق مع التزام Pedersen المقابل له، مما يضمن أن الرصيد المتاح الجديد لحساب المصدر محسوبٌ بشكل صحيح على النحو
new_balance = current_balance - transfer_amount.إثبات صحة نص التشفير (BatchedGroupedCiphertext3HandlesValidityProofData): يتحقق من أن نصوص تشفير مبلغ التحويل مُولَّدة بشكل صحيح لجميع الأطراف الثلاثة (المصدر والوجهة والمراجع)، مما يضمن تشفير مبلغ التحويل بشكل صحيح باستخدام المفتاح العام لكل طرف.
إثبات النطاق (BatchedRangeProofU128Data): يتحقق من أن الرصيد المتاح الجديد ومبلغ التحويل (المقسَّم إلى بتات منخفضة/عالية) كلها غير سالبة وضمن نطاق محدد.
-
لكل إثبات:
- استدعِ برنامج إثبات ZK ElGamal للتحقق من بيانات الإثبات.
- خزِّن البيانات الوصفية الخاصة بالإثبات في حساب "حالة السياق" للاستخدام في التعليمات الأخرى.
-
استدعِ تعليمة ConfidentialTransferInstruction::Transfer مع توفير حسابات حالة سياق الإثبات.
-
أغلق حسابات حالة سياق الإثبات لاسترداد SOL المُستخدَم في إنشائها.
يوضح المخطط التالي الخطوات المتبعة في نقل الرموز من token account الخاص بالمُرسِل إلى token account الخاص بالمستلم.
التعليمات المطلوبة
لنقل الرموز بشكل سري من token account إلى آخر، يجب عليك:
- إنشاء إثبات مساواة وإثبات صحة نص تشفير وإثبات نطاق على جانب العميل
- استدعاء برنامج إثبات Zk ElGamal للتحقق من الإثباتات وتهيئة حسابات "حالة السياق"
- استدعاء تعليمة ConfidentialTransferInstruction::Transfer مع توفير حسابات الإثبات الثلاثة.
- إغلاق حسابات الإثبات الثلاثة لاسترداد rent.
يقوم مثال Rust أدناه بتوليد الإثباتات باستخدام الـ
spl-token-confidential-transfer-proof-generation، والتحقق من كل منها في حساب
حالة السياق عبر برنامج ZK ElGamal Proof، والإشارة إلى الحسابات الثلاثة في تعليمة
النقل، وإغلاقها بعد ذلك. يستخدم مثال TypeScript المساعد
getConfidentialTransferInstructionPlan من
@solana-program/token-2022/confidential، الذي يجمع حسابات الإثبات والنقل
والإغلاق نفسها في خطة تعليمات متعددة المعاملات.
مثال على الكود
يوضح المثال التالي كيفية نقل الرموز بشكل سري من حساب إلى آخر. يجب أن يكون كلا الحسابين مُهيَّأين مسبقًا للتحويلات السرية، ويجب أن يمتلك المرسل رصيدًا سريًا متاحًا.
تعتمد التحويلات السرية على برنامج ZK ElGamal Proof، وهو مُفعَّل على الشبكة
الرئيسية وشبكة devnet. لا يقوم الـ solana-test-validator الافتراضي بتفعيله،
غير أن validator محلي يعمل بنسخة مطابقة للشبكة الرئيسية مثل
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(¤t_available,¤t_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?