بنية المعاملة

ملخص

تحتوي المعاملة على توقيعات + رسالة. تحتوي الرسالة على رأسية، وعناوين الحسابات، وتجزئة كتلة حديثة، وتعليمات مجمعة. الحد الأقصى للحجم المتسلسل: 1,232 بايت.

تحتوي Transaction على حقلين رئيسيين:

  • signatures: مصفوفة من التوقيعات
  • message: معلومات المعاملة، بما في ذلك قائمة التعليمات المراد معالجتها
Transaction
pub struct Transaction {
pub signatures: Vec<Signature>,
pub message: Message,
}

رسم تخطيطي يوضح الجزأين من المعاملةرسم تخطيطي يوضح الجزأين من المعاملة

يجب ألا يتجاوز الحجم الإجمالي المتسلسل للمعاملة PACKET_DATA_SIZE (1,232 بايت). يساوي هذا الحد 1,280 بايت (الحد الأدنى لـ IPv6 MTU) مطروحًا منه 48 بايت لرؤوس الشبكة (40 بايت IPv6 + 8 بايت رأسية الجزء). تشمل الـ 1,232 بايت كلاً من مصفوفة signatures وبنية message.

رسم تخطيطي يوضح تنسيق المعاملة وحدود الحجمرسم تخطيطي يوضح تنسيق المعاملة وحدود الحجم

التوقيعات

حقل signatures هو مصفوفة مشفرة بشكل مضغوط من قيم Signature. كل Signature هو توقيع Ed25519 بحجم 64 بايت لـ Message المتسلسل، موقع بالمفتاح الخاص لحساب الموقع. يلزم توقيع واحد لكل حساب موقع مشار إليه بواسطة تعليمات المعاملة.

يتم إنتاج كل توقيع بواسطة مفتاح خاص. يُعدّ مكان وجود ذلك المفتاح — سواء أكان keypair محليًا، أو وحدة أمان تشفير سحابية (HSM/KMS)، أو خدمة محفظة مُدارة — قرارًا تصميميًا على مستوى الإنتاج. راجع التوقيع في بيئة الإنتاج.

ينتمي التوقيع الأول في المصفوفة إلى دافع الرسوم، وهو الحساب الذي يدفع الرسوم الأساسية للمعاملة ورسوم الأولوية. يعمل هذا التوقيع الأول أيضًا بوصفه معرّف المعاملة، المستخدَم للبحث عن المعاملة على الشبكة. يُشار إلى معرّف المعاملة بشكل شائع باسم توقيع المعاملة.

متطلبات دافع الرسوم:

  • يجب أن يكون الحساب الأول في الرسالة (الفهرس 0) وأن يكون موقِّعًا.
  • يجب أن يكون حسابًا مملوكًا لـ System Program أو حساب nonce (يتم التحقق منه بواسطة validate_fee_payer).
  • يجب أن يحتوي على ما يكفي من lamport لتغطية rent_exempt_minimum + total_fee؛ وإلا فستفشل المعاملة مع InsufficientFundsForFee.

الرسالة

حقل message هو Message بنية تحتوي على حمولة المعاملة:

Message
pub struct Message {
/// The message header, identifying signed and read-only `account_keys`.
pub header: MessageHeader,
/// All the account keys used by this transaction.
#[serde(with = "short_vec")]
pub account_keys: Vec<Pubkey>,
/// The id of a recent ledger entry.
pub recent_blockhash: Hash,
/// Programs that will be executed in sequence and committed in
/// one atomic transaction if all succeed.
#[serde(with = "short_vec")]
pub instructions: Vec<CompiledInstruction>,
}

الترويسة

حقل header هو MessageHeader بنية تحتوي على ثلاثة حقول u8 تقسّم مصفوفة account_keys إلى مجموعات صلاحيات:

  • num_required_signatures: العدد الإجمالي للتوقيعات المطلوبة للمعاملة.
  • num_readonly_signed_accounts: عدد الحسابات الموقَّعة للقراءة فقط.
  • num_readonly_unsigned_accounts: عدد الحسابات غير الموقَّعة للقراءة فقط.
MessageHeader
pub struct MessageHeader {
/// The number of signatures required for this message to be considered
/// valid. The signers of those signatures must match the first
/// `num_required_signatures` of [`Message::account_keys`].
pub num_required_signatures: u8,
/// The last `num_readonly_signed_accounts` of the signed keys are read-only
/// accounts.
pub num_readonly_signed_accounts: u8,
/// The last `num_readonly_unsigned_accounts` of the unsigned keys are
/// read-only accounts.
pub num_readonly_unsigned_accounts: u8,
}

