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

В 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);
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));
Click to execute the code.

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

  • 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 состоит из:

  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 байта. Это ограничение происходит из максимального размера передаваемого блока (MTU) IPv6 в 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));
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: Хеш блока, используемый для временной метки транзакции

  • 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?