Транзакции и инструкции

В Solana пользователи отправляют транзакции, чтобы взаимодействовать с сетью. Транзакции содержат одну или несколько инструкций, которые определяют операции для обработки. Логика выполнения инструкций хранится в программах, развернутых в сети Solana, где каждая программа определяет свой собственный набор инструкций.

Ниже приведены ключевые детали обработки транзакций в Solana:

  • Если транзакция включает несколько инструкций, инструкции выполняются в порядке их добавления в транзакцию.
  • Транзакции являются "атомарными" — все инструкции должны быть успешно обработаны, иначе вся транзакция завершится неудачей, и никаких изменений не произойдет.

Транзакция по сути является запросом на обработку одной или нескольких инструкций.

Упрощённая транзакцияУпрощённая транзакция

Транзакция похожа на конверт с формами. Каждая форма — это инструкция, которая указывает сети, что нужно сделать. Отправка транзакции подобна отправке конверта, чтобы формы были обработаны.

Основные моменты

  • Транзакции Solana включают инструкции, которые вызывают программы в сети.
  • Транзакции атомарны — если какая-либо инструкция завершится неудачей, вся транзакция завершится неудачей, и никаких изменений не произойдет.
  • Инструкции в транзакции выполняются в последовательном порядке.
  • Ограничение размера транзакции составляет 1232 байт.
  • Каждая инструкция требует три элемента информации:
    1. Адрес программы, которую нужно вызвать
    2. Аккаунты, из которых инструкция читает или в которые записывает данные
    3. Любые дополнительные данные, необходимые для инструкции (например, аргументы функции)

Пример перевода SOL

Диаграмма ниже представляет транзакцию с одной инструкцией для перевода SOL от отправителя к получателю.

На Solana "кошельки" — это аккаунты, принадлежащие Системной программе. Только владелец программы может изменять данные аккаунта, поэтому для перевода SOL необходимо отправить транзакцию, чтобы вызвать Системную программу.

Перевод SOLПеревод SOL

Аккаунт отправителя должен подписать (is_signer) транзакцию, чтобы позволить Системной программе списать его баланс в лампортах. Аккаунты отправителя и получателя должны быть доступны для записи (is_writable), так как их балансы в лампортах изменяются.

После отправки транзакции Системная программа обрабатывает инструкцию перевода. Затем Системная программа обновляет балансы в лампортах как у отправителя, так и у получателя.

Процесс перевода SOLПроцесс перевода SOL

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

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.

Клиентские библиотеки часто абстрагируют детали создания инструкций программы. Если библиотека недоступна, вы можете вручную создать инструкцию. Для этого необходимо знать детали реализации инструкции.

Примеры ниже показывают, как вручную создать инструкцию перевода. Вкладка Expanded Instruction функционально эквивалентна вкладке Instruction.

  • Kit
const transferAmount = 0.01; // 0.01 SOL
const transferInstruction = getTransferSolInstruction({
source: sender,
destination: recipient.address,
amount: transferAmount * LAMPORTS_PER_SOL
});
  • Legacy
const transferAmount = 0.01; // 0.01 SOL
const transferInstruction = SystemProgram.transfer({
fromPubkey: sender.publicKey,
toPubkey: receiver.publicKey,
lamports: transferAmount * LAMPORTS_PER_SOL
});
  • Rust
let transfer_amount = LAMPORTS_PER_SOL / 100; // 0.01 SOL
let transfer_instruction =
system_instruction::transfer(&sender.pubkey(), &recipient.pubkey(), transfer_amount);

В следующих разделах мы подробно рассмотрим транзакции и инструкции.

Инструкции

Инструкция в программе Solana может рассматриваться как публичная функция, которую может вызвать любой пользователь сети Solana.

Для вызова инструкции программы требуется три ключевых элемента информации:

  • ID программы: Программа с логикой выполнения для инструкции
  • Аккаунты: Список аккаунтов, необходимых для инструкции
  • Instruction Data: Массив байтов, указывающий инструкцию для вызова в программе и любые аргументы, необходимые для инструкции
Instruction
pub struct Instruction {
/// Pubkey of the program that executes this instruction.
pub program_id: Pubkey,
/// Metadata describing accounts that should be passed to the program.
pub accounts: Vec<AccountMeta>,
/// Opaque data passed to the program for its own interpretation.
pub data: Vec<u8>,
}

Инструкция транзакцииИнструкция транзакции

AccountMeta

Каждый аккаунт, необходимый для инструкции, должен быть предоставлен как AccountMeta, который содержит:

  • pubkey: Адрес аккаунта
  • is_signer: Требуется ли подпись аккаунта для транзакции
  • is_writable: Изменяет ли инструкция данные аккаунта
AccountMeta
pub struct AccountMeta {
/// An account's public key.
pub pubkey: Pubkey,
/// True if an `Instruction` requires a `Transaction` signature matching `pubkey`.
pub is_signer: bool,
/// True if the account data or metadata may be mutated during program execution.
pub is_writable: bool,
}

AccountMetaAccountMeta

Указывая заранее, какие аккаунты читает или изменяет инструкция, транзакции, которые не изменяют одни и те же аккаунты, могут выполняться параллельно.

Пример структуры инструкции

Запустите примеры ниже, чтобы увидеть структуру инструкции перевода SOL.

