Транзакції та інструкції
На Solana користувачі надсилають транзакції для взаємодії з мережею. Транзакції містять одну або більше інструкцій, які визначають операції для обробки. Логіка виконання інструкцій зберігається в програмах, розгорнутих у мережі Solana, де кожна програма визначає власний набір інструкцій.
Нижче наведено ключові деталі про обробку транзакцій Solana:
- Якщо транзакція включає кілька інструкцій, вони виконуються в порядку додавання до транзакції.
- Транзакції є "атомарними" - всі інструкції повинні бути оброблені успішно, інакше вся транзакція не виконується і жодних змін не відбувається.
Транзакція — це по суті запит на обробку однієї або кількох інструкцій.
Спрощена транзакція
Транзакція схожа на конверт, що містить форми. Кожна форма — це інструкція, яка вказує мережі, що робити. Надсилання транзакції схоже на відправлення конверта поштою для обробки форм.
Ключові моменти
- Транзакції Solana включають інструкції, які викликають програми в мережі.
- Транзакції є атомарними — якщо будь-яка інструкція не виконується, вся транзакція не виконується і жодних змін не відбувається.
- Інструкції в транзакції виконуються в послідовному порядку.
- Обмеження розміру транзакції становить 1232 байти.
- Кожна інструкція потребує трьох елементів інформації:
- Адреса програми для виклику
- Облікові записи, з яких інструкція читає або в які записує
- Будь-які додаткові дані, необхідні для інструкції (наприклад, аргументи функції)
Приклад переказу SOL
Діаграма нижче представляє транзакцію з однією інструкцією для переказу SOL від відправника до отримувача.
У Solana "гаманці" — це рахунки, якими володіє Системна програма. Лише власник програми може змінювати дані рахунку, тому для переказу SOL потрібно надіслати транзакцію для виклику Системної програми.
Переказ SOL
Рахунок відправника повинен підписати (is_signer
) транзакцію, щоб дозволити
Системній програмі зменшити баланс його лампортів. Рахунки відправника та
отримувача повинні бути доступними для запису (is_writable
), оскільки їхні
баланси лампортів змінюються.
Після надсилання транзакції Системна програма обробляє інструкцію переказу. Потім Системна програма оновлює баланси лампортів обох рахунків — відправника та отримувача.
Процес переказу 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 clusterconst rpc = createSolanaRpc("http://localhost:8899");const rpcSubscriptions = createSolanaRpcSubscriptions("ws://localhost:8900");// Generate sender and recipient keypairsconst 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 airdropawait airdropFactory({ rpc, rpcSubscriptions })({recipientAddress: sender.address,lamports: lamports(LAMPORTS_PER_SOL), // 1 SOLcommitment: "confirmed"});// Check balance before transferconst { 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 recipientconst transferInstruction = getTransferSolInstruction({source: sender,destination: recipient.address,amount: transferAmount // 0.01 SOL in lamports});// Add the transfer instruction to a new transactionconst { 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 networkconst signedTransaction =await signTransactionMessageWithSigners(transactionMessage);await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signedTransaction,{ commitment: "confirmed" });const transactionSignature = getSignatureFromTransaction(signedTransaction);// Check balance after transferconst { 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);
Клієнтські бібліотеки часто абстрагують деталі для створення інструкцій програми. Якщо бібліотека недоступна, ви можете вручну створити інструкцію. Це вимагає знання деталей реалізації інструкції.
Приклади нижче показують, як вручну створити інструкцію переказу. Вкладка
Expanded Instruction
функціонально еквівалентна вкладці Instruction
.
- Kit
const transferAmount = 0.01; // 0.01 SOLconst transferInstruction = getTransferSolInstruction({source: sender,destination: recipient.address,amount: transferAmount * LAMPORTS_PER_SOL});
- Legacy
const transferAmount = 0.01; // 0.01 SOLconst transferInstruction = SystemProgram.transfer({fromPubkey: sender.publicKey,toPubkey: receiver.publicKey,lamports: transferAmount * LAMPORTS_PER_SOL});
- Rust
let transfer_amount = LAMPORTS_PER_SOL / 100; // 0.01 SOLlet transfer_instruction =system_instruction::transfer(&sender.pubkey(), &recipient.pubkey(), transfer_amount);
У розділах нижче ми розглянемо деталі транзакцій та інструкцій.
Інструкції
Інструкцію для програми Solana можна розглядати як публічну функцію, яку може викликати будь-хто, використовуючи мережу Solana.
Для виклику інструкції програми потрібні три ключові елементи інформації:
- ID програми: програма з логікою виконання для інструкції
- Облікові записи: список облікових записів, які потрібні інструкції
- Instruction data: байтовий масив даних, що визначає інструкцію для виклику в програмі та будь-які аргументи, необхідні для інструкції
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
: чи модифікує інструкція дані облікового запису
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,}
AccountMeta
Завдяки попередньому визначенню, які облікові записи інструкція читає або записує, транзакції, що не змінюють одні й ті самі облікові записи, можуть виконуватися паралельно.
Приклад структури інструкції
Запустіть приклади нижче, щоб побачити структуру інструкції переказу SOL.
import { generateKeyPairSigner, lamports } from "@solana/kit";import { getTransferSolInstruction } from "@solana-program/system";// Generate sender and recipient keypairsconst sender = await generateKeyPairSigner();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 = getTransferSolInstruction({source: sender,destination: recipient.address,amount: transferAmount});console.log(JSON.stringify(transferInstruction, null, 2));
Наступні приклади показують вивід з попередніх фрагментів коду. Точний формат відрізняється залежно від 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 складається з:
- Підписи: Масив підписів, включених у транзакцію.
- Повідомлення: Список інструкцій, які будуть оброблені атомарно.
pub struct Transaction {#[wasm_bindgen(skip)]#[serde(with = "short_vec")]pub signatures: Vec<Signature>,#[wasm_bindgen(skip)]pub 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 байти) та інструкції
Формат транзакції
Заголовок повідомлення
Заголовок повідомлення використовує три байти для визначення привілеїв рахунків.
- Необхідні підписи
- Кількість підписаних рахунків лише для читання
- Кількість непідписаних рахунків лише для читання
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,}
Заголовок повідомлення
Формат компактного масиву
Компактний масив у повідомленні транзакції — це масив, серіалізований у такому форматі:
- Довжина масиву (закодована як compact-u16)
- Елементи масиву, перелічені один за одним
Формат компактного масиву
Цей формат використовується для кодування довжин масивів адрес облікових записів та інструкцій у повідомленнях транзакцій.
Масив адрес облікових записів
Повідомлення транзакції містить масив адрес облікових записів, необхідних для її інструкцій. Масив починається з числа compact-u16, яке вказує скільки адрес він містить. Адреси впорядковані за їхніми привілеями, як визначено в заголовку повідомлення.
- Облікові записи, які доступні для запису та є підписантами
- Облікові записи, які доступні лише для читання та є підписантами
- Облікові записи, які доступні для запису та не є підписантами
- Облікові записи, які доступні лише для читання та не є підписантами
Компактний масив адрес облікових записів
Нещодавній хеш блоку
Кожна транзакція потребує нещодавнього хешу блоку, який служить двом цілям:
- Діє як часова мітка
- Запобігає дублюванню транзакцій
Хеш блоку закінчується після 150 блоків (приблизно 1 хвилина, припускаючи час блоку 400 мс), після чого транзакція не може бути оброблена.
Ви можете використовувати метод RPC
getLatestBlockhash
для отримання
поточного хешу блоку та останньої висоти блоку, на якій хеш блоку буде дійсним.
Ось приклад на
Solana Playground.
Масив інструкцій
Повідомлення транзакції містить масив інструкцій у типі CompiledInstruction. Інструкції перетворюються на цей тип при додаванні до транзакції.
Як і масив адрес облікових записів у повідомленні, він починається з compact-u16 довжини, за якою йдуть дані інструкції. Кожна інструкція містить:
- Індекс ID програми: індекс u8, який вказує на адресу програми в масиві адрес облікових записів. Це визначає програму, яка оброблятиме інструкцію.
- Індекси облікових записів: масив індексів u8, які вказують на адреси облікових записів, необхідних для цієї інструкції.
- Instruction 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>,}
Компактний масив інструкцій
Приклад структури транзакції
Запустіть приклади нижче, щоб побачити структуру транзакції з однією інструкцією переказу 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 keypairsconst sender = await generateKeyPairSigner();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 = getTransferSolInstruction({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}}]}
Коли ви отримуєте транзакцію за її підписом після надсилання до мережі, ви отримаєте відповідь з наступною структурою.
Поле message
містить такі поля:
-
header
: визначає привілеї читання/запису та підписувача для адрес у масивіaccountKeys
-
accountKeys
: масив усіх адрес облікових записів, використаних в інструкціях транзакції -
recentBlockhash
: хеш блоку, використаний для часової мітки транзакції -
instructions
: масив інструкцій для виконання. Коженaccount
таprogramIdIndex
в інструкції посилається на масивaccountKeys
за індексом. -
signatures
: масив, що включає підписи для всіх облікових записів, які потрібні як підписувачі для інструкцій транзакції. Підпис створюється шляхом підписання повідомлення транзакції за допомогою відповідного приватного ключа для облікового запису.
{"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?