Ringkasan
Solana memiliki tiga format transaksi: legacy, v0, dan v1. v0 menambahkan Address Lookup Tables (ALT) untuk mereferensikan akun melalui indeks 1-byte. v1 meningkatkan batas ukuran menjadi 4.096 byte, memindahkan batas sumber daya ke dalam pesan itu sendiri, dan menghapus ALT.
Solana mendukung tiga format transaksi: legacy, v0, dan v1. Masing-masing dijelaskan di bawah dengan tiga bagian yang sama: cara byte ditata dalam transmisi, cara akun direferensikan, dan dari mana batas sumber dayanya berasal.
Status aktivasi v1
Format v1 sudah aktif di mainnet, devnet, dan testnet. Membaca transaksi merupakan perubahan yang bersifat breaking: klien yang tidak ikut serta (opt in) akan gagal pada transaksi v1. Aplikasi yang sudah ada sebaiknya meninjau persiapan untuk v1.
Perbandingan format
| Batas | legacy | v0 | v1 |
|---|---|---|---|
| Ukuran transaksi maksimal | 1.232 byte | 1.232 byte | 4.096 byte |
| Alamat akun | ~32, dibatasi ukuran | 64, melalui lookup table | 64, inline (96 dengan SIMD-0596) |
| Address lookup table | tidak didukung | didukung | tidak didukung |
| Batas sumber daya | Instruksi ComputeBudget | Instruksi ComputeBudget | konfigurasi pesan |
Format legacy
Format asli, dan masih menjadi default di sebagian besar tooling. Format ini sama sekali tidak memiliki
prefiks versi: byte pertama dari transaksi adalah hitungan compact-u16 dari
array tanda tangan, dan byte pertama dari pesan adalah num_required_signatures,
yang bit tertingginya selalu tidak disetel.
Tata letak wire legacy
| Field | Size | Description |
|---|---|---|
num_signatures | compact-u16 | Jumlah tanda tangan |
signatures | num_signatures x 64 byte | Tanda tangan Ed25519 |
header | 3 byte | MessageHeader — byte pertama memiliki bit versi yang tidak disetel |
num_account_keys | compact-u16 | Jumlah kunci akun |
account_keys | num_account_keys x 32 byte | Kunci publik, semua inline |
recent_blockhash | 32 byte | Penentu masa berlaku |
num_instructions | compact-u16 | Jumlah instruksi |
instructions | variabel | Setiap instruksi diserialisasi secara berurutan |
Setiap array dengan panjang variabel diawali dengan panjang compact-u16: 1 byte untuk nilai 0–127, 2–3 byte untuk nilai yang lebih besar. Untuk tata letak per instruksi dan perhitungan ukuran secara rinci, lihat format biner transaksi.
Akun dalam legacy
Setiap akun ditulis sebagai kunci publik 32-byte penuh dalam account_keys, dan
instruksi mereferensikannya dengan indeks 1-byte ke dalam array tersebut. Tidak ada cara untuk
mereferensikan akun yang tidak tercantum dalam transaksi, itulah yang
membatasi transaksi legacy pada sekitar 32 akun sebelum kapasitas
1.232 byte habis.
Batas sumber daya dalam legacy
Batas unit komputasi, batas ukuran data akun yang dimuat, ukuran heap, dan biaya prioritas semuanya diminta dengan menyertakan instruksi program ComputeBudget dalam transaksi. Masing-masing memerlukan satu slot instruksi dan 150 unit komputasi. Menghilangkan nya aman: runtime akan menggunakan default 200.000 CU per instruksi (dibatasi hingga 1,4M), batas ukuran data 64 MiB, heap 32 KiB, dan biaya prioritas nol.
Format v0
v0 adalah pesan legacy ditambah dua hal: byte prefiks versi 0x80 dan
array address_table_lookups yang ditambahkan setelah instruksi. Semua yang ada sebelumnya
byte-identik dengan legacy.
Tata letak wire v0
| Field | Ukuran | Deskripsi |
|---|---|---|
num_signatures | compact-u16 | Jumlah tanda tangan |
signatures | num_signatures x 64 byte | Tanda tangan Ed25519 |
0x80 | 1 byte | Byte prefiks versi — byte pertama dari pesan |
header | 3 byte | MessageHeader (sama dengan legacy) |
num_account_keys | compact-u16 | Jumlah kunci akun statis |
static_account_keys | num_account_keys x 32 byte | Kunci yang muncul secara literal dalam transaksi |
recent_blockhash | 32 bytes | Penentu masa berlaku |
num_instructions | compact-u16 | Jumlah instruksi |
instructions | variabel | Format sama dengan legacy |
address_table_lookups | compact-u16 + variabel | Referensi ALT (lihat di bawah) |
Setiap entri pencarian tabel alamat berisi:
| Field | Size | Description |
|---|---|---|
account_key | 32 byte | Kunci publik akun ALT |
writable_indexes | compact-u16 + N x 1 byte | Indeks ke dalam ALT untuk akun yang dapat ditulis |
readonly_indexes | compact-u16 + N x 1 byte | Indeks ke dalam ALT untuk akun read-only |
Address lookup table
ALT adalah akun onchain yang menyimpan hingga 256 kunci publik. Dengan mereferensikan ALT, sebuah transaksi dapat menyertakan akun tambahan menggunakan indeks 1-byte alih-alih kunci publik 32-byte, sehingga secara signifikan mengurangi overhead per akun.
Saat runtime, sebelum eksekusi dimulai, validator menyelesaikan semua referensi ALT menjadi kunci publik lengkap. Alamat yang telah diselesaikan ditambahkan ke kunci akun statis untuk membentuk daftar kunci akun yang lengkap. Akun yang diselesaikan melalui ALT mengikuti urutan yang sama dengan akun statis: pencarian yang dapat ditulis (writable lookups) muncul sebelum pencarian read-only.
Tabel pencarian alamat hanya memengaruhi cara akun direferensikan dalam transaksi on-wire. Pada waktu eksekusi, runtime menyelesaikan semua indeks menjadi alamat akun lengkap. Akun yang diselesaikan melalui ALT hanya dapat bersifat writable atau read-only (non-signer); mereka tidak dapat menjadi signer.
Membuat dan menggunakan address lookup table
Contoh ini membuat lookup table, menambahkan sebuah alamat, lalu menggunakan tabel tersebut untuk mengompilasi dan mengirim transaksi v0.
Hanya akun non-penanda tangan yang dapat dimuat dari lookup table. Pembayar biaya dan setiap penanda tangan yang diperlukan tetap berada di daftar akun statis pesan. Sebuah alamat juga tidak dapat dicari di slot yang sama dengan saat alamat tersebut ditambahkan, sehingga contoh ini menunggu slot terkonfirmasi berikutnya sebelum mengompilasi pesan v0.
Batas sumber daya dalam v0
Tidak berubah dari legacy: instruksi ComputeBudget, dengan default yang sama ketika mereka dihilangkan.
Format v1
v1 meningkatkan batas ukuran menjadi 4.096 byte dan merestrukturisasi pesan di sekitar konfigurasi transaksi: batas sumber daya dipindahkan dari instruksi ComputeBudget ke field berposisi tetap dalam pesan itu sendiri. Hal ini memungkinkan jaringan mengurutkan transaksi berdasarkan biaya prioritas dengan satu pembacaan offset tetap alih-alih memindai dan mendeserialisasi daftar instruksinya.
Tata letak wire v1
| Field | Size | Description |
|---|---|---|
0x81 | 1 byte | Byte prefiks versi — byte pertama dari transaksi |
header | 3 byte | MessageHeader (sama dengan legacy) |
config_mask | 4 byte | Bitmask u32 LE yang menandai nilai konfigurasi mana yang ada |
recent_blockhash | 32 byte | Penentu masa berlaku |
num_instructions | 1 byte | Hitungan lebar tetap, maksimal 64 |
num_addresses | 1 byte | Hitungan lebar tetap, maksimal 64 (96 dengan SIMD-0596) |
addresses | N x 32 byte | Alamat akun, semua inline — tanpa referensi lookup table |
config_values | 0–20 byte | Satu nilai per bit mask yang disetel, dalam urutan bit (lihat di bawah) |
instruction_headers | N x 4 byte | Per instruksi: program_id_index (u8), num_accounts (u8), data_len (u16 LE) |
instruction_payloads | variabel | Per instruksi: indeks akun, lalu instruction data |
signatures | N x 64 byte | Di bagian akhir, tanpa prefiks panjang — hitungannya berasal dari header |
Dua perbedaan struktural dari legacy dan v0 perlu diperhatikan saat menulis
dekoder. Hitungannya adalah field u8 lebar tetap, bukan compact-u16, dan
instruksi dibagi menjadi dua bagian: semua header berukuran tetap terlebih dahulu, kemudian semua
payload dengan panjang variabel, alih-alih setiap instruksi bersifat berurutan.
Akun dalam v1: tanpa address lookup table
v1 sengaja menghapus dukungan ALT. 64 alamat mentah berukuran 2.048 byte, dan 96 alamat yang diizinkan setelah SIMD-0596 berukuran 3.072 byte, keduanya jauh di dalam batas 4.096 byte, sehingga setiap alamat berada inline seperti pada legacy. Jika aplikasi Anda bergantung pada lookup table, beralih ke v1 berarti menyertakan alamat-alamat tersebut secara inline.
Batas sumber daya dalam v1: konfigurasi transaksi
Konfigurasi adalah bitmask u32 diikuti oleh nilai lebar tetap untuk setiap field
yang bitnya disetel:
| Bit | Field | Lebar | Catatan |
|---|---|---|---|
| 0–1 | Biaya prioritas | u64 | Total lamport — kedua bit disetel bersamaan |
| 2 | Batas unit komputasi | u32 | |
| 3 | Batas ukuran data akun yang dimuat | u32 | |
| 4 | Ukuran heap yang diminta | u32 |
Bit yang tidak dikenal akan ditolak. Karena pesan ditandatangani, field konfigurasi yang tidak dikenal tidak dapat diabaikan secara diam-diam.
Biaya prioritas adalah total lamport, bukan harga
Dalam legacy dan v0, biaya prioritas ditetapkan melalui SetComputeUnitPrice sebagai
micro-lamport per unit komputasi, dikalikan dengan batas unit komputasi. Dalam
v1, nilainya adalah total absolut dalam lamport — tanpa perkalian, tanpa pembulatan.
Jangan membawa aritmetika per-CU tersebut. Formula total biaya tidak berubah:
(signatures × lamports_per_signature) + priority_fee.
Instruksi ComputeBudget adalah no-op dalam v1
Transaksi v1 tidak menolak instruksi ComputeBudget — instruksi tersebut diabaikan untuk konfigurasi. Instruksi tersebut tetap dieksekusi sebagai no-op yang berhasil, mengonsumsi 150 unit komputasi dan satu dari 64 slot instruksi tanpa memberikan efek apa pun pada anggaran. Hilangkan instruksi tersebut saat membangun transaksi v1, dan hentikan pemindaian terhadapnya saat membaca transaksi v1: nilainya ada dalam konfigurasi pesan.
Field konfigurasi harus disetel secara eksplisit
Perubahan perilaku terpenting bagi pengirim: tidak seperti legacy dan v0, transaksi v1 harus menetapkan batas unit komputasi dan batas ukuran data akun yang dimuat secara eksplisit, atau transaksi Anda akan gagal.
| Field yang tidak disetel | legacy / v0 | v1 | Gejala jika dihilangkan |
|---|---|---|---|
| Batas unit komputasi | 200k per instruksi, maks 1,4M | 0 CU | Langsung gagal, anggaran habis |
| Ukuran data akun yang dimuat | 64 MiB | 0 byte | MaxLoadedAccountsDataSizeExceeded pada akun pertama yang dimuat |
| Biaya prioritas | 0 | 0 | — |
| Ukuran heap | 32 KiB | 32 KiB | — |
Pendekatan yang direkomendasikan adalah melakukan simulasi sekali dengan kedua batas dimaksimalkan, lalu menulis
nilai unitsConsumed dan loadedAccountsDataSize yang dikembalikan ke dalam konfigurasi,
membulatkan ukuran data ke atas ke halaman 32 KiB berikutnya untuk cadangan (model biaya blok
menghitung dalam halaman 32 KiB, sehingga cadangan di bawah batas halaman berikutnya
gratis).
Membangun transaksi v1
Pesan v1 dirakit melalui pipeline yang sama seperti pesan legacy atau v0. Perbedaannya
adalah setTransactionMessageConfig: batas sumber daya merupakan properti
dari pesan, sehingga daftar instruksi hanya berisi instruksi program.
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);
Kit juga menyediakan setter untuk setiap field konfigurasi —
setTransactionMessagePriorityFeeLamports,
setTransactionMessageComputeUnitLimit,
setTransactionMessageLoadedAccountsDataSizeLimit, dan
setTransactionMessageHeapSize — untuk mengaturnya satu per satu.
Menentukan ukuran batas melalui simulasi
Daripada menebak dua batas wajib tersebut, ukurlah. Simulasi berjalan dengan kedua batas dinaikkan ke maksimum runtime, sehingga tidak mungkin gagal karena kekurangan sumber daya yang sedang diukur. Angka yang dikembalikan adalah biaya persis dari satu kali simulasi, jadi tambahkan margin sebelum mengandalkannya.
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),);
Persiapan untuk v1
v1 mengubah cara membaca transaksi, bukan hanya mengirimkannya. Klien mana pun yang
memanggil getTransaction atau getBlock tanpa ikut serta (opt in) akan gagal pada transaksi v1:
- Berikan
maxSupportedTransactionVersion: 1— integer JSON1, bukan string"1"— kegetTransactiondangetBlock. Memberikan0akan gagal pada transaksi v1 sama seperti menghilangkan parameter tersebut, sehingga kode yang diperbarui saat peluncuran v0 masih perlu mengubah nilainya. getTransactiongagal pada transaksi v1 dengan error-32015, dan satu transaksi v1 membuat seluruh responsgetBlockgagal dengan error yang sama — tidak ada hasil parsial.blockSubscribemengembalikanblock: nulldan berhenti maju, sehingga konsumen yang membaca itu sebagai blok kosong secara diam-diam akan tertinggal sejak slot v1 pertama dan seterusnya.getSignaturesForAddresstidak pernah memeriksa isi transaksi, sehingga tanda tangan v1 terdaftar seperti biasa.- Respons yang ikut serta (opt-in) menyertakan objek
transactionConfigdalam pesan untuk transaksi v1 (sama sekali tidak ada untuk legacy dan v0). Pipeline yang menurunkan biaya prioritas atau batas komputasi dengan memindai instruksi ComputeBudget akan secara diam-diam melaporkan nol untuk setiap transaksi v1. - Gunakan
encoding: 'base64'saat mendekode transaksi di sisi klien, dan untuksendTransaction/simulateTransactiondengan transaksi di atas 1.232 byte — encoding base58 tetap dibatasi pada ukuran lama. - Dukungan library klien memerlukan versi terbaru:
@solana/kit8.0+, crate Rust generasi Agave 4.2.x, atau web3.js v3. web3.js v1 membaca v1 mulai dari 1.99.0 ke atas, tetapi tidak dapat membangun atau mengirimnya.
Membaca transaksi v1
Untuk ikut serta (opt in), gunakan bilangan bulat JSON 1. Mendekode byte yang dikembalikan akan menghasilkan
versi dan konfigurasi, yang menjadi tempat batas sumber daya berada — memindai
daftar instruksi untuk mencari instruksi ComputeBudget tidak akan menemukan apa pun.
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);}
Untuk panduan migrasi lengkap — termasuk dukungan pustaka klien, deteksi versi streaming (Geyser/gRPC), dan perilaku simulasi — lihat halaman upgrade Transaction Format v1. Contoh yang dapat dijalankan dalam TypeScript, Rust, Python, dan Go tersedia di solana-foundation/transaction-v1-examples.
Is this page helpful?