المعاملات ذات الإصدارات

ملخص

تمتلك سولانا ثلاث صيغ للمعاملات: legacy وv0 وv1. تضيف v0 جداول بحث العناوين (ALTs) للإشارة إلى الحسابات عبر مؤشرات أحادية البايت. أما v1 فترفع حد الحجم إلى 4,096 بايت، وتنقل حدود الموارد إلى داخل الرسالة نفسها، وتزيل دعم ALTs.

تدعم سولانا ثلاث صيغ للمعاملات: legacy وv0 وv1. يُوصف كلٌّ منها أدناه بالأجزاء الثلاثة ذاتها: كيفية ترتيب بياناته على السلك، وكيفية الإشارة إلى الحسابات، ومصدر حدود موارده.

حالة تفعيل v1

صيغة v1 مفعّلة على mainnet وdevnet وtestnet. قراءة المعاملات تغيير جذري: العملاء الذين لا يشتركون فيه تفشل لديهم معاملات v1. ينبغي للتطبيقات الحالية مراجعة الاستعداد لـ v1.

مقارنة الصيغ

الحدlegacyv0v1
الحد الأقصى لحجم المعاملة1,232 بايت1,232 بايت4,096 بايت
عناوين الحسابات~32، محدودة بالحجم64، عبر جداول البحث64، مضمّنة (96 مع SIMD-0596)
جداول بحث العناوينغير مدعومةمدعومةغير مدعومة
حدود المواردتعليمات ComputeBudgetتعليمات ComputeBudgetإعداد الرسالة

انتقل إلى: Legacy · v0 · v1

صيغة Legacy

الصيغة الأصلية، ولا تزال الافتراضية في معظم الأدوات. لا تحتوي على بادئة إصدار على الإطلاق: البايت الأول من المعاملة هو عدد compact-u16 لمصفوفة التوقيعات، والبايت الأول من الرسالة هو num_required_signatures الذي تكون بتته العليا دائماً غير مضبوطة.

تخطيط سلك Legacy

الحقلالحجمالوصف
num_signaturescompact-u16عدد التوقيعات
signaturesnum_signatures × 64 بايتتوقيعات Ed25519
header3 بايتاتMessageHeader — البايت الأول لا تكون فيه بتة الإصدار مضبوطة
num_account_keyscompact-u16عدد مفاتيح الحسابات
account_keysnum_account_keys × 32 بايتالمفاتيح العامة، جميعها مضمّنة
recent_blockhash32 بايتمحدد العمر الافتراضي
num_instructionscompact-u16عدد التعليمات
instructionsمتغيركل تعليمة مُسلسَلة بشكل متتالٍ

كل مصفوفة متغيرة الطول تُسبق بطول compact-u16: بايت واحد للقيم من 0 إلى 127، و2–3 بايتات للقيم الأكبر. للاطلاع على تخطيط التعليمة الفردية وحساب حجم عملي، انظر الصيغة الثنائية للمعاملة.

الحسابات في Legacy

يُكتب كل حساب كمفتاح عام كامل بطول 32 بايت في account_keys، وتُشير إليه التعليمات بمؤشر أحادي البايت في تلك المصفوفة. لا توجد طريقة للإشارة إلى حساب غير مُدرج صراحةً في المعاملة، وهذا ما يحدّ معاملة legacy بحوالي 32 حساباً قبل أن تنفد الـ 1,232 بايت.

حدود الموارد في Legacy

يُطلب تحديد حد وحدات الحوسبة، وحد حجم بيانات الحسابات المُحمَّلة، وحجم الكومة، ورسوم الأولوية، بإدراج تعليمات برنامج ComputeBudget في المعاملة. يكلّف كل منها فتحة تعليمة slot و150 وحدة حوسبة. الحذف آمن: يعود وقت التشغيل إلى القيم الافتراضية وهي 200,000 وحدة حوسبة لكل تعليمة (بحد أقصى 1.4M)، وحد حجم بيانات 64 ميبيبايت، وكومة 32 كيبيبايت، ورسوم أولوية بقيمة صفر.

