Транзакції та інструкції

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

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

  • Якщо транзакція включає кілька інструкцій, вони виконуються в порядку додавання до транзакції.
  • Транзакції є "атомарними" - всі інструкції повинні бути оброблені успішно, інакше вся транзакція не виконується і жодних змін не відбувається.

Транзакція - це по суті запит на обробку однієї або кількох інструкцій. Ви можете уявити транзакцію як конверт, що містить форми. Кожна форма - це інструкція, яка вказує мережі, що робити. Надсилання транзакції подібне до відправлення конверта поштою для обробки форм.

Спрощена транзакціяСпрощена транзакція

Ключові моменти

  • Транзакції Solana включають інструкції, які викликають програми в мережі.
  • Транзакції є атомарними - якщо будь-яка інструкція не виконується, вся транзакція не виконується і жодних змін не відбувається.
  • Інструкції в транзакції виконуються в послідовному порядку.
  • Обмеження розміру транзакції становить 1232 байти.
  • Кожна інструкція потребує трьох елементів інформації:
    1. Адреса програми для виклику
    2. Облікові записи, з яких інструкція читає або в які записує
    3. Будь-які додаткові дані, необхідні для інструкції (наприклад, аргументи функції)

Приклад переказу SOL

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

У Solana "гаманці" — це рахунки, якими володіє System Program. Лише власник програми може змінювати дані рахунку, тому для переказу SOL потрібно надіслати транзакцію для виклику System Program.

Переказ SOLПереказ SOL

Рахунок відправника повинен підписати (is_signer) транзакцію, щоб дозволити System Program зменшити баланс його lamport. Рахунки відправника та отримувача повинні бути доступними для запису (is_writable), оскільки їхні баланси lamport змінюються.

Після надсилання транзакції System Program обробляє інструкцію переказу. Потім System Program оновлює баланси lamport обох рахунків — відправника та отримувача.

Процес переказу SOLПроцес переказу SOL

Приклади нижче показують, як надіслати транзакцію, яка переказує SOL з одного рахунку на інший. Перегляньте вихідний код інструкції переказу System Program тут.

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.

const transferAmount = 0.01; // 0.01 SOL
const transferInstruction = getTransferSolInstruction({
source: sender,
destination: recipient.address,
amount: transferAmount * LAMPORTS_PER_SOL
});

У розділах нижче ми розглянемо деталі транзакцій та інструкцій.

Інструкції

Інструкцію в Solana програмі можна розглядати як публічну функцію, яку може викликати будь-хто, використовуючи мережу Solana.

Ви можете уявити програму Solana як веб-сервер, розміщений у мережі Solana, де кожна інструкція подібна до публічної кінцевої точки API, яку користувачі можуть викликати для виконання певних дій. Виклик інструкції схожий на надсилання POST запиту до кінцевої точки API, що дозволяє користувачам виконувати бізнес-логіку програми.

Щоб викликати інструкцію програми в Solana, вам потрібно створити Instruction з трьома елементами інформації:

  • 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

Під час створення Instruction ви повинні надати кожен необхідний обліковий запис як 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,
}

Завдяки попередньому визначенню того, які рахунки інструкція читає або записує, транзакції, які не змінюють одні й ті самі рахунки, можуть виконуватися паралельно.

Щоб знати, які рахунки потрібні для інструкції, включаючи ті, що мають бути доступними для запису, лише для читання або підписувати транзакцію, вам необхідно звернутися до реалізації інструкції, як визначено програмою.

На практиці вам зазвичай не потрібно створювати Instruction вручну. Більшість розробників програм надають клієнтські бібліотеки з допоміжними функціями, які створюють інструкції за вас.

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 вимагає наступної інформації:

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

Транзакції

Після того, як ви створили інструкції, які хочете викликати, наступним кроком є створення Transaction і додавання інструкцій до транзакції. Транзакція Solana складається з:

  1. Підписи: Масив підписів від усіх облікових записів, необхідних як підписанти для інструкцій у транзакції. Підпис створюється шляхом підписання транзакції Message за допомогою приватного ключа облікового запису.
  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 байти кожен), останній хеш блоку (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 числа, що вказує скільки адрес він містить.

Щоб заощадити місце, транзакція не зберігає дозволи для кожного облікового запису окремо. Натомість вона покладається на комбінацію MessageHeader та суворий порядок адрес облікових записів для визначення дозволів.

Адреси завжди впорядковані таким чином:

  1. Облікові записи, які є записуваними та підписантами
  2. Облікові записи, які є лише для читання та підписантами
  3. Облікові записи, які є записуваними та не підписантами
  4. Облікові записи, які є лише для читання та не підписантами

MessageHeader надає значення, які використовуються для визначення кількості облікових записів для кожної групи дозволів.

Компактний масив адрес облікових записівКомпактний масив адрес облікових записів

Останній хеш блоку

Кожна транзакція вимагає останнього хешу блоку, який служить двом цілям:

  1. Діє як часова мітка створення транзакції
  2. Запобігає дублюванню транзакцій

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

Ви можете використовувати метод RPC getLatestBlockhash для отримання поточного хешу блоку та останньої висоти блоку, на якій хеш блоку буде дійсним.

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

Повідомлення транзакції містить масив інструкцій типу CompiledInstruction. Інструкції перетворюються на цей тип під час додавання до транзакції.

Як і масив адрес облікових записів у повідомленні, він починається з compact-u16 довжини, за якою слідують дані інструкції. Кожна інструкція містить:

  1. Індекс ідентифікатора програми: Індекс, який вказує на адресу програми в масиві адрес облікових записів. Це визначає програму, яка обробляє інструкцію.
  2. Індекси облікових записів: Масив індексів, які вказують на адреси облікових записів, необхідних для цієї інструкції.
  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
}
}
]
}

Після відправлення транзакції ви можете отримати її деталі за допомогою RPC-методу getTransaction. Відповідь матиме структуру, подібну до наступного фрагмента. Крім того, ви можете переглянути транзакцію за допомогою Solana Explorer.

"Підпис транзакції" унікально ідентифікує транзакцію в Solana. Ви використовуєте цей підпис для пошуку деталей транзакції в мережі. Підпис транзакції — це просто перший підпис у транзакції. Зауважте, що перший підпис також є підписом платника комісії за транзакцію.

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?