Samenvatting
Solana heeft drie transactieformaten: legacy, v0 en v1. v0 voegt Address Lookup Tables (ALTs) toe voor het verwijzen naar accounts via 1-byte-indices. v1 verhoogt de groottelimiet naar 4.096 bytes, verplaatst resourcelimieten naar het bericht zelf en verwijdert ALTs.
Solana ondersteunt drie transactieformaten: legacy, v0 en v1. Elk formaat wordt hieronder beschreven aan de hand van dezelfde drie onderdelen: hoe de bytes op de wire worden ingedeeld, hoe accounts worden gerefereerd en waar de resourcelimieten vandaan komen.
v1-activeringsstatus
Het v1-formaat is actief op mainnet, devnet en testnet. Het lezen van transacties is een brekende wijziging: clients die zich er niet voor aanmelden, mislukken bij v1-transacties. Bestaande apps moeten Voorbereiden op v1 doornemen.
Formaatoverzicht
| Limiet | legacy | v0 | v1 |
|---|---|---|---|
| Max. transactiegrootte | 1.232 bytes | 1.232 bytes | 4.096 bytes |
| Accountadressen | ~32, groottegebonden | 64, via lookup-tabellen | 64, inline (96 met SIMD-0596) |
| Address lookup-tabellen | niet ondersteund | ondersteund | niet ondersteund |
| Resourcelimieten | ComputeBudget-instructies | ComputeBudget-instructies | berichtconfiguratie |
Legacy-formaat
Het originele formaat, en nog steeds de standaard in de meeste tooling. Het heeft helemaal geen versieprefix: de eerste byte van de transactie is de compact-u16-teller van de handtekeningenarray, en de eerste byte van het bericht is num_required_signatures, waarvan het hoogste bit altijd niet is ingesteld.
Legacy wire-indeling
| Veld | Grootte | Beschrijving |
|---|---|---|
num_signatures | compact-u16 | Aantal handtekeningen |
signatures | num_signatures x 64 bytes | Ed25519-handtekeningen |
header | 3 bytes | MessageHeader — eerste byte heeft versiebit niet ingesteld |
num_account_keys | compact-u16 | Aantal accountsleutels |
account_keys | num_account_keys x 32 bytes | Publieke sleutels, allemaal inline |
recent_blockhash | 32 bytes | Levensduurspecificatie |
num_instructions | compact-u16 | Aantal instructies |
instructions | variabel | Elke instructie aaneengesloten geserialiseerd |
Elke array met variabele lengte wordt voorafgegaan door een compact-u16-lengte: 1 byte voor waarden 0–127, 2–3 bytes voor grotere waarden. Zie voor de indeling per instructie en een uitgewerkte grootteberekening binair transactieformaat.
Accounts in legacy
Elk account wordt als volledige 32-byte publieke sleutel in account_keys opgeschreven, en
instructies verwijzen ernaar via een 1-byte-index in die array. Er is geen manier om
te verwijzen naar een account dat niet expliciet in de transactie is opgenomen, wat
bewerkstelligt dat een legacy-transactie beperkt blijft tot ongeveer 32 accounts voordat
de 1.232 bytes zijn opgebruikt.
Resourcelimieten in legacy
De limiet voor rekeneenheden, de datalimiet voor geladen accounts, de heapgrootte en de prioriteitstoeslag worden allemaal aangevraagd door ComputeBudget-programma-instructies in de transactie op te nemen. Elk kost een instructie-slot en 150 rekeneenheden. Weglaten is veilig: de runtime valt terug op standaardwaarden van 200.000 CU per instructie (maximaal 1,4M), een datalimiet van 64 MiB, een heap van 32 KiB en een prioriteitstoeslag van nul.
V0-formaat
v0 is een legacy-bericht plus twee zaken: een versieprefix-byte 0x80 en een
address_table_lookups-array die na de instructies wordt toegevoegd. Alles daarvoor
is byte-identiek aan legacy.
V0-wire-indeling
| Veld | Grootte | Beschrijving |
|---|---|---|
num_signatures | compact-u16 | Aantal handtekeningen |
signatures | num_signatures x 64 bytes | Ed25519-handtekeningen |
0x80 | 1 byte | Versieprefix-byte — eerste byte van het bericht |
header | 3 bytes | MessageHeader (hetzelfde als legacy) |
num_account_keys | compact-u16 | Aantal statische accountsleutels |
static_account_keys | num_account_keys x 32 bytes | Sleutels die letterlijk in de transactie verschijnen |
recent_blockhash | 32 bytes | Levensduurspecificatie |
num_instructions | compact-u16 | Aantal instructies |
instructions | variabel | Hetzelfde formaat als legacy |
address_table_lookups | compact-u16 + variabel | ALT-referenties (zie hieronder) |
Elke adrestabel-opzoekvermelding bevat:
| Veld | Grootte | Beschrijving |
|---|---|---|
account_key | 32 bytes | De publieke sleutel van het ALT-account |
writable_indexes | compact-u16 + N x 1 byte | Indices in de ALT voor schrijfbare accounts |
readonly_indexes | compact-u16 + N x 1 byte | Indices in de ALT voor alleen-lezen accounts |
Address lookup-tabellen
Een ALT is een onchain-account dat tot 256 publieke sleutels opslaat. Door naar een ALT te verwijzen, kan een transactie aanvullende accounts opnemen met behulp van 1-byte indices in plaats van 32-byte publieke sleutels, waardoor de overhead per account aanzienlijk wordt verminderd.
Tijdens runtime, voordat de uitvoering begint, lost de validator alle ALT-verwijzingen op naar volledige publieke sleutels. De opgeloste adressen worden toegevoegd aan de statische account sleutels om de volledige lijst met accountsleutels te vormen. ALT-opgeloste accounts volgen dezelfde volgorde als statische accounts: schrijfbare opzoekingen komen vóór alleen-lezen opzoekingen.
Adrestabel-opzoektabellen beïnvloeden alleen hoe accounts worden gerefereerd in de on-wire transactie. Tijdens uitvoering lost de runtime alle indices op naar volledige accountadressen. ALT-opgeloste accounts kunnen alleen schrijfbaar of alleen-lezen (niet-ondertekenaar) zijn; ze kunnen geen ondertekenaars zijn.
Een address lookup-tabel aanmaken en gebruiken
Dit voorbeeld maakt een lookup-tabel aan, voegt een adres toe en gebruikt de tabel om een v0-transactie te compileren en te verzenden.
Alleen accounts die geen ondertekenaar zijn, kunnen uit een lookup-tabel worden geladen. De fee payer en elke vereiste ondertekenaar blijven in de statische accountlijst van het bericht staan. Een adres kan ook niet worden opgezocht in dezelfde slot waarin het is toegevoegd, dus wacht het voorbeeld op de volgende bevestigde slot voordat het v0-bericht wordt gecompileerd.
Resourcelimieten in v0
Ongewijzigd ten opzichte van legacy: ComputeBudget-instructies, met dezelfde standaardwaarden wanneer ze worden weggelaten.
V1-formaat
v1 verhoogt de groottelimiet naar 4.096 bytes en herstructureert het bericht rond een transactieconfiguratie: resourcelimieten verhuizen uit ComputeBudget-instructies naar velden op vaste posities in het bericht zelf. Hierdoor kan het netwerk een transactie rangschikken op prioriteitstoeslag met één read op een vast offset, in plaats van de instructielijst te scannen en te deserialiseren.
V1-wire-indeling
| Veld | Grootte | Beschrijving |
|---|---|---|
0x81 | 1 byte | Versieprefix-byte — de eerste byte van de transactie |
header | 3 bytes | MessageHeader (hetzelfde als legacy) |
config_mask | 4 bytes | u32 LE-bitmasker dat aangeeft welke configuratiewaarden aanwezig zijn |
recent_blockhash | 32 bytes | Levensduurspecificatie |
num_instructions | 1 byte | Teller met vaste breedte, max. 64 |
num_addresses | 1 byte | Teller met vaste breedte, max. 64 (96 met SIMD-0596) |
addresses | N x 32 bytes | Accountadressen, allemaal inline — geen lookup-tabelverwijzingen |
config_values | 0–20 bytes | Één waarde per ingesteld maskerbit, in bitvolgorde (zie hieronder) |
instruction_headers | N x 4 bytes | Per instructie: program_id_index (u8), num_accounts (u8), data_len (u16 LE) |
instruction_payloads | variabel | Per instructie: accountindices, dan instruction data |
signatures | N x 64 bytes | Aan het einde, zonder lengteprefix — de teller komt uit de header |
Twee structurele verschillen ten opzichte van legacy en v0 zijn het vermelden waard bij het schrijven van een decoder. De tellers zijn u8-velden met vaste breedte in plaats van compact-u16, en de instructies zijn opgesplitst in twee reeksen: eerst alle headers met vaste grootte, dan alle payloads met variabele lengte, in plaats van elke instructie aaneengesloten.
Accounts in v1: geen address lookup-tabellen
v1 verwijdert ALT-ondersteuning bewust. 64 ruwe adressen zijn 2.048 bytes, en de 96 adressen die na SIMD-0596 zijn toegestaan, zijn 3.072 bytes, beide ruim binnen de limiet van 4.096 bytes, dus elk adres staat inline, zoals in legacy. Als je applicatie afhankelijk is van lookup-tabellen, betekent overstappen naar v1 dat je die adressen inline moet opnemen.
Resourcelimieten in v1: de transactieconfiguratie
De configuratie is een u32-bitmasker gevolgd door waarden met vaste breedte voor elk veld waarvan het bit is ingesteld:
| Bit(s) | Veld | Breedte | Opmerkingen |
|---|---|---|---|
| 0–1 | Prioriteitstoeslag | u64 | Totaal lamports — beide bits samen ingesteld |
| 2 | Limiet voor rekeneenheden | u32 | |
| 3 | Datalimiet voor geladen accounts | u32 | |
| 4 | Gevraagde heapgrootte | u32 |
Onbekende bits worden afgewezen. Omdat het bericht ondertekend is, kunnen niet-herkende configuratievelden niet stilzwijgend worden weggelaten.
Prioriteitstoeslag is totaal in lamports, geen prijs
In legacy en v0 wordt de prioriteitstoeslag ingesteld via SetComputeUnitPrice als
micro-lamports per rekeneenheid, vermenigvuldigd met de limiet voor rekeneenheden. In
v1 is het een absoluut totaal in lamports — geen vermenigvuldiging, geen afronding.
Gebruik de per-CU-rekenwijze niet over. De totale toeslagformule blijft verder
ongewijzigd: (signatures × lamports_per_signature) + priority_fee.
ComputeBudget-instructies zijn no-ops in v1
Een v1-transactie wijst ComputeBudget-instructies niet af — ze worden genegeerd voor configuratie. Ze worden nog steeds uitgevoerd als succesvolle no-ops, verbruiken 150 rekeneenheden en één van de 64 instructie-slots, zonder enig effect op het budget. Verwijder ze bij het bouwen van v1-transacties en stop met het scannen ernaar bij het lezen van v1-transacties: de waarden staan in de berichtconfiguratie.
Configuratievelden moeten expliciet worden ingesteld
De belangrijkste gedragswijziging voor verzenders: in tegenstelling tot legacy en v0 moeten v1-transacties de limiet voor rekeneenheden en de datalimiet voor geladen accounts expliciet instellen, anders mislukt je transactie.
| Niet-ingesteld veld | legacy / v0 | v1 | Gevolg bij weglaten |
|---|---|---|---|
| Limiet voor rekeneenheden | 200k per instructie, max. 1,4M | 0 CU | Mislukt onmiddellijk, budget uitgeput |
| Datagrootte geladen accounts | 64 MiB | 0 bytes | MaxLoadedAccountsDataSizeExceeded bij het eerste geladen account |
| Prioriteitstoeslag | 0 | 0 | — |
| Heapgrootte | 32 KiB | 32 KiB | — |
De aanbevolen aanpak is om eenmalig te simuleren met beide limieten gemaximaliseerd, vervolgens de teruggegeven unitsConsumed en loadedAccountsDataSize terug te schrijven naar de configuratie, waarbij de datagrootte naar boven wordt afgerond naar de volgende 32 KiB-pagina voor marge (het blokkostenmodel rekent in 32 KiB-pagina's, dus marge onder de volgende paginagrens is gratis).
Een v1-transactie bouwen
Een v1-bericht wordt samengesteld via dezelfde pipeline als een legacy- of v0-bericht. Het
verschil is setTransactionMessageConfig: de resourcelimieten zijn een eigenschap
van het bericht, dus de instructielijst bevat alleen de programma-instructies.
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 biedt ook een setter per configuratieveld —
setTransactionMessagePriorityFeeLamports,
setTransactionMessageComputeUnitLimit,
setTransactionMessageLoadedAccountsDataSizeLimit en
setTransactionMessageHeapSize — om ze één voor één in te stellen.
De limieten bepalen via simulatie
In plaats van de twee verplichte limieten te raden, kun je ze meten. Simulatie draait met beide limieten verhoogd naar het runtime-maximum, dus ze kan niet mislukken door een tekort aan de resources die ze meet. Het geretourneerde getal is de exacte kostprijs van één gesimuleerde run, dus voeg een marge toe voordat je erop vertrouwt.
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),);
Voorbereiden op v1
v1 verandert het lezen van transacties, niet alleen het verzenden ervan. Elke client die
getTransaction of getBlock aanroept zonder zich aan te melden, mislukt bij v1-transacties:
- Geef
maxSupportedTransactionVersion: 1door — het JSON-geheel getal1, niet de string"1"— aangetTransactionengetBlock.0doorgeven mislukt op v1-transacties net als het weglaten van de parameter, dus een codebase die tijdens de v0- uitrol is bijgewerkt, moet de waarde alsnog wijzigen. getTransactionmislukt bij een v1-transactie met fout-32015, en één v1- transactie laat een volledigegetBlock-respons mislukken met dezelfde fout — er is geen gedeeltelijk resultaat.blockSubscribegeeftblock: nullterug en stopt met vooruitgaan, zodat een consumer die dit als een leeg blok leest stilzwijgend achteropraakt vanaf de eerste v1-slot.getSignaturesForAddressinspecteert nooit transactie-inhoud, dus v1-handtekeningen worden normaal weergegeven.- Responsen met opt-in bevatten voor v1-transacties een
transactionConfig-object in het bericht (volledig afwezig voor legacy en v0). Pipelines die prioriteitsfees of rekenlimieten afleiden door te scannen op ComputeBudget-instructies, rapporteren stilzwijgend nul voor elke v1-transactie. - Gebruik
encoding: 'base64'wanneer je transacties aan clientzijde decodeert, en voorsendTransaction/simulateTransactionmet transacties groter dan 1.232 bytes — base58-codering blijft beperkt tot de oude grootte. - Ondersteuning voor clientbibliotheken vereist recente versies:
@solana/kit8.0+, Agave 4.2.x-generatie Rust-crates, of web3.js v3. web3.js v1 leest v1 vanaf 1.99.0 en hoger, maar kan het niet bouwen of verzenden.
Een v1-transactie lezen
Aanmelden gebeurt met het JSON-geheel getal 1. Het decoderen van de geretourneerde bytes levert de
versie en de config op, en daar staan de resourcelimieten — het scannen van de
instructielijst op ComputeBudget-instructies levert niets op.
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);}
Voor de volledige migratiegids — inclusief ondersteuning door clientbibliotheken, detectie van de versie bij streaming (Geyser/gRPC) en simulatiegedrag — zie de upgradepagina voor Transaction Format v1. Uitvoerbare voorbeelden in TypeScript, Rust, Python en Go staan in solana-foundation/transaction-v1-examples.
Is this page helpful?