صيغة V0

v0 هي رسالة legacy مضافاً إليها أمران: بايت بادئة إصدار 0x80 ومصفوفة address_table_lookups تُلحق بعد التعليمات. كل ما قبل ذلك مطابق بالبايت لصيغة legacy.

تخطيط سلك V0

الحقلالحجمالوصف
num_signaturescompact-u16عدد التوقيعات
signaturesnum_signatures × 64 بايتتوقيعات Ed25519
0x801 بايتبايت بادئة الإصدار — البايت الأول من الرسالة
header3 بايتاتMessageHeader (نفس القديم)
num_account_keyscompact-u16عدد مفاتيح الحسابات الثابتة
static_account_keysnum_account_keys × 32 بايتالمفاتيح التي تظهر حرفياً في المعاملة
recent_blockhash32 بايتمحدد العمر الافتراضي
num_instructionscompact-u16عدد التعليمات
instructionsمتغيرنفس التنسيق القديم
address_table_lookupscompact-u16 + متغيرمراجع ALT (انظر أدناه)

يحتوي كل إدخال في جدول البحث عن العناوين على:

الحقلالحجمالوصف
account_key32 بايتالمفتاح العام لحساب ALT
writable_indexescompact-u16 + N x 1 بايتالفهارس في ALT للحسابات القابلة للكتابة
readonly_indexescompact-u16 + N x 1 بايتالفهارس في ALT للحسابات للقراءة فقط

جداول بحث العناوين

ALT هو حساب على السلسلة يخزن ما يصل إلى 256 مفتاحاً عاماً. من خلال الإشارة إلى ALT، يمكن للمعاملة تضمين حسابات إضافية باستخدام فهارس بحجم 1 بايت بدلاً من المفاتيح العامة بحجم 32 بايت، مما يقلل بشكل كبير من العبء لكل حساب.

في وقت التشغيل، قبل بدء التنفيذ، يقوم المدقق بتحليل جميع مراجع ALT إلى مفاتيح عامة كاملة. يتم إلحاق العناوين المحللة بمفاتيح الحسابات الثابتة لتشكيل قائمة مفاتيح الحسابات الكاملة. تتبع الحسابات المحللة من ALT نفس الترتيب الخاص بالحسابات الثابتة: تأتي عمليات البحث القابلة للكتابة قبل عمليات البحث للقراءة فقط.

تؤثر جداول البحث عن العناوين فقط على كيفية الإشارة إلى الحسابات في المعاملة على الشبكة. في وقت التنفيذ، يقوم وقت التشغيل بتحليل جميع الفهارس إلى عناوين حسابات كاملة. يمكن أن تكون الحسابات المحللة من ALT قابلة للكتابة أو للقراءة فقط (غير موقّعة)؛ لا يمكن أن تكون موقّعة.

إنشاء جدول بحث العناوين واستخدامه

يُنشئ هذا المثال جدول بحث، ويضيف إليه عنوانًا، ثم يستخدم الجدول لتجميع معاملة v0 وإرسالها.

لا يمكن تحميل سوى الحسابات غير الموقِّعة من جدول البحث. يظل دافع الرسوم وكل موقِّع مطلوب في قائمة الحسابات الثابتة في الرسالة. كما لا يمكن البحث عن عنوان في نفس الـ slot الذي أُضيف فيه، لذا ينتظر المثال الـ slot المؤكَّد التالي قبل تجميع رسالة v0.

حدود الموارد في v0

لم تتغير عن legacy: تعليمات ComputeBudget، مع القيم الافتراضية ذاتها عند حذفها.

صيغة V1

