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

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

  • 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 байти кожен), останній хеш блоку (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: хеш блоку, використаний для часової мітки транзакції

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