إنشاء حساب رمز مميز

كيفية إنشاء token account مع امتداد النقل السري

يُتيح امتداد النقل السري إجراء تحويلات رمزية خاصة عن طريق إضافة حالة إضافية إلى token account. يشرح هذا القسم كيفية إنشاء token account مع تفعيل هذا الامتداد.

يوضح المخطط التالي الخطوات المتبعة في إنشاء token account مع امتداد النقل السري:

Create Token Account with Confidential Transfer Extension

حالة token account للنقل السري

يُضيف الامتداد حالة ConfidentialTransferAccount إلى 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,
}

يحتوي ConfidentialTransferAccount على عدة حقول لإدارة النقل السري:

  • approved: حالة الموافقة على الحساب لإجراء النقل السري. إذا كان إعداد auto_approve_new_accounts الخاص بـ mint account مضبوطاً على true، فسيتم اعتماد جميع token accounts تلقائياً لإجراء النقل السري.

  • elgamal_pubkey: مفتاح ElGamal العام المستخدم لتشفير الأرصدة ومبالغ التحويل.

  • pending_balance_lo: الـ 16 بت السفلية المشفرة من الرصيد المعلق. يتم تقسيم الرصيد إلى أجزاء عليا وسفلى لتسهيل فك التشفير.

  • pending_balance_hi: الـ 48 بت العلوية المشفرة من الرصيد المعلق. يتم تقسيم الرصيد إلى أجزاء عليا وسفلى لتسهيل فك التشفير.

  • available_balance: الرصيد المشفر المتاح للتحويلات.

  • decryptable_available_balance: الرصيد المتاح مشفراً بمفتاح معيار التشفير المتقدم (AES) لتسهيل فك التشفير من قِبل مالك الحساب.

  • allow_confidential_credits: إذا كانت القيمة true، فإنه يسمح بالنقل السري الوارد.

  • allow_non_confidential_credits: إذا كانت القيمة true، فإنه يسمح بالنقل غير السري الوارد.

  • pending_balance_credit_counter: يحسب ائتمانات الرصيد المعلق الواردة من تعليمات الإيداع والتحويل.

  • maximum_pending_balance_credit_counter: حد العدد للائتمانات المعلقة قبل الاشتراط بتعليمة ApplyPendingBalance لتحويل الرصيد المعلق إلى الرصيد المتاح.

  • expected_pending_balance_credit_counter: قيمة pending_balance_credit_counter التي يوفرها العميل من خلال instruction data في آخر مرة تمت فيها معالجة تعليمة ApplyPendingBalance.

  • actual_pending_balance_credit_counter: قيمة pending_balance_credit_counter على token account في وقت معالجة آخر تعليمة ApplyPendingBalance.

الرصيد المعلق مقابل الرصيد المتاح

تنقسم الأرصدة السرية إلى أرصدة معلقة وأرصدة متاحة للحماية من هجمات حجب الخدمة (DoS). بدون هذا الفصل، يمكن للمهاجم إرسال الرموز المميزة بشكل متكرر إلى token account، مما يعيق قدرة مالك token account على تحويل الرموز المميزة. لن يتمكن مالك token account من تحويل الرموز المميزة لأن الرصيد المشفر سيتغير بين وقت إرسال المعاملة ووقت معالجتها، مما يؤدي إلى فشل المعاملة.

تُضاف جميع مبالغ الإيداع والتحويل في البداية إلى الرصيد المعلق. يجب على أصحاب token account استخدام تعليمة ApplyPendingBalance لتحويل الرصيد المعلق إلى الرصيد المتاح. لا تؤثر التحويلات أو الإيداعات الواردة على الرصيد المتاح لـ token account.

تقسيم الرصيد المعلق إلى قيمة عالية/منخفضة

يُقسَّم الرصيد المعلق السري إلى pending_balance_lo وpending_balance_hi لأن فك تشفير ElGamal يتطلب قدرًا أكبر من الحسابات للأعداد الكبيرة. يمكنك الاطلاع على تنفيذ العمليات الحسابية على النص المشفر هنا، والذي يُستخدم في تعليمة ApplyPendingBalance هنا.

عدادات ائتمان الرصيد المعلق

عند استدعاء تعليمة ApplyPendingBalance لتحويل الرصيد المعلق إلى الرصيد المتاح:

  1. يبحث العميل عن الأرصدة المعلقة والمتاحة الحالية، ويشفّر مجموعها، ويوفر decryptable_available_balance مشفرًا باستخدام مفتاح AES الخاص بمالك token account.

  2. يتتبع عدادا الائتمان المعلق المتوقع والفعلي التغييرات في قيمة العداد بين وقت إنشاء تعليمة ApplyPendingBalance ووقت معالجتها:

    • expected_pending_balance_credit_counter: قيمة pending_balance_credit_counter عندما ينشئ العميل تعليمة ApplyPendingBalance
    • actual_pending_balance_credit_counter: قيمة pending_balance_credit_counter على token account في وقت معالجة تعليمة ApplyPendingBalance