ترفع v1 حد الحجم إلى 4,096 بايت وتعيد هيكلة الرسالة حول إعداد المعاملة: تنتقل حدود الموارد من تعليمات ComputeBudget إلى حقول ذات موضع ثابت في الرسالة نفسها. يتيح ذلك للشبكة ترتيب المعاملات حسب رسوم الأولوية بقراءة واحدة ذات إزاحة ثابتة بدلاً من فحص قائمة التعليمات وفك تسلسلها.

تخطيط سلك V1

الحقلالحجمالوصف
0x811 بايتبايت بادئة الإصدار — البايت الأول من المعاملة
header3 بايتاتMessageHeader (نفس القديم)
config_mask4 بايتاتقناع بتات u32 LE يحدد قيم الإعداد الموجودة
recent_blockhash32 بايتمحدد العمر الافتراضي
num_instructions1 بايتعدد ذو عرض ثابت، الحد الأقصى 64
num_addresses1 بايتعدد ذو عرض ثابت، الحد الأقصى 64 (96 مع SIMD-0596)
addressesN × 32 بايتعناوين الحسابات، جميعها مضمّنة — بدون إشارات إلى جداول البحث
config_values0–20 بايتقيمة واحدة لكل بتة مضبوطة في القناع، بترتيب البتات (انظر أدناه)
instruction_headersN × 4 بايتاتلكل تعليمة: program_id_index (u8)، num_accounts (u8)، data_len (u16 LE)
instruction_payloadsمتغيرلكل تعليمة: مؤشرات الحسابات، ثم instruction data
signaturesN × 64 بايتفي الذيل، بدون بادئة طول — يُستخرج العدد من الرأس

ثمة فارقان هيكليان عن legacy وv0 يستحقان الانتباه عند كتابة مُفككّ ترميز. الأعداد حقول u8 ذات عرض ثابت بدلاً من compact-u16، والتعليمات مقسّمة إلى جريانين: كل الرؤوس ذات الحجم الثابت أولاً، ثم كل الحمولات متغيرة الطول، بدلاً من أن تكون كل تعليمة متتالية.

الحسابات في v1: بدون جداول بحث العناوين

تزيل v1 دعم ALT عن قصد. فـ 64 عنوانًا خامًا تساوي 2,048 بايت، والعناوين الـ 96 المسموح بها بعد SIMD-0596 تساوي 3,072 بايت، وكلاهما ضمن حد 4,096 بايت بسهولة، لذا يكون كل عنوان مضمّنًا كما هو الحال في Legacy. إذا كان تطبيقك يعتمد على جداول البحث، فإن الانتقال إلى v1 يعني تضمين تلك العناوين.

حدود الموارد في v1: إعداد المعاملة

الإعداد هو قناع بتات u32 يعقبه قيم ذات عرض ثابت لكل حقل مضبوطة بتّته:

البت(ات)الحقلالعرضملاحظات
0–1رسوم الأولويةu64إجمالي lamport — يُضبط البتّان معاً
2حد وحدات الحوسبةu32
3حد حجم بيانات الحسابات المُحمَّلةu32
4حجم الكومة المطلوبu32

تُرفض البتات غير المعروفة. ونظراً لأن الرسالة موقّعة، لا يمكن حذف حقول الإعداد غير المعروفة بصمت.

رسوم الأولوية هي إجمالي lamport، وليست سعراً

في legacy وv0، تُضبط رسوم الأولوية عبر SetComputeUnitPrice بوصفها micro-lamports لكل وحدة حوسبة، مضروبةً في حد وحدات الحوسبة. أما في v1 فهي إجمالي مطلق بوحدة lamport — بدون ضرب ولا تقريب. لا تنقل حسابات الـ per-CU إلى v1. صيغة الرسوم الإجمالية لا تتغير: (signatures × lamports_per_signature) + priority_fee.

تعليمات ComputeBudget لا تؤثر في v1

