Podsumowanie
Solana obsługuje trzy formaty transakcji: legacy, v0 i v1. v0 dodaje tablice wyszukiwania adresów (ALT) umożliwiające odwoływanie się do kont za pomocą 1-bajtowych indeksów. v1 zwiększa limit rozmiaru do 4 096 bajtów, przenosi limity zasobów do samej wiadomości i usuwa ALT.
Solana obsługuje trzy formaty transakcji: legacy, v0 i v1. Każdy z nich opisano poniżej w tych samych trzech częściach: układ bajtów w sieci, sposób odwoływania się do kont oraz źródło limitów zasobów.
Status aktywacji v1
Format v1 jest aktywny w sieciach mainnet, devnet i testnet. Odczyt transakcji to zmiana łamiąca zgodność: klienty, które nie włączą jego obsługi, zwracają błąd dla transakcji v1. Istniejące aplikacje powinny zapoznać się z sekcją przygotowanie do v1.
Porównanie formatów
| Limit | legacy | v0 | v1 |
|---|---|---|---|
| Maksymalny rozmiar transakcji | 1 232 bajty | 1 232 bajty | 4 096 bajtów |
| Adresy kont | ~32, ograniczone rozmiarem | 64, przez tablice wyszukiwania | 64, bezpośrednio (96 z SIMD-0596) |
| Tablice wyszukiwania adresów | nieobsługiwane | obsługiwane | nieobsługiwane |
| Limity zasobów | Instrukcje ComputeBudget | Instrukcje ComputeBudget | konfiguracja wiadomości |
Format legacy
Oryginalny format, wciąż domyślny w większości narzędzi. Nie ma żadnego prefiksu
wersji: pierwszy bajt transakcji to liczba podpisów zakodowana jako compact-u16,
a pierwszy bajt wiadomości to num_required_signatures, którego najwyższy bit
jest zawsze wyzerowany.
Układ bajtów legacy
| Pole | Rozmiar | Opis |
|---|---|---|
num_signatures | compact-u16 | Liczba podpisów |
signatures | num_signatures x 64 bajty | Podpisy Ed25519 |
header | 3 bajty | MessageHeader — najwyższy bit pierwszego bajtu jest wyzerowany |
num_account_keys | compact-u16 | Liczba kluczy kont |
account_keys | num_account_keys x 32 bajty | Klucze publiczne, wszystkie bezpośrednio |
recent_blockhash | 32 bajty | Specyfikator czasu życia |
num_instructions | compact-u16 | Liczba instrukcji |
instructions | zmienna | Każda instrukcja serializowana ciągiem |
Każda tablica o zmiennej długości poprzedzona jest długością zakodowaną jako compact-u16: 1 bajt dla wartości 0–127, 2–3 bajty dla większych wartości. Układ poszczególnych instrukcji i przykładowe obliczenie rozmiaru znajdziesz w binarnym formacie transakcji.
Konta w formacie legacy
Każde konto zapisywane jest jako pełny 32-bajtowy klucz publiczny w account_keys,
a instrukcje odwołują się do nich za pomocą 1-bajtowego indeksu do tej tablicy.
Nie ma możliwości odwołania się do konta, które nie jest jawnie wymienione
w transakcji — to właśnie ogranicza transakcję legacy do około 32 kont,
zanim wyczerpie limit 1 232 bajtów.
Limity zasobów w formacie legacy
Limit jednostek obliczeniowych, limit rozmiaru danych załadowanych kont, rozmiar sterty i opłata priorytetowa są ustawiane przez dołączenie instrukcji programu ComputeBudget do transakcji. Każda z nich zużywa jeden slot instrukcji i 150 jednostek obliczeniowych. Ich pominięcie jest bezpieczne: środowisko wykonawcze przyjmuje wartości domyślne: 200 000 CU na instrukcję (max 1,4 M), limit danych 64 MiB, stertę 32 KiB i opłatę priorytetową równą zero.
Format V0
v0 to wiadomość legacy z dwoma dodatkami: bajtem prefiksu wersji 0x80 oraz
tablicą address_table_lookups dołączoną po instrukcjach. Wszystko przed tymi
dodatkami jest identyczne bajt po bajcie z formatem legacy.
Układ bajtów V0
| Pole | Rozmiar | Opis |
|---|---|---|
num_signatures | compact-u16 | Liczba podpisów |
signatures | num_signatures x 64 bajty | Podpisy Ed25519 |
0x80 | 1 bajt | Bajt prefiksu wersji — pierwszy bajt wiadomości |
header | 3 bajty | MessageHeader (jak w legacy) |
num_account_keys | compact-u16 | Liczba statycznych kluczy kont |
static_account_keys | num_account_keys x 32 bajty | Klucze występujące dosłownie w transakcji |
recent_blockhash | 32 bajty | Specyfikator czasu życia |
num_instructions | compact-u16 | Liczba instrukcji |
instructions | zmienna | Ten sam format co legacy |
address_table_lookups | compact-u16 + zmienna | Odwołania ALT (patrz poniżej) |
Każdy wpis w tabeli wyszukiwania adresów zawiera:
| Pole | Rozmiar | Opis |
|---|---|---|
account_key | 32 bajty | Klucz publiczny konta ALT |
writable_indexes | compact-u16 + N x 1 bajt | Indeksy do ALT dla kont z prawem zapisu |
readonly_indexes | compact-u16 + N x 1 bajt | Indeksy do ALT dla kont tylko do odczytu |
Tablice wyszukiwania adresów
ALT to konto onchain, które przechowuje do 256 kluczy publicznych. Odwołując się do ALT, transakcja może zawierać dodatkowe konta przy użyciu 1-bajtowych indeksów zamiast 32-bajtowych kluczy publicznych, znacząco redukując narzut na konto.
W czasie wykonywania, zanim rozpocznie się egzekucja, validator rozwiązuje wszystkie odwołania do ALT na pełne klucze publiczne. Rozwiązane adresy są dołączane do statycznych kluczy kont, tworząc pełną listę kluczy kont. Konta rozwiązane przez ALT są uporządkowane tak samo jak konta statyczne: najpierw zapisywalne, potem tylko do odczytu.
Tabele wyszukiwania adresów wpływają jedynie na sposób odwoływania się do kont w transakcji przesyłanej w sieci. Podczas wykonywania, środowisko uruchomieniowe rozwiązuje wszystkie indeksy na pełne adresy kont. Konta rozwiązane przez ALT mogą być tylko zapisywalne lub tylko do odczytu (nie mogą być sygnatariuszami).
Tworzenie i używanie tablicy wyszukiwania adresów
Ten przykład tworzy tablicę wyszukiwania, dodaje do niej adres, a następnie używa jej do skompilowania i wysłania transakcji v0.
Z tablicy wyszukiwania można ładować tylko konta niebędące sygnatariuszami. Płatnik opłaty oraz każdy wymagany sygnatariusz pozostają na statycznej liście kont wiadomości. Adresu nie można też wyszukać w tej samej jednostce czasu (slot), w której został dodany, dlatego przykład czeka na kolejny potwierdzony slot przed skompilowaniem wiadomości v0.
Limity zasobów w v0
Bez zmian względem legacy: instrukcje ComputeBudget, z tymi samymi wartościami domyślnymi przy ich pominięciu.
Format V1
v1 zwiększa limit rozmiaru do 4 096 bajtów i restrukturyzuje wiadomość wokół konfiguracji transakcji: limity zasobów są przenoszone z instrukcji ComputeBudget do pól o stałej pozycji w samej wiadomości. Dzięki temu sieć może uszeregować transakcję według opłaty priorytetowej za pomocą jednego odczytu o stałym przesunięciu, zamiast skanować i deserializować listę instrukcji.
Układ bajtów V1
| Pole | Rozmiar | Opis |
|---|---|---|
0x81 | 1 bajt | Bajt prefiksu wersji — pierwszy bajt transakcji |
header | 3 bajty | MessageHeader (jak w legacy) |
config_mask | 4 bajty | Maska bitowa u32 LE wskazująca, które wartości konfiguracji są obecne |
recent_blockhash | 32 bajty | Specyfikator czasu życia |
num_instructions | 1 bajt | Licznik o stałej szerokości, maks. 64 |
num_addresses | 1 bajt | Licznik o stałej szerokości, maks. 64 (96 z SIMD-0596) |
addresses | N x 32 bajty | Adresy kont, wszystkie bezpośrednio — brak odwołań do tablic wyszukiwania |
config_values | 0–20 bajtów | Jedna wartość na każdy ustawiony bit maski, w kolejności bitów (patrz niżej) |
instruction_headers | N x 4 bajty | Na instrukcję: program_id_index (u8), num_accounts (u8), data_len (u16 LE) |
instruction_payloads | zmienna | Na instrukcję: indeksy kont, następnie instruction data |
signatures | N x 64 bajty | Na końcu, bez prefiksu długości — liczba podpisów pochodzi z nagłówka |
Przy pisaniu dekodera warto zwrócić uwagę na dwie różnice strukturalne względem
legacy i v0. Liczniki są polami u8 o stałej szerokości zamiast compact-u16,
a instrukcje są podzielone na dwa ciągłe bloki: najpierw wszystkie nagłówki
o stałym rozmiarze, a następnie wszystkie ładunki o zmiennej długości, zamiast
kolejnych instrukcji serializowanych ciągiem.
Konta w v1: brak tablic wyszukiwania adresów
v1 celowo usuwa obsługę ALT. 64 surowe adresy to 2048 bajtów, a 96 adresów dozwolonych po SIMD-0596 to 3072 bajty — oba warianty mieszczą się z zapasem w limicie 4096 bajtów, więc każdy adres jest zapisany bezpośrednio, tak jak w formacie legacy. Jeśli Twoja aplikacja korzysta z tablic wyszukiwania, przejście na v1 oznacza umieszczenie tych adresów bezpośrednio w wiadomości.
Limity zasobów w v1: konfiguracja transakcji
Konfiguracja to maska bitowa u32 po której następują wartości o stałej
szerokości dla każdego pola, którego bit jest ustawiony:
| Bit(y) | Pole | Szerokość | Uwagi |
|---|---|---|---|
| 0–1 | Opłata priorytetowa | u64 | Łączna liczba lamport — oba bity ustawiane razem |
| 2 | Limit jednostek obliczeniowych | u32 | |
| 3 | Limit rozmiaru danych załadowanych kont | u32 | |
| 4 | Żądany rozmiar sterty | u32 |
Nieznane bity są odrzucane. Ponieważ wiadomość jest podpisana, nierozpoznanych pól konfiguracji nie można cicho pomijać.
Opłata priorytetowa to łączna liczba lamport, nie cena
W formatach legacy i v0 opłata priorytetowa jest ustawiana przez SetComputeUnitPrice
jako mikro-lamporty na jednostkę obliczeniową, pomnożone przez limit jednostek
obliczeniowych. W v1 jest to bezwzględna suma w lamportach — bez mnożenia,
bez zaokrąglania. Nie przenoś arytmetyki per-CU. Całkowity wzór na opłatę pozostaje
niezmieniony: (signatures × lamports_per_signature) + priority_fee.
Instrukcje ComputeBudget są no-opami w v1
Transakcja v1 nie odrzuca instrukcji ComputeBudget — ignoruje je na potrzeby konfiguracji. Nadal są wykonywane jako udane no-opy, zużywając 150 jednostek obliczeniowych i jeden z 64 slotów instrukcji, nie mając żadnego wpływu na budżet. Usuń je podczas budowania transakcji v1 i przestań ich szukać przy odczytywaniu transakcji v1: wartości znajdują się w konfiguracji wiadomości.
Pola konfiguracji muszą być ustawiane jawnie
Najważniejsza zmiana behawioralna dla nadawców: w odróżnieniu od legacy i v0, transakcje v1 muszą jawnie ustawiać limit jednostek obliczeniowych oraz limit rozmiaru danych załadowanych kont — w przeciwnym razie transakcja zakończy się niepowodzeniem.
| Nieustawione pole | legacy / v0 | v1 | Objaw przy pominięciu |
|---|---|---|---|
| Limit jednostek obliczeniowych | 200k na instrukcję, maks. 1,4M | 0 CU | Natychmiastowe niepowodzenie — brak budżetu |
| Rozmiar danych załadowanych kont | 64 MiB | 0 bajtów | MaxLoadedAccountsDataSizeExceeded przy pierwszym załadowanym koncie |
| Opłata priorytetowa | 0 | 0 | — |
| Rozmiar sterty | 32 KiB | 32 KiB | — |
Zalecane podejście to jednorazowa symulacja z oboma limitami ustawionymi na
maksimum, a następnie zapisanie zwróconych wartości unitsConsumed i
loadedAccountsDataSize z powrotem do konfiguracji, zaokrąglając rozmiar danych
w górę do kolejnej strony 32 KiB dla zapasu (model kosztów bloków nalicza opłaty
w stronach 32 KiB, więc zapas poniżej granicy następnej strony jest bezpłatny).
Budowanie transakcji v1
Wiadomość v1 jest składana w tym samym potoku co wiadomość legacy lub v0.
Różnica polega na setTransactionMessageConfig: limity zasobów są właściwością
wiadomości, więc lista instrukcji zawiera wyłącznie instrukcje programu.
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 udostępnia też osobny setter dla każdego pola konfiguracji —
setTransactionMessagePriorityFeeLamports,
setTransactionMessageComputeUnitLimit,
setTransactionMessageLoadedAccountsDataSizeLimit oraz
setTransactionMessageHeapSize — do ustawiania ich pojedynczo.
Dobieranie limitów za pomocą symulacji
Zamiast zgadywać dwa obowiązkowe limity, zmierz je. Symulacja jest uruchamiana z obydwoma limitami podniesionymi do maksimum środowiska uruchomieniowego, więc nie może zakończyć się błędem z powodu braku zasobów, które mierzy. Zwrócona wartość to dokładny koszt jednego symulowanego uruchomienia, dlatego przed poleganiem na niej dodaj margines.
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),);
Przygotowanie do v1
v1 zmienia odczyt transakcji, a nie tylko ich wysyłanie. Każdy klient, który
wywołuje getTransaction lub getBlock bez włączenia obsługi, zwraca błąd dla transakcji v1:
- Przekaż
maxSupportedTransactionVersion: 1— liczbę całkowitą JSON1, nie ciąg"1"— dogetTransactionigetBlock. Przekazanie0powoduje błędy dla transakcji v1 tak samo jak pominięcie parametru, więc baza kodu zaktualizowana podczas wdrożenia v0 nadal wymaga zmiany tej wartości. getTransactionzwraca błąd-32015dla transakcji v1, a jedna transakcja v1 powoduje niepowodzenie całej odpowiedzigetBlockz tym samym błędem — nie ma wyniku częściowego.blockSubscribeemitujeblock: nulli przestaje się przesuwać, więc konsument odczytujący to jako pusty blok będzie cicho zostawać w tyle od pierwszego slot z v1 wzwyż.getSignaturesForAddressnigdy nie sprawdza treści transakcji, więc podpisy v1 są wyświetlane normalnie.- Odpowiedzi z włączoną obsługą zawierają w wiadomości obiekt
transactionConfigdla transakcji v1 (w ogóle nie występuje dla legacy i v0). Potoki, które wyznaczają opłaty priorytetowe lub limity obliczeniowe, wyszukując instrukcje ComputeBudget, po cichu raportują zero dla każdej transakcji v1. - Używaj
encoding: 'base64'podczas dekodowania transakcji po stronie klienta oraz dlasendTransaction/simulateTransactionz transakcjami powyżej 1 232 bajtów — kodowanie base58 nadal jest ograniczone do starego rozmiaru. - Obsługa w bibliotekach klienckich wymaga najnowszych wersji:
@solana/kit8.0+, skrzynki Rust z generacji Agave 4.2.x lub web3.js v3. web3.js v1 odczytuje v1 od wersji 1.99.0 wzwyż, ale nie może go budować ani wysyłać.
Odczytywanie transakcji v1
Włączenie obsługi wymaga liczby całkowitej JSON 1. Zdekodowanie zwróconych bajtów daje
wersję i konfigurację, w której znajdują się limity zasobów — przeszukiwanie
listy instrukcji pod kątem instrukcji ComputeBudget niczego nie znajduje.
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);}
Pełny przewodnik migracji — w tym obsługę bibliotek klienckich, wykrywanie wersji w strumieniach (Geyser/gRPC) oraz zachowanie symulacji — znajdziesz na stronie aktualizacji do formatu transakcji v1. Działające przykłady w TypeScript, Rust, Python i Go są dostępne w solana-foundation/transaction-v1-examples.
Is this page helpful?