Struktur transaksi

Ringkasan

Transaksi memiliki tanda tangan + pesan. Pesan berisi header, alamat akun, blockhash terbaru, dan instruksi terkompilasi. Ukuran serialisasi maksimal: 1.232 byte.

Sebuah Transaction memiliki dua field tingkat atas:

  • signatures: Array tanda tangan
  • message: Informasi transaksi, termasuk daftar instruksi yang akan diproses
Transaction
pub struct Transaction {
pub signatures: Vec<Signature>,
pub message: Message,
}

Diagram yang menunjukkan dua bagian dari transaksiDiagram yang menunjukkan dua bagian dari transaksi

Total ukuran serialisasi transaksi tidak boleh melebihi PACKET_DATA_SIZE (1.232 byte). Batas ini sama dengan 1.280 byte (MTU minimum IPv6) dikurangi 48 byte untuk header jaringan (40 byte IPv6 + 8 byte fragment header). 1.232 byte tersebut mencakup array signatures dan struct message.

Diagram yang menunjukkan format transaksi dan batas ukuranDiagram yang menunjukkan format transaksi dan batas ukuran

Tanda tangan

Field signatures adalah array compact-encoded dari nilai Signature. Setiap Signature adalah tanda tangan Ed25519 64-byte dari Message yang diserialisasi, ditandatangani dengan kunci privat akun penandatangan. Satu tanda tangan diperlukan untuk setiap akun penandatangan yang direferensikan oleh instruksi transaksi.

Setiap tanda tangan dihasilkan oleh private key. Tempat penyimpanan key tersebut — keypair lokal, cloud HSM atau KMS, atau layanan wallet terkelola — merupakan keputusan desain untuk lingkungan produksi. Lihat Penandatanganan di Produksi.

Tanda tangan pertama dalam array milik fee payer, yaitu akun yang membayar biaya dasar dan biaya prioritisasi transaksi. Tanda tangan pertama ini juga berfungsi sebagai ID transaksi, yang digunakan untuk mencari transaksi di jaringan. ID transaksi umumnya disebut sebagai tanda tangan transaksi.

Persyaratan fee payer:

  • Harus menjadi akun pertama dalam pesan (indeks 0) dan merupakan penanda tangan.
  • Harus berupa akun milik System Program atau akun nonce (divalidasi oleh validate_fee_payer).
  • Harus memiliki cukup lamport untuk menanggung rent_exempt_minimum + total_fee; jika tidak, transaksi akan gagal dengan InsufficientFundsForFee.

Pesan

Field message adalah sebuah struct Message yang berisi payload transaksi:

  • header: Header pesan
  • account_keys: Array dari alamat akun yang diperlukan oleh instruksi transaksi
  • recent_blockhash: Sebuah blockhash yang berfungsi sebagai cap waktu transaksi
  • instructions: Array dari instruksi
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>,
}

Field header adalah sebuah struct MessageHeader dengan tiga field u8 yang membagi array account_keys ke dalam kelompok izin:

  • num_required_signatures: Jumlah total tanda tangan yang diperlukan oleh transaksi.
  • num_readonly_signed_accounts: Jumlah akun bertanda tangan yang bersifat hanya-baca.
  • num_readonly_unsigned_accounts: Jumlah akun tidak bertanda tangan yang bersifat hanya-baca.
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,
}

Prefiks pesan lama dan berversi

Dalam pesan transaksi lama, byte pertama pesan adalah num_required_signatures, diikuti oleh dua byte MessageHeader lainnya. Dalam pesan transaksi berversi, byte pertama adalah prefiks versi; tiga byte MessageHeader dimulai tepat setelah prefiks tersebut. Lihat transaksi berversi untuk tata letak pesan v0 secara lengkap.

Diagram yang menampilkan tiga bagian dari header pesanDiagram yang menampilkan tiga bagian dari header pesan

Alamat akun

Field account_keys adalah array kunci publik yang dikodekan secara compact. Setiap entri mengidentifikasi akun yang digunakan oleh setidaknya satu instruksi dalam transaksi. Array tersebut harus menyertakan setiap akun dan harus mengikuti urutan ketat berikut:

  1. Penandatangan + Dapat Ditulis
  2. Penandatangan + Hanya Baca
  3. Bukan Penandatangan + Dapat Ditulis
  4. Bukan Penandatangan + Hanya Baca

Urutan ketat ini memungkinkan array account_keys digabungkan dengan tiga hitungan dalam header pesan untuk menentukan izin bagi setiap akun tanpa menyimpan flag metadata per akun. Hitungan header membagi array menjadi empat kelompok izin yang tercantum di atas.

Diagram yang menampilkan urutan array alamat akunDiagram yang menampilkan urutan array alamat akun

Blockhash terbaru

Field recent_blockhash adalah hash 32-byte yang memiliki dua tujuan:

  1. Cap waktu: membuktikan bahwa transaksi dibuat baru-baru ini.
  2. Deduplikasi: mencegah transaksi yang sama diproses dua kali.

Sebuah blockhash kedaluwarsa setelah 150 slot. Jika blockhash sudah tidak valid ketika transaksi tiba, transaksi akan ditolak dengan BlockhashNotFound, kecuali jika itu adalah transaksi nonce tahan lama yang valid.

Metode RPC getLatestBlockhash memungkinkan Anda mendapatkan blockhash saat ini dan tinggi blok terakhir di mana blockhash masih berlaku.

Instruksi

