Кратко
Транзакция состоит из подписей и сообщения. Сообщение содержит заголовок, адреса аккаунтов, недавний blockhash и скомпилированные инструкции. Максимальный сериализованный размер: 1 232 байта.
Transaction
имеет два основных поля:
signatures: Массив подписейmessage: Информация о транзакции, включая список инструкций для обработки
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: Массив инструкций
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: Количество неподписанных аккаунтов только для чтения.
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
— это компактно закодированный массив публичных ключей. Каждый элемент
определяет аккаунт, используемый хотя бы одной из инструкций транзакции. Массив
должен включать каждый аккаунт и строго соблюдать следующий порядок:
- Подписант + с правом записи
- Подписант + только для чтения
- Не подписант + с правом записи
- Не подписант + только для чтения
Такой строгий порядок позволяет массиву account_keys использоваться вместе с
тремя счетчиками из header сообщения для определения разрешений
для каждого аккаунта без необходимости хранить отдельные флаги метаданных для
каждого аккаунта. Счетчики в заголовке разбивают массив на четыре группы
разрешений, перечисленные выше.
Диаграмма, показывающая порядок массива адресов аккаунтов
Последний blockhash
Поле recent_blockhash — это 32-байтовый хеш, который выполняет две функции:
- Метка времени: доказывает, что транзакция была создана недавно.
- Дедупликация: предотвращает повторную обработку одной и той же транзакции.
Blockhash истекает через 150 слотов. Если blockhash больше не действителен к
моменту поступления транзакции, она отклоняется с ошибкой
BlockhashNotFound, если только это не валидная
транзакция с долговременным nonce.
Метод RPC getLatestBlockhash позволяет
получить текущий blockhash и последний номер блока, на котором blockhash будет
действителен.
Инструкции
Поле
instructions
— это компактно закодированный массив структур
CompiledInstruction.
Каждая CompiledInstruction ссылается на аккаунты по индексу в массиве
account_keys, а не по полному публичному ключу. Она содержит:
program_id_index: Индекс вaccount_keys, определяющий вызываемую программу.accounts: Массив индексов вaccount_keys, указывающий аккаунты, передаваемые в программу.data: Массив байтов, содержащий дискриминатор инструкции и сериализованные аргументы.
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_signatures | 1–3 байта (compact-u16) | Количество подписей |
signatures | num_signatures × 64 байта | Подписи Ed25519 |
num_required_signatures | 1 байт | Поле 1 MessageHeader |
num_readonly_signed | 1 байт | Поле 2 MessageHeader |
num_readonly_unsigned | 1 байт | Поле 3 MessageHeader |
num_account_keys | 1–3 байта (compact-u16) | Количество статических ключей аккаунтов |
account_keys | num_account_keys × 32 байта | Публичные ключи |
recent_blockhash | 32 байта | Blockhash |
num_instructions | 1–3 байта (compact-u16) | Количество инструкций |
instructions | переменный | Массив скомпилированных инструкций |
Каждая скомпилированная инструкция сериализуется следующим образом:
| Поле | Размер | Описание |
|---|---|---|
program_id_index | 1 байт | Индекс в ключах аккаунтов |
num_accounts | 1–3 байта (compact-u16) | Количество индексов аккаунтов |
account_indices | num_accounts × 1 байт | Индексы ключей аккаунтов |
data_len | 1–3 байта (compact-u16) | Длина instruction data |
data | data_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
После отправки транзакции System Program обрабатывает инструкцию перевода и обновляет баланс lamport обоих аккаунтов.
Диаграмма процесса перевода 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 clusterconst rpc = createSolanaRpc("http://localhost:8899");const rpcSubscriptions = createSolanaRpcSubscriptions("ws://localhost:8900");// Generate sender and recipient keypairsconst 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 airdropawait airdropFactory({ rpc, rpcSubscriptions })({recipientAddress: sender.address,lamports: lamports(LAMPORTS_PER_SOL), // 1 SOLcommitment: "confirmed"});// Check balance before transferconst { 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 recipientconst transferInstruction = getTransferSolInstruction({source: sender,destination: recipient.address,amount: transferAmount // 0.01 SOL in lamports});// Add the transfer instruction to a new transactionconst { 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 networkconst signedTransaction =await signTransactionMessageWithSigners(transactionMessage);await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signedTransaction,{ commitment: "confirmed" });const transactionSignature = getSignatureFromTransaction(signedTransaction);// Check balance after transferconst { 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);
Следующий пример показывает структуру транзакции, содержащей одну инструкцию перевода 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 keypairsconst sender = await generateKeyPairSigner();const recipient = await generateKeyPairSigner();// Define the amount to transferconst 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 recipientconst transferInstruction = getTransferSolInstruction({source: sender,destination: recipient.address,amount: transferAmount});// Create transaction messageconst transactionMessage = pipe(createTransactionMessage({ version: 0 }),(tx) => setTransactionMessageFeePayerSigner(sender, tx),(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),(tx) => appendTransactionMessageInstructions([transferInstruction], tx));const signedTransaction =await signTransactionMessageWithSigners(transactionMessage);// Decode the messageBytesconst compiledTransactionMessage =getCompiledTransactionMessageDecoder().decode(signedTransaction.messageBytes);console.log(JSON.stringify(compiledTransactionMessage, null, 2));
Ниже приведён вывод предыдущих фрагментов кода. Формат отличается в зависимости от 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.
{"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?