بادئات رسائل الإصدار القديم والرسائل ذات الإصدار

في رسالة معاملة قديمة، يكون البايت الأول من الرسالة هو num_required_signatures، يليه البايتان الآخران MessageHeader. أما في رسالة معاملة ذات إصدار، فإن البايت الأول يكون بادئة إصدار بدلاً من ذلك؛ ويبدأ MessageHeader المكوّن من ثلاثة بايتات مباشرةً بعد تلك البادئة. راجع المعاملات ذات الإصدار للاطلاع على التخطيط الكامل لرسالة v0.

مخطط يوضح الأجزاء الثلاثة لترويسة الرسالةمخطط يوضح الأجزاء الثلاثة لترويسة الرسالة

عناوين الحسابات

الحقل account_keys هو مصفوفة مشفرة بصيغة مضغوطة من المفاتيح العامة. يُحدد كل إدخال حساباً يستخدمه تعليمة واحدة على الأقل من تعليمات المعاملة. يجب أن تشتمل المصفوفة على جميع الحسابات وأن تتبع هذا الترتيب الصارم:

  1. موقّع + قابل للكتابة
  2. موقّع + للقراءة فقط
  3. غير موقّع + قابل للكتابة
  4. غير موقّع + للقراءة فقط

يتيح هذا الترتيب الصارم دمج مصفوفة account_keys مع العدادات الثلاثة في header الخاص بالرسالة لتحديد الصلاحيات لكل حساب دون الحاجة إلى تخزين أعلام بيانات وصفية لكل حساب. تُقسّم عدادات الترويسة المصفوفة إلى مجموعات الأذونات الأربع المذكورة أعلاه.

مخطط يوضح ترتيب مصفوفة عناوين الحساباتمخطط يوضح ترتيب مصفوفة عناوين الحسابات

الـ blockhash الأخير

الحقل recent_blockhash هو تجزئة بحجم 32 بايت تخدم غرضين:

  1. الطابع الزمني: يُثبت أن المعاملة تمت مؤخراً.
  2. منع التكرار: يمنع معالجة المعاملة ذاتها مرتين.

تنتهي صلاحية الـ blockhash بعد 150 slot. إذا لم يعد الـ blockhash صالحاً عند وصول المعاملة، فسيتم رفضها بالخطأ BlockhashNotFound، ما لم تكن معاملة nonce دائمة صالحة.

تتيح لك طريقة RPC getLatestBlockhash الحصول على الـ blockhash الحالي وآخر ارتفاع للكتلة الذي سيكون فيه الـ blockhash صالحاً.

التعليمات

حقل instructions هو مصفوفة مشفرة بصيغة مضغوطة من هياكل CompiledInstruction. كل CompiledInstruction يُشير إلى الحسابات عبر الفهرس في مصفوفة account_keys بدلاً من استخدام المفتاح العام الكامل. ويحتوي على:

  1. program_id_index: فهرس في account_keys يُحدد البرنامج المراد استدعاؤه.
  2. accounts: مصفوفة من المؤشرات في account_keys تُحدد الحسابات التي سيتم تمريرها إلى البرنامج.
  3. data: مصفوفة بايت تحتوي على instruction data ومحدد التعليمة والوسيطات المُسلسَلة.
CompiledInstruction
pub struct CompiledInstruction {
/// Index into the transaction keys array indicating the program account that executes this instruction.
pub program_id_index: u8,
/// Ordered indices into the transaction keys array indicating which accounts to pass to the program.
#[serde(with = "short_vec")]
pub accounts: Vec<u8>,
/// The program input data.
#[serde(with = "short_vec")]
pub data: Vec<u8>,
}

مصفوفة مضغوطة من التعليماتمصفوفة مضغوطة من التعليمات

تنسيق المعاملة الثنائي

تُسلسَل المعاملات باستخدام مخطط ترميز مضغوط. جميع المصفوفات ذات الطول المتغير (التواقيع، ومفاتيح الحسابات، والتعليمات) مسبوقة بترميز طول compact-u16. يستخدم هذا التنسيق بايتًا واحدًا للقيم من 0 إلى 127، وبايتين أو ثلاثة بايتات للقيم الأكبر.

