Transaksi berversi

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

Bataslegacyv0v1
Ukuran transaksi maksimal1.232 byte1.232 byte4.096 byte
Alamat akun~32, dibatasi ukuran64, melalui lookup table64, inline (96 dengan SIMD-0596)
Address lookup tabletidak didukungdidukungtidak didukung
Batas sumber dayaInstruksi ComputeBudgetInstruksi ComputeBudgetkonfigurasi pesan

Langsung ke: Legacy · v0 · v1

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

FieldSizeDescription
num_signaturescompact-u16Jumlah tanda tangan
signaturesnum_signatures x 64 byteTanda tangan Ed25519
header3 byteMessageHeader — byte pertama memiliki bit versi yang tidak disetel
num_account_keyscompact-u16Jumlah kunci akun
account_keysnum_account_keys x 32 byteKunci publik, semua inline
recent_blockhash32 bytePenentu masa berlaku
num_instructionscompact-u16Jumlah instruksi
instructionsvariabelSetiap 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

FieldUkuranDeskripsi
num_signaturescompact-u16Jumlah tanda tangan
signaturesnum_signatures x 64 byteTanda tangan Ed25519
0x801 byteByte prefiks versi — byte pertama dari pesan
header3 byteMessageHeader (sama dengan legacy)
num_account_keyscompact-u16Jumlah kunci akun statis
static_account_keysnum_account_keys x 32 byteKunci yang muncul secara literal dalam transaksi
recent_blockhash32 bytesPenentu masa berlaku
num_instructionscompact-u16Jumlah instruksi
instructionsvariabelFormat sama dengan legacy
address_table_lookupscompact-u16 + variabelReferensi ALT (lihat di bawah)

Setiap entri pencarian tabel alamat berisi:

FieldSizeDescription
account_key32 byteKunci publik akun ALT
writable_indexescompact-u16 + N x 1 byteIndeks ke dalam ALT untuk akun yang dapat ditulis
readonly_indexescompact-u16 + N x 1 byteIndeks 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

FieldSizeDescription
0x811 byteByte prefiks versi — byte pertama dari transaksi
header3 byteMessageHeader (sama dengan legacy)
config_mask4 byteBitmask u32 LE yang menandai nilai konfigurasi mana yang ada
recent_blockhash32 bytePenentu masa berlaku
num_instructions1 byteHitungan lebar tetap, maksimal 64
num_addresses1 byteHitungan lebar tetap, maksimal 64 (96 dengan SIMD-0596)
addressesN x 32 byteAlamat akun, semua inline — tanpa referensi lookup table
config_values0–20 byteSatu nilai per bit mask yang disetel, dalam urutan bit (lihat di bawah)
instruction_headersN x 4 bytePer instruksi: program_id_index (u8), num_accounts (u8), data_len (u16 LE)
instruction_payloadsvariabelPer instruksi: indeks akun, lalu instruction data
signaturesN x 64 byteDi 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:

BitFieldLebarCatatan
0–1Biaya prioritasu64Total lamport — kedua bit disetel bersamaan
2Batas unit komputasiu32
3Batas ukuran data akun yang dimuatu32
4Ukuran heap yang dimintau32

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 disetellegacy / v0v1Gejala jika dihilangkan
Batas unit komputasi200k per instruksi, maks 1,4M0 CULangsung gagal, anggaran habis
Ukuran data akun yang dimuat64 MiB0 byteMaxLoadedAccountsDataSizeExceeded pada akun pertama yang dimuat
Biaya prioritas00—
Ukuran heap32 KiB32 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.

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 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.

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.

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 JSON 1, bukan string "1" — ke getTransaction dan getBlock. Memberikan 0 akan gagal pada transaksi v1 sama seperti menghilangkan parameter tersebut, sehingga kode yang diperbarui saat peluncuran v0 masih perlu mengubah nilainya.
  • getTransaction gagal pada transaksi v1 dengan error -32015, dan satu transaksi v1 membuat seluruh respons getBlock gagal dengan error yang sama — tidak ada hasil parsial.
  • blockSubscribe mengembalikan block: null dan berhenti maju, sehingga konsumen yang membaca itu sebagai blok kosong secara diam-diam akan tertinggal sejak slot v1 pertama dan seterusnya.
  • getSignaturesForAddress tidak pernah memeriksa isi transaksi, sehingga tanda tangan v1 terdaftar seperti biasa.
  • Respons yang ikut serta (opt-in) menyertakan objek transactionConfig dalam 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 untuk sendTransaction/simulateTransaction dengan transaksi di atas 1.232 byte — encoding base58 tetap dibatasi pada ukuran lama.
  • Dukungan library klien memerlukan versi terbaru: @solana/kit 8.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.

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.

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?

© 2026 Yayasan Solana. Semua hak dilindungi.