Транзакции и инструкции
В 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 требует следующей информации:
- 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 состоит из следующих элементов:
- Подписи: массив подписей, включенных в транзакцию.
- Сообщение: список инструкций, которые должны быть обработаны атомарно.
pub struct Transaction {#[wasm_bindgen(skip)]#[serde(with = "short_vec")]pub signatures: Vec<Signature>,#[wasm_bindgen(skip)]pub message: Message,}
Формат транзакции
Структура сообщения транзакции состоит из следующих элементов:
- Заголовок сообщения: указывает количество подписантов и аккаунтов только для чтения.
- Адреса аккаунтов: массив адресов аккаунтов, необходимых для выполнения инструкций в транзакции.
- Недавний Blockhash: действует как временная метка для транзакции.
- Инструкции: массив инструкций для выполнения.
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 байта) и инструкции
Формат транзакции
Заголовок сообщения
Заголовок сообщения использует три байта для определения привилегий аккаунтов.
- Необходимые подписи
- Количество подписанных аккаунтов только для чтения
- Количество неподписанных аккаунтов только для чтения
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
: Blockhash, используемый для отметки времени транзакции -
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?