Транзакции и инструкции
В 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
.
const transferAmount = 0.01; // 0.01 SOLconst transferInstruction = getTransferSolInstruction({source: sender,destination: recipient.address,amount: transferAmount * LAMPORTS_PER_SOL});
В следующих разделах мы подробно рассмотрим транзакции и инструкции.
Инструкции
Инструкция в программе Solana program может быть представлена как публичная функция, которую может вызвать любой пользователь сети Solana.
Программу Solana можно представить как веб-сервер, размещённый в сети Solana,
где каждая инструкция подобна публичной конечной точке API, которую пользователи
могут вызывать для выполнения определённых действий. Вызов инструкции аналогичен
отправке POST
запроса на конечную точку API, что позволяет пользователям
выполнять бизнес-логику программы.
Чтобы вызвать инструкцию программы в Solana, необходимо создать Instruction
с
тремя элементами информации:
- Program ID: Адрес программы с бизнес-логикой для вызываемой инструкции.
- Accounts: Список всех аккаунтов, из которых инструкция читает данные или в которые записывает.
- 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
При создании Instruction
необходимо предоставить каждый требуемый аккаунт в
виде
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,}
Указывая заранее, какие аккаунты инструкция читает или изменяет, транзакции, которые не модифицируют одни и те же аккаунты, могут выполняться параллельно.
Чтобы узнать, какие аккаунты требуются для выполнения инструкции, включая те, которые должны быть доступны для записи, только для чтения или подписывать транзакцию, необходимо обратиться к реализации инструкции, определенной программой.
На практике вам обычно не нужно вручную создавать Instruction
. Большинство
разработчиков программ предоставляют клиентские библиотеки с вспомогательными
функциями, которые создают инструкции за вас.
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: Список аккаунтов, необходимых для выполнения инструкции. Для каждого аккаунта инструкция должна указать его адрес, необходимость подписания транзакции и возможность записи в него.
- Data: Буфер байтов, который сообщает программе, какую инструкцию выполнить, и включает любые аргументы, необходимые для выполнения инструкции.
{"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 состоит из:
- Подписи: Массив
подписей
от всех аккаунтов, которые необходимы в качестве подписантов для инструкций в
транзакции. Подпись создается путем подписания транзакции
Message
с использованием приватного ключа аккаунта. - Сообщение: Транзакция сообщение включает список инструкций, которые должны быть обработаны атомарно.
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, указывающего, сколько адресов он содержит.
Чтобы сэкономить место, транзакция не хранит разрешения для каждого аккаунта по
отдельности. Вместо этого она использует комбинацию MessageHeader
и строгую
упорядоченность адресов аккаунтов для определения разрешений.
Адреса всегда упорядочены следующим образом:
- Аккаунты, которые имеют права записи и являются подписантами
- Аккаунты, которые только для чтения и являются подписантами
- Аккаунты, которые имеют права записи и не являются подписантами
- Аккаунты, которые только для чтения и не являются подписантами
MessageHeader
предоставляет значения, используемые для определения количества
аккаунтов в каждой группе разрешений.
Компактный массив адресов аккаунтов
Недавний блокхеш
Каждая транзакция требует недавнего блокхеша, который выполняет две функции:
- Действует как временная метка для создания транзакции
- Предотвращает дублирование транзакций
Блокхеш истекает через 150 блоков (примерно 1 минута при времени блока 400 мс), после чего транзакция считается истекшей и не может быть обработана.
Вы можете использовать метод RPC
getLatestBlockhash
, чтобы получить
текущий блокхеш и последний номер блока, на котором блокхеш будет действителен.
Массив инструкций
Сообщение транзакции содержит массив инструкций в формате CompiledInstruction. Инструкции преобразуются в этот формат при добавлении в транзакцию.
Как и массив адресов аккаунтов в сообщении, он начинается с длины в формате compact-u16, за которой следуют данные инструкции. Каждая инструкция содержит:
- Индекс Program ID: Индекс, указывающий на адрес программы в массиве адресов аккаунтов. Это определяет программу, которая обрабатывает инструкцию.
- Индексы аккаунтов: Массив индексов, указывающих на адреса аккаунтов, необходимых для выполнения этой инструкции.
- Данные инструкции: Массив байтов, определяющий, какую инструкцию вызвать в программе, а также любые дополнительные данные, необходимые для выполнения инструкции (например, аргументы функции).
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}}]}
После отправки транзакции вы можете получить её детали, используя метод RPC getTransaction. Ответ будет иметь структуру, похожую на следующий фрагмент. Кроме того, вы можете изучить транзакцию с помощью Solana Explorer.
"Подпись транзакции" уникально идентифицирует транзакцию в Solana. Вы используете эту подпись, чтобы найти детали транзакции в сети. Подпись транзакции — это просто первая подпись в транзакции. Обратите внимание, что первая подпись также является подписью плательщика комиссии за транзакцию.
{"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?