Versioned transactions

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

Limietlegacyv0v1
Max. transactiegrootte1.232 bytes1.232 bytes4.096 bytes
Accountadressen~32, groottegebonden64, via lookup-tabellen64, inline (96 met SIMD-0596)
Address lookup-tabellenniet ondersteundondersteundniet ondersteund
ResourcelimietenComputeBudget-instructiesComputeBudget-instructiesberichtconfiguratie

Ga naar: Legacy · v0 · v1

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

VeldGrootteBeschrijving
num_signaturescompact-u16Aantal handtekeningen
signaturesnum_signatures x 64 bytesEd25519-handtekeningen
header3 bytesMessageHeader — eerste byte heeft versiebit niet ingesteld
num_account_keyscompact-u16Aantal accountsleutels
account_keysnum_account_keys x 32 bytesPublieke sleutels, allemaal inline
recent_blockhash32 bytesLevensduurspecificatie
num_instructionscompact-u16Aantal instructies
instructionsvariabelElke 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

VeldGrootteBeschrijving
num_signaturescompact-u16Aantal handtekeningen
signaturesnum_signatures x 64 bytesEd25519-handtekeningen
0x801 byteVersieprefix-byte — eerste byte van het bericht
header3 bytesMessageHeader (hetzelfde als legacy)
num_account_keyscompact-u16Aantal statische accountsleutels
static_account_keysnum_account_keys x 32 bytesSleutels die letterlijk in de transactie verschijnen
recent_blockhash32 bytesLevensduurspecificatie
num_instructionscompact-u16Aantal instructies
instructionsvariabelHetzelfde formaat als legacy
address_table_lookupscompact-u16 + variabelALT-referenties (zie hieronder)

Elke adrestabel-opzoekvermelding bevat:

VeldGrootteBeschrijving
account_key32 bytesDe publieke sleutel van het ALT-account
writable_indexescompact-u16 + N x 1 byteIndices in de ALT voor schrijfbare accounts
readonly_indexescompact-u16 + N x 1 byteIndices 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

VeldGrootteBeschrijving
0x811 byteVersieprefix-byte — de eerste byte van de transactie
header3 bytesMessageHeader (hetzelfde als legacy)
config_mask4 bytesu32 LE-bitmasker dat aangeeft welke configuratiewaarden aanwezig zijn
recent_blockhash32 bytesLevensduurspecificatie
num_instructions1 byteTeller met vaste breedte, max. 64
num_addresses1 byteTeller met vaste breedte, max. 64 (96 met SIMD-0596)
addressesN x 32 bytesAccountadressen, allemaal inline — geen lookup-tabelverwijzingen
config_values0–20 bytesÉén waarde per ingesteld maskerbit, in bitvolgorde (zie hieronder)
instruction_headersN x 4 bytesPer instructie: program_id_index (u8), num_accounts (u8), data_len (u16 LE)
instruction_payloadsvariabelPer instructie: accountindices, dan instruction data
signaturesN x 64 bytesAan 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)VeldBreedteOpmerkingen
0–1Prioriteitstoeslagu64Totaal lamports — beide bits samen ingesteld
2Limiet voor rekeneenhedenu32
3Datalimiet voor geladen accountsu32
4Gevraagde heapgrootteu32

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 veldlegacy / v0v1Gevolg bij weglaten
Limiet voor rekeneenheden200k per instructie, max. 1,4M0 CUMislukt onmiddellijk, budget uitgeput
Datagrootte geladen accounts64 MiB0 bytesMaxLoadedAccountsDataSizeExceeded bij het eerste geladen account
Prioriteitstoeslag00—
Heapgrootte32 KiB32 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.

Kit
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);
Console
Click to execute the code.

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.

Kit
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),
);
Console
Click to execute the code.

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: 1 door — het JSON-geheel getal 1, niet de string "1" — aan getTransaction en getBlock. 0 doorgeven 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.
  • getTransaction mislukt bij een v1-transactie met fout -32015, en één v1- transactie laat een volledige getBlock-respons mislukken met dezelfde fout — er is geen gedeeltelijk resultaat.
  • blockSubscribe geeft block: null terug en stopt met vooruitgaan, zodat een consumer die dit als een leeg blok leest stilzwijgend achteropraakt vanaf de eerste v1-slot.
  • getSignaturesForAddress inspecteert 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 voor sendTransaction/simulateTransaction met transacties groter dan 1.232 bytes — base58-codering blijft beperkt tot de oude grootte.
  • Ondersteuning voor clientbibliotheken vereist recente versies: @solana/kit 8.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.

Kit
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);
}
Console
Click to execute the code.

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?

© 2026 Solana Foundation. Alle rechten voorbehouden.