تخطيط المعاملة القديمة (على الشبكة):

الحقلالحجمالوصف
num_signatures1-3 بايت (compact-u16)عدد التواقيع
signaturesnum_signatures × 64 بايتتواقيع Ed25519
num_required_signatures1 بايتالحقل 1 في MessageHeader
num_readonly_signed1 بايتالحقل 2 في MessageHeader
num_readonly_unsigned1 بايتالحقل 3 في MessageHeader
num_account_keys1-3 بايت (compact-u16)عدد مفاتيح الحسابات الثابتة
account_keysnum_account_keys × 32 بايتpubkey
recent_blockhash32 بايتتجزئة الكتلة
num_instructions1-3 بايت (compact-u16)عدد التعليمات
instructionsمتغيرمصفوفة من التعليمات المُجمَّعة

تُسلسَل كل تعليمة مُجمَّعة على النحو التالي:

الحقلالحجمالوصف
program_id_index1 بايتفهرس في مفاتيح الحسابات
num_accounts1-3 بايت (compact-u16)عدد مؤشرات الحسابات
account_indicesnum_accounts × 1 بايتمؤشرات مفاتيح الحسابات
data_len1-3 بايت (compact-u16)طول instruction data
datadata_len بايتinstruction data غير مُفسَّر

حساب الحجم

بافتراض أن PACKET_DATA_SIZE = 1,232 بايت، يمكن حساب المساحة المتاحة على النحو التالي:

Total = 1232 bytes
- compact-u16(num_sigs) # 1 byte
- num_sigs * 64 # signature bytes
- 3 # message header
- compact-u16(num_keys) # 1 byte
- num_keys * 32 # account key bytes
- 32 # recent blockhash
- compact-u16(num_ixs) # 1 byte
- sum(instruction_sizes) # per-instruction overhead + data

مثال: معاملة تحويل SOL

يوضح الرسم البياني أدناه كيف تعمل المعاملات والتعليمات معاً للسماح للمستخدمين بالتفاعل مع الشبكة. في هذا المثال، يتم تحويل SOL من حساب إلى آخر.

تشير البيانات الوصفية لحساب المُرسِل إلى أنه يجب أن يوقّع على المعاملة. يتيح ذلك لـ System Program خصم lamport. يجب أن يكون كلٌّ من حساب المُرسِل والمستلم قابلَين للكتابة، حتى يتسنى تغيير رصيد lamport الخاص بهما. لتنفيذ هذه التعليمة، يُرسل محفظة المُرسِل المعاملةَ التي تحتوي على توقيعه والرسالةَ التي تتضمن تعليمة تحويل SOL.

مخطط تحويل SOLمخطط تحويل SOL

بعد إرسال المعاملة، تعالج System Program تعليمة التحويل وتُحدّث رصيد lamport لكلا الحسابين.

مخطط عملية تحويل SOLمخطط عملية تحويل SOL

تحقق من المستلم قبل إرسال SOL

يُضيف تحويل System Program lamport إلى أي حساب. لا يوجد فحص على مستوى البروتوكول للتحقق من أن المستلم يستطيع إعادة إخراج SOL. لا يمكن إخراج lamport إلا بواسطة البرنامج المالك للحساب، لذا فإن إرسال SOL إلى token mint أو برنامج أو PDA لا تتحكم فيه يُعرّضك لخطر الفقدان الدائم للأموال — لا يمكن إعادتها إلا من قِبَل جهة مخوّلة تحددها البرنامج المالك. لا يمكن استرداد SOL المُرسَل إلى token account إلا من قِبَل مالك ذلك الحساب، وليس المُرسِل أبداً.

تحويلات token عبر SPL محمية جزئياً بذاتها: يرفض Token Program أي تحويل لا تتطابق حساباته مع الـ mint المتوقعة. أما تحويلات SOL الأصلية فلا تمتلك هذه الحماية، لذا يجب على المُرسِل التحقق من المستلم قبل التوقيع. راجع التحقق من العنوان للاطلاع على منطق التصنيف الكامل.

يوضح المثال أدناه الكود المتعلق بالمخططات أعلاه. راجع دالة transfer الخاصة بـ System Program.

