Підсумок
Транзакція містить підписи + повідомлення. Повідомлення містить заголовок, адреси облікових записів, останній блокхеш та скомпільовані інструкції. Максимальний серіалізований розмір: 1 232 байти.
Transaction
має два поля верхнього рівня:
signatures: масив підписівmessage: інформація про транзакцію, включаючи список інструкцій для обробки
pub struct Transaction {pub signatures: Vec<Signature>,pub message: Message,}
Діаграма, що показує дві частини транзакції
Загальний серіалізований розмір транзакції не повинен перевищувати
PACKET_DATA_SIZE
(1 232 байти). Це обмеження дорівнює 1 280 байтам (мінімальний MTU IPv6) мінус
48 байтів для мережевих заголовків (40 байтів IPv6 + 8 байтів заголовок
фрагмента). 1 232 байти включають як масив signatures, так і
структуру message.
Діаграма, що показує формат транзакції та обмеження розміру
Підписи
Поле signatures — це компактно закодований масив значень
Signature.
Кожен Signature — це 64-байтний підпис Ed25519 серіалізованого Message,
підписаний приватним ключем облікового запису підписувача. Один підпис потрібен
для кожного облікового запису підписувача, на який
посилаються інструкції транзакції.
Кожен підпис створюється приватним ключем. Де зберігається цей ключ — локальний keypair, хмарний HSM або KMS, чи керований сервіс гаманця — є рішенням у рамках проєктування виробничої системи. Дивіться Підписання у виробничому середовищі.
Перший підпис у масиві належить платнику комісії — обліковому запису, який сплачує базову комісію та комісію за пріоритизацію транзакції. Цей перший підпис також слугує ідентифікатором транзакції, що використовується для пошуку транзакції в мережі. Ідентифікатор транзакції зазвичай називають підписом транзакції.
Вимоги до платника комісії:
- Повинен бути першим обліковим записом у повідомленні (індекс 0) і підписантом.
- Повинен бути обліковим записом, що належить System Program, або
nonce-обліковим записом (перевіряється
validate_fee_payer). - Повинен мати достатньо lamport для покриття
rent_exempt_minimum + total_fee; інакше транзакція завершується з помилкоюInsufficientFundsForFee.
Повідомлення
Поле message є
Message
структурою, що містить корисне навантаження транзакції:
header: Заголовок повідомленняaccount_keys: Масив адрес облікових записів, необхідних для інструкцій транзакціїrecent_blockhash: Хеш блоку, що слугує міткою часу для транзакціїinstructions: Масив інструкцій
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>,}
Заголовок
Поле header є
MessageHeader
структурою з трьома полями u8, що розподіляють масив account_keys на групи
за дозволами:
num_required_signatures: Загальна кількість підписів, необхідних для транзакції.num_readonly_signed_accounts: Кількість підписаних облікових записів, доступних лише для читання.num_readonly_unsigned_accounts: Кількість непідписаних облікових записів, доступних лише для читання.
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,}
Діаграма, що показує три частини заголовка повідомлення
Адреси облікових записів
Поле
account_keys
є масивом відкритих ключів із компактним кодуванням. Кожен елемент ідентифікує
обліковий запис, що використовується принаймні однією з інструкцій транзакції.
Масив повинен містити кожен обліковий запис і відповідати такому суворому
порядку:
- Підписант + Запис
- Підписант + Лише читання
- Не-підписант + Запис
- Не-підписант + Лише читання
Такий суворий порядок дозволяє масиву account_keys у поєднанні з трьома
лічильниками у header повідомлення визначати права доступу для
кожного акаунту без збереження позначок метаданих для кожного акаунту окремо.
Лічильники заголовка розбивають масив на чотири групи прав доступу, наведені
вище.
Діаграма, що показує порядок масиву адрес акаунтів
Останній blockhash
Поле recent_blockhash — це 32-байтовий хеш, що виконує дві функції:
- Мітка часу: підтверджує, що транзакцію було створено нещодавно.
- Дедублікація: запобігає повторній обробці тієї самої транзакції.
Blockhash стає недійсним після 150 slot. Якщо blockhash більше не є дійсним на
момент надходження транзакції, вона відхиляється з помилкою
BlockhashNotFound, якщо це не дійсна
транзакція зі стійким nonce.
Метод RPC getLatestBlockhash дозволяє
отримати поточний blockhash і висоту останнього блоку, до якої blockhash буде
дійсним.
Інструкції
Поле
instructions
є компактно закодованим масивом структур
CompiledInstruction.
Кожна CompiledInstruction посилається на акаунти за індексом у масиві
account_keys, а не за повним публічним ключем. Вона містить:
program_id_index: Індекс у масивіaccount_keys, що вказує на програму для виклику.accounts: Масив індексів уaccount_keys, що визначає акаунти, які передаються до програми.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>,}
Компактний масив інструкцій
Двійковий формат транзакції
Транзакції серіалізуються з використанням схеми компактного кодування. Усі масиви змінної довжини (підписи, ключі акаунтів, інструкції) мають префікс у вигляді compact-u16 кодування довжини. Цей формат використовує 1 байт для значень 0–127 та 2–3 байти для більших значень.
Формат застарілої транзакції (у мережі):
| Поле | Розмір | Опис |
|---|---|---|
num_signatures | 1-3 байти (compact-u16) | Кількість підписів |
signatures | num_signatures x 64 байти | Підписи Ed25519 |
num_required_signatures | 1 байт | Поле MessageHeader 1 |
num_readonly_signed | 1 байт | Поле MessageHeader 2 |
num_readonly_unsigned | 1 байт | Поле MessageHeader 3 |
num_account_keys | 1-3 байти (compact-u16) | Кількість статичних ключів облікових записів |
account_keys | num_account_keys x 32 байти | Публічні ключі |
recent_blockhash | 32 байти | Blockhash |
num_instructions | 1-3 байти (compact-u16) | Кількість інструкцій |
instructions | змінний | Масив скомпільованих інструкцій |
Кожна скомпільована інструкція серіалізується як:
| Поле | Розмір | Опис |
|---|---|---|
program_id_index | 1 байт | Індекс у масиві ключів облікових записів |
num_accounts | 1-3 байти (compact-u16) | Кількість індексів облікових записів |
account_indices | num_accounts x 1 байт | Індекси ключів облікових записів |
data_len | 1-3 байти (compact-u16) | Довжина instruction data |
data | data_len байтів | Непрозорі instruction data |
Розрахунок розміру
За умови PACKET_DATA_SIZE = 1 232 байти, доступний простір можна
розрахувати:
Total = 1232 bytes- compact-u16(num_sigs) # 1 byte- num_sigs * 64 # signature bytes- 3 # message header- compact-u16(num_keys) # 1 byte- num_keys * 32 # account key bytes- 32 # recent blockhash- compact-u16(num_ixs) # 1 byte- sum(instruction_sizes) # per-instruction overhead + data
Приклад: транзакція переказу SOL
Діаграма нижче показує, як транзакції та інструкції працюють разом, дозволяючи користувачам взаємодіяти з мережею. У цьому прикладі SOL переказується з одного облікового запису на інший.
Метадані рахунку відправника вказують, що він повинен підписати транзакцію. Це дозволяє System Program знімати lamport. Обидва рахунки — відправника та отримувача — мають бути доступні для запису, щоб їхній баланс у lamport міг змінюватися. Для виконання цієї інструкції гаманець відправника надсилає транзакцію, що містить його підпис і повідомлення з інструкцією переказу SOL.
Діаграма переказу SOL
Після надсилання транзакції System Program обробляє інструкцію переказу та оновлює баланс у lamport обох рахунків.
Діаграма процесу переказу SOL
Перевірте отримувача перед надсиланням SOL
Переказ через System Program додає lamport на будь-який рахунок. На рівні протоколу не існує перевірки того, чи може отримувач вивести SOL назад. Lamport можуть бути виведені лише програмою-власником рахунку, тому надсилання SOL на монетний двір токена, програму або PDA, яким ви не керуєте, загрожує безповоротною втратою коштів — повернути їх може лише повноваження, визначене програмою-власником. SOL, надісланий на token account, може бути повернутий лише власником цього рахунку, але не відправником.
Перекази SPL токенів частково самозахищені: Token Program відхиляє переказ, якщо рахунки не відповідають очікуваному монетному двору. Перекази нативного SOL не мають такого захисту, тому відправник повинен перевірити отримувача перед підписанням. Дивіться Перевірка адреси для повної логіки класифікації.
Наведений нижче приклад показує код, що стосується діаграм вище. Дивіться
функцію transfer
у System Program.
import { createClient, generateKeyPairSigner, lamports } from "@solana/kit";import { solanaRpc, rpcAirdrop } from "@solana/kit-plugin-rpc";import { generatedPayer, airdropPayer } from "@solana/kit-plugin-signer";import { systemProgram } from "@solana-program/system";const client = await createClient().use(generatedPayer()).use(solanaRpc({rpcUrl: "http://localhost:8899",rpcSubscriptionsUrl: "ws://localhost:8900"})).use(rpcAirdrop()).use(airdropPayer(lamports(1_000_000_000n))).use(systemProgram());const sender = client.payer;const recipient = await generateKeyPairSigner();const LAMPORTS_PER_SOL = 1_000_000_000n;const transferAmount = lamports(LAMPORTS_PER_SOL / 100n); // 0.01 SOL// Check balance before transferconst { value: preBalance1 } = await client.rpc.getBalance(sender.address).send();const { value: preBalance2 } = await client.rpc.getBalance(recipient.address).send();// Create a transfer instruction for transferring SOL from sender to recipientconst transferInstruction = client.system.instructions.transferSol({source: sender,destination: recipient.address,amount: transferAmount // 0.01 SOL in lamports});const transactionSignature = await client.sendTransaction([transferInstruction]);// Check balance after transferconst { value: postBalance1 } = await client.rpc.getBalance(sender.address).send();const { value: postBalance2 } = await client.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.context.signature);
Наступний приклад показує структуру транзакції, що містить одну інструкцію переказу SOL.
import {createClient,generateKeyPairSigner,lamports,createTransactionMessage,setTransactionMessageFeePayerSigner,setTransactionMessageLifetimeUsingBlockhash,appendTransactionMessageInstructions,pipe,signTransactionMessageWithSigners,getCompiledTransactionMessageDecoder} from "@solana/kit";import { solanaRpc, rpcAirdrop } from "@solana/kit-plugin-rpc";import { generatedPayer, airdropPayer } from "@solana/kit-plugin-signer";import { systemProgram } from "@solana-program/system";const client = await createClient().use(generatedPayer()).use(solanaRpc({rpcUrl: "http://localhost:8899",rpcSubscriptionsUrl: "ws://localhost:8900"})).use(rpcAirdrop()).use(airdropPayer(lamports(1_000_000_000n))).use(systemProgram());const { value: latestBlockhash } = await client.rpc.getLatestBlockhash().send();const sender = client.payer;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 = client.system.instructions.transferSol({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}}]}
Перевірте отримувача перед переказом
Оскільки переказ SOL успішно виконується на будь-який рахунок, перевірте отримувача перед підписанням. Отримайте інформацію про рахунок і надсилайте кошти лише на гаманець System Program (або на нефінансований адрес на кривій); відхиляйте монети, token account, програми та PDA, якими ви не керуєте.
import {type Address,createSolanaRpc,fetchJsonParsedAccount,isOffCurveAddress} from "@solana/kit";const rpc = createSolanaRpc("https://api.mainnet-beta.solana.com");const SYSTEM_PROGRAM = "11111111111111111111111111111111" as Address;/*** Throws if `recipient` cannot safely receive native SOL.** Only System Program wallets (or unfunded on-curve addresses) are safe. Any* other account locks the lamports because no authority can debit them.*/async function assertSafeSolRecipient(recipient: Address): Promise<void> {const account = await fetchJsonParsedAccount(rpc, recipient);if (!account.exists) {// Off-curve = a PDA with no account; reject conservatively.if (isOffCurveAddress(recipient)) {throw new Error("Recipient is a PDA with no account; SOL would be locked");}// On-curve = an unfunded wallet, safe to fund.return;}if (account.programAddress !== SYSTEM_PROGRAM) {throw new Error(`Recipient is owned by ${account.programAddress}, not a wallet; SOL would be locked`);}}// A wallet: safe.await assertSafeSolRecipient("H8sMJSCQxfKiFTCfDR3DUMLPwcRbM61LGFJ8N4dK3WjS" as Address);// The USDC mint: rejected before any SOL leaves the sender.await assertSafeSolRecipient("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" as Address);
Цей фрагмент перевіряє отримувачів нативного SOL. Для повної класифікації, яка також охоплює відправлення токенів SPL (token account, ATA, Token-2022), див. Verify Address.
Отримання деталей транзакції
Після відправлення отримайте деталі транзакції за допомогою підпису транзакції та RPC-методу getTransaction.
Ви також можете знайти транзакцію за допомогою Solana Explorer.
{"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"}
Необроблена відповідь ідентифікує акаунти за індексом і зберігає внутрішні (CPI) інструкції у вигляді закодованих блоків. Щоб розпізнати їх як адреси та пройти повне дерево інструкцій, дивіться Інтроспекція транзакцій.
Is this page helpful?