Структура транзакции

Кратко

Транзакция состоит из подписей и сообщения. Сообщение содержит заголовок, адреса аккаунтов, недавний blockhash и скомпилированные инструкции. Максимальный сериализованный размер: 1 232 байта.

Transaction имеет два основных поля:

  • signatures: Массив подписей
  • message: Информация о транзакции, включая список инструкций для обработки
Transaction
pub struct Transaction {
pub signatures: Vec<Signature>,
pub message: Message,
}

Схема, показывающая две части транзакцииСхема, показывающая две части транзакции

Общий сериализованный размер транзакции не должен превышать PACKET_DATA_SIZE (1 232 байт). Этот лимит равен 1 280 байтам (минимальный MTU IPv6) минус 48 байт на сетевые заголовки (40 байт IPv6 + 8 байт заголовок фрагмента). В эти 1 232 байта входят как массив signatures, так и структура message.

Схема, показывающая формат транзакции и ограничения по размеруСхема, показывающая формат транзакции и ограничения по размеру

Подписи

Поле signatures — это компактно закодированный массив Signature. Каждый Signature — это 64-байтовая подпись Ed25519 сериализованного Message, подписанная приватным ключом аккаунта-подписанта. Одна подпись требуется для каждого аккаунта-подписанта, на который ссылаются инструкции транзакции.

Первая подпись в массиве принадлежит плательщику комиссии — аккаунту, который оплачивает базовую и приоритетную комиссию за транзакцию. Эта первая подпись также служит ID транзакции, который используется для поиска транзакции в сети. ID транзакции часто называют подписью транзакции.

Требования к плательщику комиссии:

  • Должен быть первым аккаунтом в сообщении (индекс 0) и быть подписантом.
  • Должен быть аккаунтом, принадлежащим System Program, или nonce-аккаунтом (проверяется с помощью validate_fee_payer).
  • Должен содержать достаточно лампортов для покрытия rent_exempt_minimum + total_fee; в противном случае транзакция завершится с ошибкой InsufficientFundsForFee.

Сообщение

Поле message — это структура Message, содержащая полезную нагрузку транзакции:

  • header: Заголовок сообщения (header)
  • account_keys: Массив адресов аккаунтов, необходимых для инструкций транзакции
  • recent_blockhash: blockhash, который выступает в роли временной метки для транзакции
  • instructions: Массив инструкций
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,
}

Диаграмма, показывающая три части заголовка сообщенияДиаграмма, показывающая три части заголовка сообщения

Адреса аккаунтов

Поле account_keys — это компактно закодированный массив публичных ключей. Каждый элемент определяет аккаунт, используемый хотя бы одной из инструкций транзакции. Массив должен включать каждый аккаунт и строго соблюдать следующий порядок:

  1. Подписант + с правом записи
  2. Подписант + только для чтения
  3. Не подписант + с правом записи
  4. Не подписант + только для чтения

Такой строгий порядок позволяет массиву account_keys использоваться вместе с тремя счетчиками из header сообщения для определения разрешений для каждого аккаунта без необходимости хранить отдельные флаги метаданных для каждого аккаунта. Счетчики в заголовке разбивают массив на четыре группы разрешений, перечисленные выше.

Диаграмма, показывающая порядок массива адресов аккаунтовДиаграмма, показывающая порядок массива адресов аккаунтов

Последний blockhash

Поле recent_blockhash — это 32-байтовый хеш, который выполняет две функции:

  1. Метка времени: доказывает, что транзакция была создана недавно.
  2. Дедупликация: предотвращает повторную обработку одной и той же транзакции.

Blockhash истекает через 150 слотов. Если blockhash больше не действителен к моменту поступления транзакции, она отклоняется с ошибкой BlockhashNotFound, если только это не валидная транзакция с долговременным nonce.

Метод RPC getLatestBlockhash позволяет получить текущий blockhash и последний номер блока, на котором blockhash будет действителен.

Инструкции

