Транзакции и инструкции
В 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,}
Формат транзакции
Структура сообщения транзакции состоит из:
- Заголовок сообщения: Указывает количество подписывающих и аккаунтов только для чтения.
- Адреса аккаунтов: Массив адресов аккаунтов, необходимых для инструкций в транзакции.
- Недавний 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 байта. Это ограничение происходит из максимального размера передаваемого блока (MTU) IPv6 в 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
: Хеш блока, используемый для временной метки транзакции -
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?