تشير قيم العدادات المتطابقة (المتوقعة/الفعلية) إلى أن decryptable_available_balance يتطابق مع available_balance.

عند جلب حالة token account لقراءة decryptable_available_balance، تستلزم القيم المختلفة للعدادات المتوقعة/الفعلية من العميل البحث عن تعليمات الإيداع/التحويل الأخيرة المقابلة لفارق العداد لحساب الرصيد الصحيح.

عملية تسوية الرصيد

عندما تختلف عدادات الرصيد المعلق المتوقعة والفعلية، اتبع هذه الخطوات لتسوية decryptable_available_balance:

  1. ابدأ بـ decryptable_available_balance من token account
  2. اجلب أحدث المعاملات المتضمنة لتعليمات الإيداع والتحويل حتى فارق العداد (الفعلي - المتوقع):
    • أضف المبالغ العلنية من تعليمات الإيداع
    • فكّ تشفير وأضف مبالغ النص المشفر للوجهة من تعليمات التحويل

التعليمات المطلوبة

يستخدم إنشاء token account وتهيئته للتحويلات السرية التعليمات التالية، والتي تتناسب جميعها في معاملة واحدة:

  1. إنشاء Token Account: استدعاء تعليمة AssociatedTokenAccountInstruction::Create الخاصة بـ Associated Token Program لإنشاء token account على عنوانه الحتمي.

  2. إعادة تخصيص مساحة الحساب: استدعاء تعليمة TokenInstruction::Reallocate الخاصة بـ Token Extensions Program لإضافة مساحة لحالة ConfidentialTransferAccount.

  3. التحقق من صحة إثبات pubkey: إنشاء حساب مملوك لبرنامج ZK ElGamal Proof، ثم استدعاء تعليمة VerifyPubkeyValidity الخاصة به للتحقق من الإثبات وتخزين النتيجة المتحقق منها في حساب حالة السياق ذلك.

  4. تهيئة التحويلات السرية: استدعاء تعليمة ConfidentialTransferInstruction::ConfigureAccount الخاصة بـ Token Extensions Program، مع الإشارة إلى حساب حالة سياق الإثبات عبر ProofLocation::ContextStateAccount، لتهيئة حالة ConfidentialTransferAccount.

يمكن لمالك حساب الرمز المميز فقط تكوين حساب الرمز المميز للتحويلات السرية.

تستلزم تعليمة ConfigureAccount توليد مفاتيح التشفير وإثبات من جانب العميل، ولا يمكن توليده إلا من قِبَل مالك token account.

يتحقق إثبات صحة pubkey من أن مفتاح ElGamal العام للحساب صالح. يتم توليده باستخدام build_pubkey_validity_proof_data، والتحقق منه على السلسلة بواسطة برنامج ZK ElGamal Proof في حساب حالة السياق، ثم الإشارة إليه من ConfigureAccount عبر ProofLocation::ContextStateAccount، بحيث لا تنتقل أي بيانات إثبات في تعليمة التوكن نفسها. للاطلاع على تفاصيل التنفيذ، راجع:

مثال على الكود

الكود التالي ينشئ associated token account ويُهيّئه لعمليات النقل السرية مقابل mint سري موجود مسبقاً.

تعتمد عمليات النقل السرية على برنامج ZK ElGamal Proof، المُفعَّل على mainnet وdevnet. لا يُفعِّله solana-test-validator الافتراضي، لكن validator محلي يعمل بنسخة متفرّعة من mainnet مثل Surfpool يُفعِّله. قم بتشغيل المثال على أحد هذه البيئات (يستخدم الكود devnet) مع حساب دافع ممول، واستبدل العنصر النائب للـ mint بـ mint تم إنشاؤه وفق إنشاء 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}`
);

تتوفر مساعدات TypeScript في المسار الفرعي @solana-program/token-2022/confidential وتعتمد على @solana/zk-sdk لعناصر التشفير الأساسية. يأتي owner و client من إعداد @solana/kit الخاص بك؛ ويتم إرسال خطة التعليمات المُعادة مع دعم خطط التعليمات في @solana/kit، الذي يوزّع العمل عبر معاملات متعددة عندما تكون الإثباتات أكبر من أن تتسع لمعاملة واحدة.

Is this page helpful?

جدول المحتويات

تعديل الصفحة