Field instructions adalah array berenkoding kompak dari struct CompiledInstruction. Setiap CompiledInstruction mereferensikan akun berdasarkan indeks ke dalam array account_keys, bukan berdasarkan public key penuh. Ini berisi:

  1. program_id_index: Indeks ke dalam account_keys yang mengidentifikasi program yang akan dipanggil.
  2. accounts: Array indeks ke dalam account_keys yang menentukan akun yang akan diteruskan ke program.
  3. data: Array byte yang berisi discriminator instruksi dan argumen yang telah diserialisasi.
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>,
}

Array kompak dari InstruksiArray kompak dari Instruksi

Format biner transaksi

Transaksi diserialisasi menggunakan skema enkoding kompak. Semua array dengan panjang variabel (tanda tangan, kunci akun, instruksi) diawali dengan enkoding panjang compact-u16. Format ini menggunakan 1 byte untuk nilai 0-127 dan 2-3 byte untuk nilai yang lebih besar.

Tata letak transaksi legacy (pada wire):

FieldUkuranDeskripsi
num_signatures1-3 byte (compact-u16)Jumlah tanda tangan
signaturesnum_signatures x 64 byteTanda tangan Ed25519
num_required_signatures1 byteField 1 MessageHeader
num_readonly_signed1 byteField 2 MessageHeader
num_readonly_unsigned1 byteField 3 MessageHeader
num_account_keys1-3 byte (compact-u16)Jumlah kunci akun statis
account_keysnum_account_keys x 32 bytePublic key
recent_blockhash32 byteBlockhash
num_instructions1-3 byte (compact-u16)Jumlah instruksi
instructionsvariabelArray dari instruksi yang dikompilasi

Setiap instruksi yang dikompilasi diserialisasi sebagai:

FieldUkuranDeskripsi
program_id_index1 byteIndeks ke dalam kunci akun
num_accounts1-3 byte (compact-u16)Jumlah indeks akun
account_indicesnum_accounts x 1 byteIndeks kunci akun
data_len1-3 byte (compact-u16)Panjang instruction data
datadata_len byteinstruction data buram

Perhitungan ukuran

Dengan PACKET_DATA_SIZE = 1.232 byte, ruang yang tersedia dapat dihitung:

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

Contoh: Transaksi transfer SOL

Diagram di bawah ini menunjukkan bagaimana transaksi dan instruksi bekerja sama untuk memungkinkan pengguna berinteraksi dengan jaringan. Dalam contoh ini, SOL ditransfer dari satu akun ke akun lain.

Metadata akun pengirim menunjukkan bahwa akun tersebut harus menandatangani transaksi. Ini memungkinkan System Program untuk memotong lamport. Baik akun pengirim maupun penerima harus dapat ditulis, agar saldo lamport mereka dapat berubah. Untuk menjalankan instruksi ini, dompet pengirim mengirimkan transaksi yang berisi tanda tangannya dan pesan yang berisi instruksi transfer SOL.

Diagram transfer SOLDiagram transfer SOL

Setelah transaksi dikirim, System Program memproses instruksi transfer dan memperbarui saldo lamport dari kedua akun.

Diagram proses transfer SOLDiagram proses transfer SOL

Verifikasi penerima sebelum mengirim SOL

Transfer System Program menambahkan lamport ke akun mana pun. Tidak ada pemeriksaan di tingkat protokol bahwa penerima dapat memindahkan SOL kembali keluar. lamport hanya dapat dipindahkan keluar oleh program pemilik akun, sehingga mengirim SOL ke token mint, sebuah program, atau PDA yang tidak Anda kendalikan berisiko kehilangan dana secara permanen — hanya otoritas yang ditentukan oleh program pemilik yang dapat mengembalikannya. SOL yang dikirim ke token account hanya dapat dipulihkan oleh pemilik akun tersebut, bukan oleh pengirim.

Transfer token SPL sebagian bersifat melindungi diri: Token Program menolak transfer yang akunnya tidak sesuai dengan mint yang diharapkan. Transfer SOL native tidak memiliki perlindungan semacam itu, sehingga pengirim harus memverifikasi penerima sebelum menandatangani. Lihat Verifikasi Alamat untuk logika klasifikasi lengkapnya.

Contoh di bawah ini menunjukkan kode yang relevan dengan diagram di atas. Lihat fungsi transfer milik 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.

Contoh berikut menunjukkan struktur transaksi yang berisi satu instruksi transfer 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.

Kode di bawah ini menampilkan output dari cuplikan kode sebelumnya. Formatnya berbeda antar SDK, namun perhatikan bahwa setiap instruksi memuat informasi yang diperlukan yang sama.

{
"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
}
}
]
}

Verifikasi penerima sebelum melakukan transfer

Karena transfer SOL berhasil ke akun mana pun, periksa penerima sebelum menandatangani. Ambil data akun dan hanya kirim ke dompet System Program (atau alamat on-curve yang belum didanai); tolak mint, token account, program, dan PDA yang tidak Anda kendalikan.

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.

Cuplikan ini memeriksa penerima SOL native. Untuk klasifikasi lengkap yang juga menangani pengiriman token SPL (token account, ATA, Token-2022), lihat Verify Address.

Mengambil detail transaksi

Setelah pengiriman, ambil detail transaksi menggunakan tanda tangan transaksi dan metode RPC getTransaction.

Anda juga dapat menemukan transaksi menggunakan Solana 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"
}

Respons mentah mengidentifikasi akun berdasarkan indeks dan menyimpan instruksi inner (CPI) sebagai blob yang telah dikodekan. Untuk menguraikannya menjadi alamat dan menelusuri pohon instruksi secara lengkap, lihat Transaction Introspection.

Is this page helpful?

Daftar Isi

Edit Halaman
© 2026 Yayasan Solana. Semua hak dilindungi.