import { createClient, generateKeyPairSigner, lamports } from "@solana/kit";
import { solanaRpc, rpcAirdrop } from "@solana/kit-plugin-rpc";
import { generatedPayer, airdropPayer } from "@solana/kit-plugin-signer";
import { systemProgram } from "@solana-program/system";
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)))
.use(systemProgram());
const sender = client.payer;
const recipient = await generateKeyPairSigner();
const LAMPORTS_PER_SOL = 1_000_000_000n;
const transferAmount = lamports(LAMPORTS_PER_SOL / 100n); // 0.01 SOL
// Check balance before transfer
const { value: preBalance1 } = await client.rpc
.getBalance(sender.address)
.send();
const { value: preBalance2 } = await client.rpc
.getBalance(recipient.address)
.send();
// Create a transfer instruction for transferring SOL from sender to recipient
const transferInstruction = client.system.instructions.transferSol({
source: sender,
destination: recipient.address,
amount: transferAmount // 0.01 SOL in lamports
});
const transactionSignature = await client.sendTransaction([
transferInstruction
]);
// Check balance after transfer
const { value: postBalance1 } = await client.rpc
.getBalance(sender.address)
.send();
const { value: postBalance2 } = await client.rpc
.getBalance(recipient.address)
.send();
console.log(
"Sender prebalance:",
Number(preBalance1) / Number(LAMPORTS_PER_SOL)
);
console.log(
"Recipient prebalance:",
Number(preBalance2) / Number(LAMPORTS_PER_SOL)
);
console.log(
"Sender postbalance:",
Number(postBalance1) / Number(LAMPORTS_PER_SOL)
);
console.log(
"Recipient postbalance:",
Number(postBalance2) / Number(LAMPORTS_PER_SOL)
);
console.log("Transaction Signature:", transactionSignature.context.signature);
Console
Click to execute the code.

يوضح المثال التالي بنية معاملة تحتوي على تعليمات تحويل SOL واحدة.

import {
createClient,
generateKeyPairSigner,
lamports,
createTransactionMessage,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstructions,
pipe,
signTransactionMessageWithSigners,
getCompiledTransactionMessageDecoder
} from "@solana/kit";
import { solanaRpc, rpcAirdrop } from "@solana/kit-plugin-rpc";
import { generatedPayer, airdropPayer } from "@solana/kit-plugin-signer";
import { systemProgram } from "@solana-program/system";
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)))
.use(systemProgram());
const { value: latestBlockhash } = await client.rpc.getLatestBlockhash().send();
const sender = client.payer;
const recipient = await generateKeyPairSigner();
// Define the amount to transfer
const LAMPORTS_PER_SOL = 1_000_000_000n;
const transferAmount = lamports(LAMPORTS_PER_SOL / 100n); // 0.01 SOL
// Create a transfer instruction for transferring SOL from sender to recipient
const transferInstruction = client.system.instructions.transferSol({
source: sender,
destination: recipient.address,
amount: transferAmount
});
// Create transaction message
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(sender, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstructions([transferInstruction], tx)
);
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
// Decode the messageBytes
const compiledTransactionMessage =
getCompiledTransactionMessageDecoder().decode(signedTransaction.messageBytes);
console.log(JSON.stringify(compiledTransactionMessage, null, 2));
Console
Click to execute the code.

يوضح الكود أدناه المخرجات من مقتطفات الكود السابقة. يختلف التنسيق بين حزم SDK، لكن لاحظ أن كل تعليمة تحتوي على نفس المعلومات المطلوبة.

{
"version": 0,
"header": {
"numSignerAccounts": 1,
"numReadonlySignerAccounts": 0,
"numReadonlyNonSignerAccounts": 1
},
"staticAccounts": [
"HoCy8p5xxDDYTYWEbQZasEjVNM5rxvidx8AfyqA4ywBa",
"5T388jBjovy7d8mQ3emHxMDTbUF8b7nWvAnSiP3EAdFL",
"11111111111111111111111111111111"
],
"lifetimeToken": "EGCWPUEXhqHJWYBfDirq3mHZb4qDpATmYqBZMBy9TBC1",
"instructions": [
{
"programAddressIndex": 2,
"accountIndices": [0, 1],
"data": {
"0": 2,
"1": 0,
"2": 0,
"3": 0,
"4": 128,
"5": 150,
"6": 152,
"7": 0,
"8": 0,
"9": 0,
"10": 0,
"11": 0
}
}
]
}

التحقق من المستلم قبل التحويل