Поле instructions — это компактно закодированный массив структур CompiledInstruction. Каждая CompiledInstruction ссылается на аккаунты по индексу в массиве account_keys, а не по полному публичному ключу. Она содержит:

  1. program_id_index: Индекс в account_keys, определяющий вызываемую программу.
  2. accounts: Массив индексов в account_keys, указывающий аккаунты, передаваемые в программу.
  3. 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 используется 1 байт, для больших значений — 2–3 байта.

Структура legacy-транзакции (в сети):

ПолеРазмерОписание
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 байтаПубличные ключи
recent_blockhash32 байтаBlockhash
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 переводится с одного аккаунта на другой.

Метаданные аккаунта отправителя metadata указывают, что он должен подписать транзакцию. Это позволяет System Program списать lamport. Оба аккаунта — отправителя и получателя — должны быть доступны для записи, чтобы их баланс lamport мог измениться. Для выполнения этой инструкции кошелёк отправителя отправляет транзакцию с подписью и сообщением, содержащим инструкцию перевода SOL.

Диаграмма перевода SOLДиаграмма перевода SOL

После отправки транзакции System Program обрабатывает инструкцию перевода и обновляет баланс lamport обоих аккаунтов.

Диаграмма процесса перевода SOLДиаграмма процесса перевода SOL

Пример ниже показывает код, относящийся к приведённым выше диаграммам. См. функцию System Program transfer.

import {
airdropFactory,
appendTransactionMessageInstructions,
createSolanaRpc,
createSolanaRpcSubscriptions,
createTransactionMessage,
generateKeyPairSigner,
getSignatureFromTransaction,
lamports,
pipe,
sendAndConfirmTransactionFactory,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
signTransactionMessageWithSigners
} from "@solana/kit";
import { getTransferSolInstruction } from "@solana-program/system";
// Create a connection to cluster
const rpc = createSolanaRpc("http://localhost:8899");
const rpcSubscriptions = createSolanaRpcSubscriptions("ws://localhost:8900");
// Generate sender and recipient keypairs
const sender = await generateKeyPairSigner();
const recipient = await generateKeyPairSigner();
const LAMPORTS_PER_SOL = 1_000_000_000n;
const transferAmount = lamports(LAMPORTS_PER_SOL / 100n); // 0.01 SOL
// Fund sender with airdrop
await airdropFactory({ rpc, rpcSubscriptions })({
recipientAddress: sender.address,
lamports: lamports(LAMPORTS_PER_SOL), // 1 SOL
commitment: "confirmed"
});
// Check balance before transfer
const { value: preBalance1 } = await rpc.getBalance(sender.address).send();
const { value: preBalance2 } = await rpc.getBalance(recipient.address).send();
// Create a transfer instruction for transferring SOL from sender to recipient
const transferInstruction = getTransferSolInstruction({
source: sender,
destination: recipient.address,
amount: transferAmount // 0.01 SOL in lamports
});
// Add the transfer instruction to a new transaction
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(sender, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstructions([transferInstruction], tx)
);
// Send the transaction to the network
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(
signedTransaction,
{ commitment: "confirmed" }
);
const transactionSignature = getSignatureFromTransaction(signedTransaction);
// Check balance after transfer
const { value: postBalance1 } = await rpc.getBalance(sender.address).send();
const { value: postBalance2 } = await 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);
Console
Click to execute the code.

Следующий пример показывает структуру транзакции, содержащей одну инструкцию перевода SOL.

import {
createSolanaRpc,
generateKeyPairSigner,
lamports,
createTransactionMessage,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstructions,
pipe,
signTransactionMessageWithSigners,
getCompiledTransactionMessageDecoder
} from "@solana/kit";
import { getTransferSolInstruction } from "@solana-program/system";
const rpc = createSolanaRpc("http://localhost:8899");
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
// Generate sender and recipient keypairs
const sender = await generateKeyPairSigner();
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 = getTransferSolInstruction({
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
}
}
]
}

Получение информации о транзакции

После отправки получите детали транзакции, используя подпись транзакции и RPC-метод getTransaction.

Найти транзакцию также можно через 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"
}

Is this page helpful?

Управляется

© 2026 Solana Foundation.
Все права защищены.
Связаться с нами