import { generateKeyPairSigner, lamports } from "@solana/kit";
import { getTransferSolInstruction } from "@solana-program/system";
// 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
});
console.log(JSON.stringify(transferInstruction, null, 2));
Console
Click to execute the code.

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

  • Program ID: адрес программы, которая выполнит инструкцию.
  • Аккаунты: список аккаунтов, необходимых для выполнения инструкции. Для каждого аккаунта инструкция должна указать его адрес, необходимость подписи транзакции и возможность записи в него.
  • Данные: буфер байтов, который сообщает программе, какую инструкцию выполнить, и включает любые аргументы, необходимые для выполнения инструкции.
{
"accounts": [
{
"address": "Hu28vRMGWpQXN56eaE7jRiDDRRz3vCXEs7EKHRfL6bC",
"role": 3,
"signer": {
"address": "Hu28vRMGWpQXN56eaE7jRiDDRRz3vCXEs7EKHRfL6bC",
"keyPair": {
"privateKey": {},
"publicKey": {}
}
}
},
{
"address": "2mBY6CTgeyJNJDzo6d2Umipw2aGUquUA7hLdFttNEj7p",
"role": 1
}
],
"programAddress": "11111111111111111111111111111111",
"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
}
}

Транзакции

Транзакция Solana transaction состоит из следующих элементов:

  1. Подписи: массив подписей, включенных в транзакцию.
  2. Сообщение: список инструкций, которые должны быть обработаны атомарно.
Transaction
pub struct Transaction {
#[wasm_bindgen(skip)]
#[serde(with = "short_vec")]
pub signatures: Vec<Signature>,
#[wasm_bindgen(skip)]
pub message: Message,
}

Формат транзакцииФормат транзакции

Структура сообщения транзакции состоит из следующих элементов:

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>,
}

Сообщение транзакцииСообщение транзакции

Размер транзакции

Транзакции Solana имеют ограничение по размеру в 1232 байт. Это ограничение связано с максимальным размером блока передачи данных IPv6 (MTU) в 1280 байт, минус 48 байт для сетевых заголовков (40 байт для IPv6 + 8 байт для заголовка фрагмента).

Общий размер транзакции (подписи и сообщение) должен оставаться в пределах этого ограничения и включает:

  • Подписи: 64 байта каждая
  • Сообщение: заголовок (3 байта), ключи аккаунтов (по 32 байта каждый), недавний blockhash (32 байта) и инструкции

Формат транзакцииФормат транзакции

Заголовок сообщения

Заголовок сообщения использует три байта для определения привилегий аккаунтов.

  1. Необходимые подписи
  2. Количество подписанных аккаунтов только для чтения
  3. Количество неподписанных аккаунтов только для чтения
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,
}

Заголовок сообщенияЗаголовок сообщения

Формат компактного массива

Компактный массив в сообщении транзакции — это массив, сериализованный в следующем формате:

  1. Длина массива (закодирована как compact-u16)
  2. Элементы массива, перечисленные один за другим

Формат компактного массиваФормат компактного массива

Этот формат используется для кодирования длин массивов адресов аккаунтов и инструкций в сообщениях транзакций.

Массив адресов аккаунтов

Сообщение транзакции содержит массив адресов аккаунтов, требуемых его инструкциями. Массив начинается с числа compact-u16, указывающего, сколько адресов он содержит. Адреса затем упорядочиваются по их привилегиям, как это определено в заголовке сообщения.

  • Аккаунты, которые могут быть изменены и являются подписантами
  • Аккаунты, которые только для чтения и являются подписантами
  • Аккаунты, которые могут быть изменены и не являются подписантами
  • Аккаунты, которые только для чтения и не являются подписантами

Компактный массив адресов аккаунтовКомпактный массив адресов аккаунтов

Недавний блокхеш

Каждая транзакция требует недавний блокхеш, который выполняет две функции:

  1. Действует как временная метка
  2. Предотвращает дублирование транзакций

Блокхеш истекает через 150 блоков (примерно 1 минута при условии времени блока 400 мс), после чего транзакция не может быть обработана.

Вы можете использовать метод RPC getLatestBlockhash, чтобы получить текущий блокхеш и последний номер блока, на котором блокхеш будет действителен. Вот пример на Solana Playground.

Массив инструкций

Сообщение транзакции содержит массив инструкций в формате CompiledInstruction. Инструкции преобразуются в этот формат при добавлении в транзакцию.

Как и массив адресов аккаунтов в сообщении, он начинается с длины compact-u16, за которой следуют данные инструкции. Каждая инструкция содержит:

  1. Индекс ID программы: Индекс типа u8, указывающий на адрес программы в массиве адресов аккаунтов. Это определяет программу, которая будет обрабатывать инструкцию.
  2. Индексы аккаунтов: Массив индексов типа u8, указывающих на адреса аккаунтов, необходимых для выполнения этой инструкции.
  3. Instruction 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>,
}

Компактный массив инструкцийКомпактный массив инструкций

Пример структуры транзакции

Запустите примеры ниже, чтобы увидеть структуру транзакции с одной инструкцией перевода 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
}
}
]
}

Когда вы запрашиваете транзакцию по её подписи после отправки в сеть, вы получите ответ со следующей структурой.

Поле message содержит следующие поля:

  • header: Указывает права на чтение/запись и привилегии подписанта для адресов в массиве accountKeys

  • accountKeys: Массив всех адресов аккаунтов, используемых в инструкциях транзакции

  • recentBlockhash: Blockhash, используемый для отметки времени транзакции

  • instructions: Массив инструкций для выполнения. Каждая account и programIdIndex в инструкции ссылаются на массив accountKeys по индексу.

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

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?