نظرًا لأن تحويل SOL ينجح إلى أي حساب، تحقق من المستلم قبل التوقيع. اجلب بيانات الحساب وأرسل فقط إلى محفظة System Program (أو عنوان على المنحنى غير ممول)؛ ارفض عمليات الإصدار، و token account، والبرامج، وعناوين PDA التي لا تتحكم فيها.

Kit
import {
type Address,
createSolanaRpc,
fetchJsonParsedAccount,
isOffCurveAddress
} from "@solana/kit";
const rpc = createSolanaRpc("https://api.mainnet-beta.solana.com");
const SYSTEM_PROGRAM = "11111111111111111111111111111111" as Address;
/**
* Throws if `recipient` cannot safely receive native SOL.
*
* Only System Program wallets (or unfunded on-curve addresses) are safe. Any
* other account locks the lamports because no authority can debit them.
*/
async function assertSafeSolRecipient(recipient: Address): Promise<void> {
const account = await fetchJsonParsedAccount(rpc, recipient);
if (!account.exists) {
// Off-curve = a PDA with no account; reject conservatively.
if (isOffCurveAddress(recipient)) {
throw new Error(
"Recipient is a PDA with no account; SOL would be locked"
);
}
// On-curve = an unfunded wallet, safe to fund.
return;
}
if (account.programAddress !== SYSTEM_PROGRAM) {
throw new Error(
`Recipient is owned by ${account.programAddress}, not a wallet; SOL would be locked`
);
}
}
// A wallet: safe.
await assertSafeSolRecipient(
"H8sMJSCQxfKiFTCfDR3DUMLPwcRbM61LGFJ8N4dK3WjS" as Address
);
// The USDC mint: rejected before any SOL leaves the sender.
await assertSafeSolRecipient(
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" as Address
);
Console
Click to execute the code.

يتحقق هذا المقتطف من مستلمي SOL الأصليين. للاطلاع على التصنيف الكامل الذي يتعامل أيضًا مع إرسال رموز SPL (token account، وATAs، وToken-2022)، راجع التحقق من العنوان.

جلب تفاصيل المعاملة

بعد الإرسال، استرجع تفاصيل المعاملة باستخدام توقيع المعاملة وطريقة getTransaction RPC.

يمكنك أيضًا العثور على المعاملة باستخدام سولانا Explorer.

Transaction Data
{
"blockTime": 1745196488,
"meta": {
"computeUnitsConsumed": 150,
"err": null,
"fee": 5000,
"innerInstructions": [],
"loadedAddresses": {
"readonly": [],
"writable": []
},
"logMessages": [
"Program 11111111111111111111111111111111 invoke [1]",
"Program 11111111111111111111111111111111 success"
],
"postBalances": [989995000, 10000000, 1],
"postTokenBalances": [],
"preBalances": [1000000000, 0, 1],
"preTokenBalances": [],
"rewards": [],
"status": {
"Ok": null
}
},
"slot": 13049,
"transaction": {
"message": {
"header": {
"numReadonlySignedAccounts": 0,
"numReadonlyUnsignedAccounts": 1,
"numRequiredSignatures": 1
},
"accountKeys": [
"8PLdpLxkuv9Nt8w3XcGXvNa663LXDjSrSNon4EK7QSjQ",
"7GLg7bqgLBv1HVWXKgWAm6YoPf1LoWnyWGABbgk487Ma",
"11111111111111111111111111111111"
],
"recentBlockhash": "7ZCxc2SDhzV2bYgEQqdxTpweYJkpwshVSDtXuY7uPtjf",
"instructions": [
{
"accounts": [0, 1],
"data": "3Bxs4NN8M2Yn4TLb",
"programIdIndex": 2,
"stackHeight": null
}
],
"indexToProgramIds": {}
},
"signatures": [
"3jUKrQp1UGq5ih6FTDUUt2kkqUfoG2o4kY5T1DoVHK2tXXDLdxJSXzuJGY4JPoRivgbi45U2bc7LZfMa6C4R3szX"
]
},
"version": "legacy"
}

تُعرِّف الاستجابة الخام الحسابات بالفهرس وتخزّن التعليمات الداخلية (CPI) كمقاطع مشفّرة. لتحليل هذه التعليمات إلى عناوين والتنقل عبر شجرة التعليمات الكاملة، راجع استبطان المعاملات.

Is this page helpful?