لا ترفض معاملة v1 تعليمات ComputeBudget — بل تتجاهلها للإعداد. لا تزال تُنفَّذ كعمليات ناجحة لا أثر لها، تستهلك 150 وحدة حوسبة وفتحة واحدة من فتحات التعليمات الـ 64، دون أي تأثير على الميزانية. احذفها عند بناء معاملات v1، وتوقف عن البحث عنها عند قراءة معاملات v1: القيم موجودة في إعداد الرسالة.

يجب ضبط حقول الإعداد صراحةً

أهم تغيير سلوكي للمُرسِلين: خلافاً لـ legacy وv0، يجب على معاملات v1 ضبط حد وحدات الحوسبة وحد حجم بيانات الحسابات المُحمَّلة صراحةً، وإلا فستفشل معاملتك.

الحقل غير المضبوطlegacy / v0v1العرض عند الحذف
حد وحدات الحوسبة200k لكل تعليمة، الحد الأقصى 1.4M0 وحدة حوسبةيفشل فوراً، نفاد الميزانية
حجم بيانات الحسابات المُحمَّلة64 ميبيبايت0 بايتMaxLoadedAccountsDataSizeExceeded عند أول حساب مُحمَّل
رسوم الأولوية00—
حجم الكومة32 كيبيبايت32 كيبيبايت—

النهج الموصى به هو المحاكاة مرة واحدة مع رفع كلا الحدين إلى الأقصى، ثم كتابة unitsConsumed وloadedAccountsDataSize المُعادَين في الإعداد، مع تقريب حجم البيانات إلى أعلى صفحة 32 كيبيبايت لإبقاء هامش (يتقاضى نموذج تكلفة الكتلة وفق صفحات 32 كيبيبايت، لذا فأي هامش دون حدود الصفحة التالية مجاني).

بناء معاملة v1

تُجمَّع رسالة v1 عبر خط المعالجة نفسه المستخدم في رسائل Legacy أو v0. أما الفرق فهو setTransactionMessageConfig: فحدود الموارد خاصية من خصائص الرسالة، ولذلك تحتوي قائمة التعليمات على تعليمات البرنامج فقط.

Kit
import {
appendTransactionMessageInstruction,
assertIsTransactionWithBlockhashLifetime,
createClient,
createTransactionMessage,
generateKeyPairSigner,
getSignatureFromTransaction,
lamports,
pipe,
sendAndConfirmTransactionFactory,
setTransactionMessageConfig,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
signTransactionMessageWithSigners,
} from "@solana/kit";
import { rpcAirdrop, solanaRpc } from "@solana/kit-plugin-rpc";
import { airdropPayer, payer } from "@solana/kit-plugin-signer";
import { getTransferSolInstruction } from "@solana-program/system";
const sender = await generateKeyPairSigner();
const recipient = await generateKeyPairSigner();
const client = await createClient()
.use(payer(sender))
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900",
}),
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)));
const { value: latestBlockhash } = await client.rpc.getLatestBlockhash().send();
// A v1 message is assembled through the same pipeline as a legacy or v0 one.
// The difference is the config: a v1 transaction carries its resource limits in
// the message itself, so no ComputeBudget instructions are compiled in.
const transactionMessage = pipe(
createTransactionMessage({ version: 1 }),
(m) => setTransactionMessageFeePayerSigner(sender, m),
(m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),
(m) =>
appendTransactionMessageInstruction(
getTransferSolInstruction({
source: sender,
destination: recipient.address,
amount: lamports(10_000_000n),
}),
m,
),
// The compute unit limit and loaded accounts data size limit both default to
// zero on v1, so a transaction that leaves them unset always fails. The
// priority fee is an absolute total in lamports, not a price per compute
// unit.
(m) =>
setTransactionMessageConfig(
{
computeUnitLimit: 20_000,
loadedAccountsDataSizeLimit: 64 * 1024,
priorityFeeLamports: 5_000n,
},
m,
),
);
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
assertIsTransactionWithBlockhashLifetime(signedTransaction);
await sendAndConfirmTransactionFactory({
rpc: client.rpc,
rpcSubscriptions: client.rpcSubscriptions,
})(signedTransaction, { commitment: "confirmed" });
const transactionSignature = getSignatureFromTransaction(signedTransaction);
console.log("Transaction Signature:", transactionSignature);
Console
Click to execute the code.

