Кратко
Solana поддерживает три формата транзакций: legacy, v0 и v1. v0 добавляет таблицы поиска адресов (ALT) для обращения к аккаунтам через однобайтовые индексы. v1 увеличивает ограничение размера до 4 096 байт, переносит ограничения ресурсов непосредственно в сообщение и убирает ALT.
Solana поддерживает три формата транзакций: legacy, v0 и v1. Каждый формат описан ниже по одному и тому же плану из трёх частей: как он располагает байты в сети, как обращается к аккаунтам и откуда берёт ограничения ресурсов.
Статус активации v1
Формат v1 активен в mainnet, devnet и testnet. Чтение транзакций — это критическое изменение: клиенты, которые не подключились к нему явно, завершаются ошибкой на транзакциях v1. Существующим приложениям следует ознакомиться с материалом «Подготовка к v1».
Сравнение форматов
| Ограничение | legacy | v0 | v1 |
|---|---|---|---|
| Максимальный размер транзакции | 1 232 байта | 1 232 байта | 4 096 байт |
| Адреса аккаунтов | ~32, ограничено размером | 64, через таблицы поиска | 64, встроенные (96 с SIMD-0596) |
| Таблицы поиска адресов | не поддерживаются | поддерживаются | не поддерживаются |
| Ограничения ресурсов | Инструкции ComputeBudget | Инструкции ComputeBudget | конфигурация сообщения |
Формат legacy
Исходный формат, по-прежнему используемый по умолчанию в большинстве инструментов. Он не
имеет префикса версии вообще: первый байт транзакции — это счётчик массива подписей в
формате compact-u16, а первый байт сообщения — num_required_signatures, у которого
старший бит всегда сброшен.
Структура legacy на уровне байтов
| Поле | Размер | Описание |
|---|---|---|
num_signatures | compact-u16 | Количество подписей |
signatures | num_signatures x 64 байта | Подписи Ed25519 |
header | 3 байта | MessageHeader — старший бит первого байта сброшен |
num_account_keys | compact-u16 | Количество ключей аккаунтов |
account_keys | num_account_keys x 32 байта | Публичные ключи, все встроенные |
recent_blockhash | 32 байта | Идентификатор времени жизни |
num_instructions | compact-u16 | Количество инструкций |
instructions | переменная | Каждая инструкция сериализована последовательно |
Каждый массив переменной длины предваряется длиной в формате compact-u16: 1 байт для значений 0–127, 2–3 байта для больших значений. Описание структуры отдельной инструкции и пример расчёта размера см. в разделе двоичный формат транзакций.
Аккаунты в legacy
Каждый аккаунт записывается в account_keys как полный 32-байтовый публичный ключ,
а инструкции ссылаются на него по однобайтовому индексу в этом массиве. Нет возможности
обратиться к аккаунту, который не указан явно в транзакции, — именно это ограничивает
транзакцию legacy примерно 32 аккаунтами до исчерпания лимита в 1 232 байта.
Ограничения ресурсов в legacy
Лимит вычислительных единиц, лимит размера данных загруженных аккаунтов, размер кучи и приоритетная комиссия задаются путём включения инструкций программы ComputeBudget в транзакцию. Каждая из них занимает один слот инструкции и 150 вычислительных единиц. Опускать их безопасно: среда выполнения использует значения по умолчанию — 200 000 CU на инструкцию (не более 1,4 млн), лимит размера данных 64 МиБ, куча 32 КиБ и приоритетная комиссия равна нулю.
Формат v0
v0 — это legacy-сообщение плюс два дополнения: байт-префикс версии 0x80 и массив
address_table_lookups, добавляемый после инструкций. Всё, что предшествует этим
добавлениям, побайтово идентично legacy.
Структура v0 на уровне байтов
| Поле | Размер | Описание |
|---|---|---|
num_signatures | compact-u16 | Количество подписей |
signatures | num_signatures x 64 байта | Подписи Ed25519 |
0x80 | 1 байт | Байт-префикс версии — первый байт сообщения |
header | 3 байта | MessageHeader (как в legacy) |
num_account_keys | compact-u16 | Количество статических ключей аккаунтов |
static_account_keys | num_account_keys x 32 байта | Ключи, которые явно присутствуют в транзакции |
recent_blockhash | 32 байта | Идентификатор времени жизни |
num_instructions | compact-u16 | Количество инструкций |
instructions | переменная | Такой же формат, как в legacy |
address_table_lookups | compact-u16 + переменный | Ссылки на ALT (см. ниже) |
Каждая запись в таблице поиска адресов содержит:
| Поле | Размер | Описание |
|---|---|---|
account_key | 32 байта | Публичный ключ ALT аккаунта |
writable_indexes | compact-u16 + N x 1 байт | Индексы в ALT для записей с правом записи |
readonly_indexes | compact-u16 + N x 1 байт | Индексы в ALT для записей только для чтения |
Таблицы поиска адресов
ALT — это ончейн-аккаунт, который хранит до 256 публичных ключей. Ссылаясь на ALT, транзакция может включать дополнительные аккаунты, используя 1-байтовые индексы вместо 32-байтовых публичных ключей, что значительно снижает накладные расходы на каждый аккаунт.
Во время выполнения, до начала исполнения, validator разрешает все ссылки на ALT в полные публичные ключи. Разрешённые адреса добавляются к статическим ключам аккаунтов для формирования полного списка ключей аккаунтов. Аккаунты, разрешённые через ALT, следуют тому же порядку, что и статические аккаунты: сначала идут записи с правом записи, затем только для чтения.
Таблицы поиска адресов влияют только на то, как аккаунты указываются в транзакции на проводе. Во время исполнения рантайм разрешает все индексы в полные адреса аккаунтов. Аккаунты, разрешённые через ALT, могут быть только с правом записи или только для чтения (не подписанты); они не могут быть подписантами.
Создание и использование таблицы поиска адресов
В этом примере создаётся таблица поиска, в неё добавляется адрес, а затем таблица используется для компиляции и отправки транзакции v0.
Из таблицы поиска можно загружать только аккаунты, не являющиеся подписантами. Плательщик комиссии и все обязательные подписанты остаются в статическом списке аккаунтов сообщения. Кроме того, адрес нельзя найти в том же slot, в котором он был добавлен, поэтому пример ждёт следующий подтверждённый slot, прежде чем компилировать сообщение v0.
Ограничения ресурсов в v0
Без изменений по сравнению с legacy: инструкции ComputeBudget с теми же значениями по умолчанию при их отсутствии.
Формат v1
v1 увеличивает ограничение размера до 4 096 байт и реструктурирует сообщение вокруг конфигурации транзакции: ограничения ресурсов выносятся из инструкций ComputeBudget в фиксированные поля самого сообщения. Это позволяет сети определять приоритет транзакции по комиссии с помощью одного чтения по фиксированному смещению, без необходимости сканировать и десериализовывать список инструкций.
Структура v1 на уровне байтов
| Поле | Размер | Описание |
|---|---|---|
0x81 | 1 байт | Байт-префикс версии — первый байт транзакции |
header | 3 байта | MessageHeader (как в legacy) |
config_mask | 4 байта | Битовая маска u32 LE, отмечающая присутствующие поля конфигурации |
recent_blockhash | 32 байта | Идентификатор времени жизни |
num_instructions | 1 байт | Счётчик фиксированной ширины, максимум 64 |
num_addresses | 1 байт | Счётчик фиксированной ширины, максимум 64 (96 с SIMD-0596) |
addresses | N x 32 байта | Адреса аккаунтов, все встроенные — без ссылок на таблицы поиска |
config_values | 0–20 байт | По одному значению на каждый установленный бит маски, в порядке битов (см. ниже) |
instruction_headers | N x 4 байта | Для каждой инструкции: program_id_index (u8), num_accounts (u8), data_len (u16 LE) |
instruction_payloads | переменная | Для каждой инструкции: индексы аккаунтов, затем instruction data |
signatures | N x 64 байта | В конце, без префикса длины — количество берётся из заголовка |
При написании декодера стоит обратить внимание на два структурных отличия от legacy и v0.
Счётчики представлены полями фиксированной ширины u8, а не compact-u16, а инструкции
разделены на два блока: сначала все заголовки фиксированного размера, затем все полезные
нагрузки переменной длины — вместо того чтобы каждая инструкция шла последовательно.
Аккаунты в v1: без таблиц поиска адресов
v1 намеренно убирает поддержку ALT. 64 необработанных адреса занимают 2 048 байт, а 96 адресов, разрешённых после SIMD-0596, — 3 072 байта; оба значения с запасом укладываются в лимит 4 096 байт, поэтому каждый адрес указывается встроенно, как и в legacy. Если ваше приложение зависит от таблиц поиска, переход на v1 означает встраивание этих адресов.
Ограничения ресурсов в v1: конфигурация транзакции
Конфигурация представляет собой битовую маску u32, за которой следуют значения
фиксированной ширины для каждого поля с установленным битом:
| Бит(ы) | Поле | Ширина | Примечания |
|---|---|---|---|
| 0–1 | Приоритетная комиссия | u64 | Итоговое количество lamport — оба бита устанавливаются вместе |
| 2 | Лимит вычислительных единиц | u32 | |
| 3 | Лимит размера данных загруженных аккаунтов | u32 | |
| 4 | Запрошенный размер кучи | u32 |
Неизвестные биты отклоняются. Поскольку сообщение подписано, нераспознанные поля конфигурации не могут быть молча проигнорированы.
Приоритетная комиссия — это итоговое количество lamport, а не цена
В legacy и v0 приоритетная комиссия задаётся через SetComputeUnitPrice как
микро-lamport за вычислительную единицу, умноженные на лимит вычислительных единиц. В
v1 это абсолютная итоговая сумма в lamport — без умножения и округления.
Не переносите арифметику на основе CU. Итоговая формула комиссии в остальном не изменилась:
(signatures × lamports_per_signature) + priority_fee.
Инструкции ComputeBudget в v1 являются no-op
Транзакция v1 не отклоняет инструкции ComputeBudget — она игнорирует их для целей конфигурации. Они по-прежнему выполняются как успешные no-op, потребляя 150 вычислительных единиц и один из 64 слотов инструкций, но не влияя на бюджет. Удаляйте их при формировании транзакций v1 и не сканируйте их при чтении транзакций v1: значения хранятся в конфигурации сообщения.
Поля конфигурации должны задаваться явно
Наиболее важное поведенческое изменение для отправителей: в отличие от legacy и v0, транзакции v1 должны явно задавать лимит вычислительных единиц и лимит размера данных загруженных аккаунтов, иначе транзакция завершится с ошибкой.
| Незаданное поле | legacy / v0 | v1 | Симптом при отсутствии |
|---|---|---|---|
| Лимит вычислительных единиц | 200k на инструкцию, максимум 1,4M | 0 CU | Немедленный сбой, бюджет исчерпан |
| Размер данных загруженных аккаунтов | 64 МиБ | 0 байт | MaxLoadedAccountsDataSizeExceeded при загрузке первого аккаунта |
| Приоритетная комиссия | 0 | 0 | — |
| Размер кучи | 32 КиБ | 32 КиБ | — |
Рекомендуемый подход — выполнить одну симуляцию с максимальными значениями обоих лимитов,
затем записать возвращённые unitsConsumed и loadedAccountsDataSize обратно в
конфигурацию, округлив размер данных вверх до следующей страницы по 32 КиБ для запаса
(модель стоимости блока тарифицирует страницами по 32 КиБ, поэтому запас ниже границы
следующей страницы бесплатен).
Создание транзакции v1
Сообщение v1 собирается через тот же конвейер, что и сообщение legacy или v0. Отличие — в
setTransactionMessageConfig: ограничения ресурсов являются свойством сообщения, поэтому список
инструкций содержит только инструкции программ.
import {appendTransactionMessageInstruction,assertIsTransactionWithBlockhashLifetime,createClient,createTransactionMessage,generateKeyPairSigner,getSignatureFromTransaction,lamports,pipe,sendAndConfirmTransactionFactory,setTransactionMessageConfig,setTransactionMessageFeePayerSigner,setTransactionMessageLifetimeUsingBlockhash,signTransactionMessageWithSigners,} from "@solana/kit";import { rpcAirdrop, solanaRpc } from "@solana/kit-plugin-rpc";import { airdropPayer, payer } from "@solana/kit-plugin-signer";import { getTransferSolInstruction } from "@solana-program/system";const sender = await generateKeyPairSigner();const recipient = await generateKeyPairSigner();const client = await createClient().use(payer(sender)).use(solanaRpc({rpcUrl: "http://localhost:8899",rpcSubscriptionsUrl: "ws://localhost:8900",}),).use(rpcAirdrop()).use(airdropPayer(lamports(1_000_000_000n)));const { value: latestBlockhash } = await client.rpc.getLatestBlockhash().send();// A v1 message is assembled through the same pipeline as a legacy or v0 one.// The difference is the config: a v1 transaction carries its resource limits in// the message itself, so no ComputeBudget instructions are compiled in.const transactionMessage = pipe(createTransactionMessage({ version: 1 }),(m) => setTransactionMessageFeePayerSigner(sender, m),(m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),(m) =>appendTransactionMessageInstruction(getTransferSolInstruction({source: sender,destination: recipient.address,amount: lamports(10_000_000n),}),m,),// The compute unit limit and loaded accounts data size limit both default to// zero on v1, so a transaction that leaves them unset always fails. The// priority fee is an absolute total in lamports, not a price per compute// unit.(m) =>setTransactionMessageConfig({computeUnitLimit: 20_000,loadedAccountsDataSizeLimit: 64 * 1024,priorityFeeLamports: 5_000n,},m,),);const signedTransaction =await signTransactionMessageWithSigners(transactionMessage);assertIsTransactionWithBlockhashLifetime(signedTransaction);await sendAndConfirmTransactionFactory({rpc: client.rpc,rpcSubscriptions: client.rpcSubscriptions,})(signedTransaction, { commitment: "confirmed" });const transactionSignature = getSignatureFromTransaction(signedTransaction);console.log("Transaction Signature:", transactionSignature);
Kit также предоставляет отдельный сеттер для каждого поля конфигурации —
setTransactionMessagePriorityFeeLamports,
setTransactionMessageComputeUnitLimit,
setTransactionMessageLoadedAccountsDataSizeLimit и
setTransactionMessageHeapSize — чтобы задавать их по одному.
Подбор лимитов с помощью симуляции
Вместо того чтобы гадать, какими должны быть два обязательных лимита, измерьте их. Симуляция выполняется с обоими лимитами, поднятыми до максимума среды выполнения, поэтому она не может завершиться сбоем из-за нехватки ресурсов, которые она измеряет. Возвращаемое значение — это точная стоимость одного симулированного запуска, поэтому добавьте запас, прежде чем на него полагаться.
import {appendTransactionMessageInstruction,assertIsTransactionWithBlockhashLifetime,createClient,createTransactionMessage,estimateAndSetResourceLimitsFactory,estimateResourceLimitsFactory,fillTransactionMessageProvisoryResourceLimits,generateKeyPairSigner,getSignatureFromTransaction,lamports,pipe,sendAndConfirmTransactionFactory,setTransactionMessageFeePayerSigner,setTransactionMessageLifetimeUsingBlockhash,setTransactionMessagePriorityFeeLamports,signTransactionMessageWithSigners,} from "@solana/kit";import { rpcAirdrop, solanaRpc } from "@solana/kit-plugin-rpc";import { airdropPayer, payer } from "@solana/kit-plugin-signer";import { getTransferSolInstruction } from "@solana-program/system";const sender = await generateKeyPairSigner();const recipient = await generateKeyPairSigner();const client = await createClient().use(payer(sender)).use(solanaRpc({rpcUrl: "http://localhost:8899",rpcSubscriptionsUrl: "ws://localhost:8900",}),).use(rpcAirdrop()).use(airdropPayer(lamports(1_000_000_000n)));const { value: latestBlockhash } = await client.rpc.getLatestBlockhash().send();// The priority fee is a pricing decision that simulation cannot measure, so it// is set directly. The two resource limits are measured below instead of// guessed. The provisory fill writes placeholder limits so the message// simulates at the same size it will be sent at.const draftMessage = pipe(createTransactionMessage({ version: 1 }),(m) => setTransactionMessageFeePayerSigner(sender, m),(m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),(m) =>appendTransactionMessageInstruction(getTransferSolInstruction({source: sender,destination: recipient.address,amount: lamports(10_000_000n),}),m,),(m) => setTransactionMessagePriorityFeeLamports(5_000n, m),fillTransactionMessageProvisoryResourceLimits,);// Simulation runs with both limits raised to the runtime maximum, so it cannot// fail for want of the resources it is measuring.const estimateAndSetResourceLimits = estimateAndSetResourceLimitsFactory(estimateResourceLimitsFactory({ rpc: client.rpc }),);const transactionMessage = await estimateAndSetResourceLimits(draftMessage, {commitment: "confirmed",});console.log("Compute unit limit:", transactionMessage.config?.computeUnitLimit);console.log("Loaded accounts data size limit:",transactionMessage.config?.loadedAccountsDataSizeLimit,);const signedTransaction =await signTransactionMessageWithSigners(transactionMessage);assertIsTransactionWithBlockhashLifetime(signedTransaction);await sendAndConfirmTransactionFactory({rpc: client.rpc,rpcSubscriptions: client.rpcSubscriptions,})(signedTransaction, { commitment: "confirmed" });// The estimate is the exact cost of one simulated run, with nothing to spare.// Add a margin before relying on it in production.console.log("Transaction Signature:",getSignatureFromTransaction(signedTransaction),);
Подготовка к v1
v1 меняет не только отправку, но и чтение транзакций. Любой клиент, который вызывает
getTransaction или getBlock без явного подключения, завершается ошибкой на транзакциях v1:
- Передавайте
maxSupportedTransactionVersion: 1— JSON целое число1, а не строку"1"— вgetTransactionиgetBlock. Передача0даёт сбой на транзакциях v1 точно так же, как и отсутствие параметра, поэтому кодовая база, обновлённая во время перехода на v0, всё равно требует изменения этого значения. getTransactionзавершается ошибкой-32015на транзакции v1, а одна транзакция v1 приводит к сбою всего ответаgetBlockс той же ошибкой — частичный результат не возвращается.blockSubscribeвозвращаетblock: nullи прекращает продвижение, поэтому потребитель, интерпретирующий это как пустой блок, молча начинает отставать начиная с первого slot v1.getSignaturesForAddressникогда не анализирует тела транзакций, поэтому подписи v1 отображаются в обычном режиме.- Ответы при явном подключении содержат объект
transactionConfigв сообщении для транзакций v1 (для legacy и v0 он полностью отсутствует). Конвейеры, которые определяют приоритетные комиссии или лимиты вычислений путём поиска инструкций ComputeBudget, молча сообщают ноль для каждой транзакции v1. - Используйте
encoding: 'base64'при декодировании транзакций на стороне клиента, а также дляsendTransaction/simulateTransactionс транзакциями размером более 1 232 байт — кодировка base58 по-прежнему ограничена старым размером. - Поддержка в клиентских библиотеках требует актуальных версий:
@solana/kit8.0+, Rust-крейты поколения Agave 4.2.x или web3.js v3. web3.js v1 читает v1 начиная с версии 1.99.0, но не может формировать и отправлять такие транзакции.
Чтение транзакции v1
Для явного подключения передаётся целое число JSON 1. Декодирование возвращённых байтов даёт
версию и конфигурацию, в которой и находятся ограничения ресурсов — поиск инструкций ComputeBudget
в списке инструкций ничего не находит.
import {decompileTransactionMessage,getBase64Encoder,getCompiledTransactionMessageDecoder,getTransactionDecoder,} from "@solana/kit";// Reading a v1 transaction requires opting in. The integer 1 is mandatory:// omitting the parameter, or passing 0, fails on a v1 transaction rather than// returning it in a degraded form.const fetched = await client.rpc.getTransaction(transactionSignature, {commitment: "confirmed",encoding: "base64",maxSupportedTransactionVersion: 1,}).send();if (fetched === null) {throw new Error("the transaction just sent was not found");}// The compiled message stores the config as a bitmask plus a positional list,// so it is decompiled to address the fields by name. Decompiling fetches no// accounts, because v1 does not support address lookup tables.const wireTransaction = getBase64Encoder().encode(fetched.transaction[0]);const transaction = getTransactionDecoder().decode(wireTransaction);const compiledMessage = getCompiledTransactionMessageDecoder().decode(transaction.messageBytes,);const message = decompileTransactionMessage(compiledMessage);console.log("Version:", message.version);if ("config" in message) {// Scanning the instruction list for ComputeBudget instructions finds nothing// here — on v1 the limits live in the config.console.log("Compute unit limit:", message.config?.computeUnitLimit);console.log("Priority fee (lamports):", message.config?.priorityFeeLamports);}
Полное руководство по миграции — включая поддержку клиентских библиотек, определение версии при потоковой передаче (Geyser/gRPC) и поведение симуляции — см. на странице обновления до формата транзакций v1. Примеры, которые можно запустить, на TypeScript, Rust, Python и Go находятся в solana-foundation/transaction-v1-examples.
Is this page helpful?