Кратко
Транзакция состоит из подписей и сообщения. Сообщение содержит заголовок, адреса аккаунтов, недавний 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
Проверьте получателя перед отправкой SOL
Перевод через System Program добавляет lamport на любой аккаунт. На уровне протокола не предусмотрено проверки того, сможет ли получатель вывести SOL обратно. Lamport могут быть выведены только программой-владельцем аккаунта, поэтому отправка SOL на mint токена, программу или PDA, которым вы не управляете, грозит безвозвратной потерей средств — вернуть их сможет только полномочный орган, указанный программой-владельцем. SOL, отправленный на token account, может быть получен только владельцем этого аккаунта, но не отправителем.
Переводы SPL токенов частично защищены от ошибок: Token Program отклоняет перевод, если аккаунты не соответствуют ожидаемому mint. Переводы нативного SOL лишены подобной защиты, поэтому отправитель обязан проверить получателя перед подписанием транзакции. Полную логику классификации см. в разделе Проверка адреса.
В примере ниже приведён код, соответствующий приведённым выше диаграммам. См.
функцию
transfer
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 transferconst { 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 recipientconst 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 transferconst { 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);
В следующем примере показана структура транзакции, содержащей единственную инструкцию перевода 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 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 = client.system.instructions.transferSol({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}}]}
Проверьте получателя перед переводом
Поскольку перевод SOL проходит успешно на любой аккаунт, проверяйте получателя перед подписанием. Получите данные аккаунта и отправляйте средства только на кошелёк System Program (или на незафондированный адрес на кривой); отклоняйте mint'ы, token account'ы, программы и PDA, которыми вы не управляете.
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);
Этот фрагмент проверяет получателей нативного SOL. Полную классификацию, которая также обрабатывает отправку SPL-токенов (token accounts, ATA, Token-2022), см. в разделе Проверка адреса.
Получение деталей транзакции
После отправки получите детали транзакции, используя подпись транзакции и метод 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?