يوفّر Kit كذلك دالة ضبط لكل حقل إعداد — setTransactionMessagePriorityFeeLamports، setTransactionMessageComputeUnitLimit، setTransactionMessageLoadedAccountsDataSizeLimit، و setTransactionMessageHeapSize — لضبطها واحدًا تلو الآخر.

تحديد حجم الحدود عبر المحاكاة

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

Kit
import {
appendTransactionMessageInstruction,
assertIsTransactionWithBlockhashLifetime,
createClient,
createTransactionMessage,
estimateAndSetResourceLimitsFactory,
estimateResourceLimitsFactory,
fillTransactionMessageProvisoryResourceLimits,
generateKeyPairSigner,
getSignatureFromTransaction,
lamports,
pipe,
sendAndConfirmTransactionFactory,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
setTransactionMessagePriorityFeeLamports,
signTransactionMessageWithSigners,
} from "@solana/kit";
import { rpcAirdrop, solanaRpc } from "@solana/kit-plugin-rpc";
import { airdropPayer, payer } from "@solana/kit-plugin-signer";
import { getTransferSolInstruction } from "@solana-program/system";
const sender = await generateKeyPairSigner();
const recipient = await generateKeyPairSigner();
const client = await createClient()
.use(payer(sender))
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900",
}),
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)));
const { value: latestBlockhash } = await client.rpc.getLatestBlockhash().send();
// The priority fee is a pricing decision that simulation cannot measure, so it
// is set directly. The two resource limits are measured below instead of
// guessed. The provisory fill writes placeholder limits so the message
// simulates at the same size it will be sent at.
const draftMessage = pipe(
createTransactionMessage({ version: 1 }),
(m) => setTransactionMessageFeePayerSigner(sender, m),
(m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),
(m) =>
appendTransactionMessageInstruction(
getTransferSolInstruction({
source: sender,
destination: recipient.address,
amount: lamports(10_000_000n),
}),
m,
),
(m) => setTransactionMessagePriorityFeeLamports(5_000n, m),
fillTransactionMessageProvisoryResourceLimits,
);
// Simulation runs with both limits raised to the runtime maximum, so it cannot
// fail for want of the resources it is measuring.
const estimateAndSetResourceLimits = estimateAndSetResourceLimitsFactory(
estimateResourceLimitsFactory({ rpc: client.rpc }),
);
const transactionMessage = await estimateAndSetResourceLimits(draftMessage, {
commitment: "confirmed",
});
console.log("Compute unit limit:", transactionMessage.config?.computeUnitLimit);
console.log(
"Loaded accounts data size limit:",
transactionMessage.config?.loadedAccountsDataSizeLimit,
);
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
assertIsTransactionWithBlockhashLifetime(signedTransaction);
await sendAndConfirmTransactionFactory({
rpc: client.rpc,
rpcSubscriptions: client.rpcSubscriptions,
})(signedTransaction, { commitment: "confirmed" });
// The estimate is the exact cost of one simulated run, with nothing to spare.
// Add a margin before relying on it in production.
console.log(
"Transaction Signature:",
getSignatureFromTransaction(signedTransaction),
);
Console
Click to execute the code.

الاستعداد لـ v1

تغيّر v1 طريقة قراءة المعاملات، لا إرسالها فقط. أي عميل يستدعي getTransaction أو getBlock دون الاشتراك في الصيغة الجديدة تفشل لديه معاملات v1:

  • مرّر maxSupportedTransactionVersion: 1 — العدد الصحيح JSON الصحيح 1، لا السلسلة النصية "1" — إلى getTransaction وgetBlock. تمرير 0 يُفشل معاملات v1 تماماً كحذف المعامل، لذا قد تحتاج قاعدة التعليمات البرمجية المحدَّثة إبان طرح v0 إلى تغيير القيمة.
  • getTransaction يفشل عند معاملة v1 بالخطأ -32015، ومعاملة v1 واحدة تُفشل استجابة getBlock بأكملها بالخطأ ذاته — لا يوجد نتيجة جزئية.
  • blockSubscribe يُصدر block: null ويتوقف عن التقدم، لذا قد يُفسّر مستهلك يعتبر ذلك كتلة فارغة أنه يتأخر بصمت اعتباراً من أول slot لـ v1.
  • getSignaturesForAddress لا يفحص أجسام المعاملات أبداً، لذا تظهر توقيعات v1 بشكل طبيعي.
  • تتضمن الاستجابات التي تم الاشتراك فيها كائن transactionConfig في الرسالة لمعاملات v1 (وهو غائب تمامًا في Legacy وv0). أما خطوط المعالجة التي تستخلص رسوم الأولوية أو حدود الحوسبة بالبحث عن تعليمات ComputeBudget فستُبلغ بصمت عن صفر لكل معاملة v1.
  • استخدم encoding: 'base64' عند فك ترميز المعاملات من جانب العميل، وكذلك مع sendTransaction/simulateTransaction للمعاملات التي تتجاوز 1,232 بايت — يظل ترميز base58 محدوداً بالحجم القديم.
  • دعم مكتبات العميل يتطلب إصدارات حديثة: @solana/kit الإصدار 8.0 فأعلى، أو حزم Rust من جيل Agave 4.2.x، أو web3.js v3. يقرأ web3.js v1 معاملات v1 اعتباراً من الإصدار 1.99.0، لكنه لا يستطيع بناءها أو إرسالها.

قراءة معاملة v1

يتم الاشتراك بتمرير العدد الصحيح 1 بصيغة JSON. وفك ترميز البايتات المُعادة يعطي الإصدار والإعداد، وهنا توجد حدود الموارد — أما البحث في قائمة التعليمات عن تعليمات ComputeBudget فلن يعثر على شيء.

Kit
import {
decompileTransactionMessage,
getBase64Encoder,
getCompiledTransactionMessageDecoder,
getTransactionDecoder,
} from "@solana/kit";
// Reading a v1 transaction requires opting in. The integer 1 is mandatory:
// omitting the parameter, or passing 0, fails on a v1 transaction rather than
// returning it in a degraded form.
const fetched = await client.rpc
.getTransaction(transactionSignature, {
commitment: "confirmed",
encoding: "base64",
maxSupportedTransactionVersion: 1,
})
.send();
if (fetched === null) {
throw new Error("the transaction just sent was not found");
}
// The compiled message stores the config as a bitmask plus a positional list,
// so it is decompiled to address the fields by name. Decompiling fetches no
// accounts, because v1 does not support address lookup tables.
const wireTransaction = getBase64Encoder().encode(fetched.transaction[0]);
const transaction = getTransactionDecoder().decode(wireTransaction);
const compiledMessage = getCompiledTransactionMessageDecoder().decode(
transaction.messageBytes,
);
const message = decompileTransactionMessage(compiledMessage);
console.log("Version:", message.version);
if ("config" in message) {
// Scanning the instruction list for ComputeBudget instructions finds nothing
// here — on v1 the limits live in the config.
console.log("Compute unit limit:", message.config?.computeUnitLimit);
console.log("Priority fee (lamports):", message.config?.priorityFeeLamports);
}
Console
Click to execute the code.

للاطلاع على دليل الترحيل الكامل — بما في ذلك دعم مكتبات العميل، واكتشاف الإصدار في البث (Geyser/gRPC)، وسلوك المحاكاة — راجع صفحة ترقية صيغة المعاملات v1. تتوفر أمثلة قابلة للتشغيل بلغات TypeScript وRust وPython وGo في solana-foundation/transaction-v1-examples.

Is this page helpful?