Zusammenfassung
Solana verfügt über drei Transaktionsformate: legacy, v0 und v1. v0 fügt Address Lookup Tables (ALTs) hinzu, um Konten über 1-Byte-Indizes zu referenzieren. v1 erhöht das Größenlimit auf 4.096 Bytes, verschiebt Ressourcenlimits in die Nachricht selbst und entfernt ALTs.
Solana unterstützt drei Transaktionsformate: legacy, v0 und v1. Jedes Format wird nachfolgend anhand derselben drei Aspekte beschrieben: der Byte-Anordnung auf der Leitung, der Art der Kontenreferenzierung und der Herkunft der Ressourcenlimits.
Aktivierungsstatus v1
Das v1-Format ist auf Mainnet, Devnet und Testnet aktiv. Das Lesen von Transaktionen ist eine Breaking Change: Clients, die sich nicht dafür entscheiden, schlagen bei v1-Transaktionen fehl. Bestehende Apps sollten Vorbereitung auf v1 lesen.
Formatvergleich
| Limit | legacy | v0 | v1 |
|---|---|---|---|
| Maximale Transaktionsgröße | 1.232 Bytes | 1.232 Bytes | 4.096 Bytes |
| Kontenadressen | ~32, größenbegrenzt | 64, über Lookup-Tabellen | 64, inline (96 mit SIMD-0596) |
| Address Lookup Tables | nicht unterstützt | unterstützt | nicht unterstützt |
| Ressourcenlimits | ComputeBudget- Anweisungen | ComputeBudget- Anweisungen | Nachrichtenkonfiguration |
Legacy-Format
Das ursprüngliche Format und nach wie vor der Standard in den meisten Tools. Es hat kein
Versionspräfix: Das erste Byte der Transaktion ist die compact-u16-Anzahl des
Signatur-Arrays, und das erste Byte der Nachricht ist num_required_signatures,
dessen höchstes Bit immer nicht gesetzt ist.
Legacy-Leitungsformat
| Feld | Größe | Beschreibung |
|---|---|---|
num_signatures | compact-u16 | Anzahl der Signaturen |
signatures | num_signatures x 64 Bytes | Ed25519-Signaturen |
header | 3 Bytes | MessageHeader — erstes Byte hat Versionsbit nicht gesetzt |
num_account_keys | compact-u16 | Anzahl der Kontenschlüssel |
account_keys | num_account_keys x 32 Bytes | Öffentliche Schlüssel, alle inline |
recent_blockhash | 32 Bytes | Lebensdauer-Bezeichner |
num_instructions | compact-u16 | Anzahl der Anweisungen |
instructions | variabel | Jede Anweisung fortlaufend serialisiert |
Jedem Array variabler Länge ist eine compact-u16-Länge vorangestellt: 1 Byte für Werte 0–127, 2–3 Bytes für größere Werte. Das Format pro Anweisung und eine Beispielgrößenberechnung finden Sie unter Transaktions-Binärformat.
Konten in legacy
Jedes Konto wird als vollständiger 32-Byte-Public-Key in account_keys geschrieben, und
Anweisungen referenzieren sie über einen 1-Byte-Index in diesem Array. Es gibt keine Möglichkeit,
ein Konto zu referenzieren, das nicht explizit in der Transaktion angegeben ist – dies begrenzt
eine Legacy-Transaktion auf ungefähr 32 Konten, bevor die 1.232 Bytes ausgeschöpft sind.
Ressourcenlimits in legacy
Compute-Unit-Limit, Datenlimit für geladene Konten, Heap-Größe und priority fee werden alle durch das Einbinden von ComputeBudget-Programm- Anweisungen in die Transaktion angefordert. Jede kostet einen Anweisungs-slot und 150 Compute Units. Das Weglassen ist sicher: Die Laufzeit fällt auf Standardwerte zurück: 200.000 CU pro Anweisung (max. 1,4 M), ein 64-MiB-Datenlimit, einen 32-KiB-Heap und eine priority fee von null.
V0-Format
v0 ist eine Legacy-Nachricht plus zwei Ergänzungen: ein 0x80-Versionspräfix-Byte und ein
address_table_lookups-Array, das nach den Anweisungen angehängt wird. Alles davor
ist byteidentisch mit legacy.
V0-Leitungsformat
| Feld | Größe | Beschreibung |
|---|---|---|
num_signatures | compact-u16 | Anzahl der Signaturen |
signatures | num_signatures x 64 Bytes | Ed25519-Signaturen |
0x80 | 1 Byte | Versionspräfix-Byte — erstes Byte der Nachricht |
header | 3 Bytes | MessageHeader (wie bei Legacy) |
num_account_keys | compact-u16 | Anzahl der statischen Kontenschlüssel |
static_account_keys | num_account_keys x 32 Bytes | Schlüssel, die wörtlich in der Transaktion erscheinen |
recent_blockhash | 32 Bytes | Lebensdauer-Bezeichner |
num_instructions | compact-u16 | Anzahl der Anweisungen |
instructions | variabel | Gleiches Format wie Legacy |
address_table_lookups | compact-u16 + variabel | ALT-Referenzen (siehe unten) |
Jeder Eintrag in der Adress-Lookup-Tabelle enthält:
| Feld | Größe | Beschreibung |
|---|---|---|
account_key | 32 Bytes | Der öffentliche Schlüssel des ALT-Kontos |
writable_indexes | compact-u16 + N x 1 Byte | Indizes in die ALT für beschreibbare Konten |
readonly_indexes | compact-u16 + N x 1 Byte | Indizes in die ALT für schreibgeschützte Konten |
Address Lookup Tables
Eine ALT ist ein On-Chain-Konto, das bis zu 256 öffentliche Schlüssel speichert. Durch die Referenzierung einer ALT kann eine Transaktion zusätzliche Konten mithilfe von 1-Byte-Indizes anstelle von 32-Byte-Public-Keys einbinden, wodurch der Overhead pro Konto erheblich reduziert wird.
Zur Laufzeit, vor Beginn der Ausführung, löst der Validator alle ALT-Referenzen in vollständige öffentliche Schlüssel auf. Die aufgelösten Adressen werden an die statischen Kontenschlüssel angehängt, um die vollständige Liste der Kontenschlüssel zu bilden. ALT-aufgelöste Konten folgen derselben Reihenfolge wie statische Konten: beschreibbare Lookups kommen vor schreibgeschützten Lookups.
Adress-Lookup-Tabellen beeinflussen nur, wie Konten in der On-Wire-Transaktion referenziert werden. Zur Ausführungszeit löst die Runtime alle Indizes in vollständige Kontenadressen auf. ALT-aufgelöste Konten können nur beschreibbar oder schreibgeschützt (nicht signierend) sein; sie können keine Signierer sein.
Eine Address Lookup Table erstellen und verwenden
Dieses Beispiel erstellt eine Address Lookup Table, fügt eine Adresse hinzu und verwendet die Table, um eine v0-Transaktion zu kompilieren und zu senden.
Nur Nicht-Signer-Konten können aus einer Lookup Table geladen werden. Der Fee Payer und jeder erforderliche Signer verbleiben in der statischen Kontenliste der Nachricht. Eine Adresse kann außerdem nicht im selben slot nachgeschlagen werden, in dem sie hinzugefügt wurde, daher wartet das Beispiel auf den nächsten bestätigten slot, bevor die v0-Nachricht kompiliert wird.
Ressourcenlimits in v0
Unverändert gegenüber legacy: ComputeBudget- Anweisungen, mit denselben Standardwerten, wenn sie weggelassen werden.
V1-Format
v1 erhöht das Größenlimit auf 4.096 Bytes und strukturiert die Nachricht um eine Transaktionskonfiguration herum: Ressourcenlimits werden aus den ComputeBudget- Anweisungen herausgelöst und in fest positionierte Felder der Nachricht selbst verschoben. Dadurch kann das Netzwerk eine Transaktion nach priority fee durch einen einzelnen Lesevorgang mit festem Offset priorisieren, anstatt dessen Anweisungsliste zu durchsuchen und zu deserialisieren.
V1-Leitungsformat
| Feld | Größe | Beschreibung |
|---|---|---|
0x81 | 1 Byte | Versionspräfix-Byte — das erste Byte der Transaktion |
header | 3 Bytes | MessageHeader (wie bei Legacy) |
config_mask | 4 Bytes | u32 LE-Bitmaske, die angibt, welche Konfigurationswerte vorhanden sind |
recent_blockhash | 32 Bytes | Lebensdauer-Bezeichner |
num_instructions | 1 Byte | Fest codierte Anzahl, max. 64 |
num_addresses | 1 Byte | Fest codierte Anzahl, max. 64 (96 mit SIMD-0596) |
addresses | N x 32 Bytes | Kontenadressen, alle inline — keine Lookup-Table-Referenzen |
config_values | 0–20 Bytes | Ein Wert pro gesetztem Maskenbit, in Bit-Reihenfolge (siehe unten) |
instruction_headers | N x 4 Bytes | Pro Anweisung: program_id_index (u8), num_accounts (u8), data_len (u16 LE) |
instruction_payloads | variabel | Pro Anweisung: Kontenindizes, dann instruction data |
signatures | N x 64 Bytes | Am Ende, ohne Längenpräfix — die Anzahl ergibt sich aus dem Header |
Beim Schreiben eines Decoders sind zwei strukturelle Unterschiede gegenüber legacy und v0 zu beachten. Die Zählfelder sind fest codierte u8-Felder statt compact-u16, und die Anweisungen sind in zwei Blöcke aufgeteilt: zunächst alle Header fester Größe, dann alle Payloads variabler Länge, anstatt dass jede Anweisung fortlaufend steht.
Konten in v1: keine Address Lookup Tables
v1 entfernt die ALT-Unterstützung bewusst. 64 Rohadressen sind 2.048 Bytes, und die 96 Adressen, die nach SIMD-0596 zulässig sind, sind 3.072 Bytes – beides liegt komfortabel innerhalb des 4.096-Byte-Limits, sodass jede Adresse inline ist, wie in legacy. Wenn Ihre Anwendung von Lookup Tables abhängt, bedeutet der Wechsel zu v1, diese Adressen inline einzubinden.
Ressourcenlimits in v1: die Transaktionskonfiguration
Die Konfiguration besteht aus einer u32-Bitmaske gefolgt von fest codierten Werten für jedes Feld,
dessen Bit gesetzt ist:
| Bit(s) | Feld | Breite | Hinweise |
|---|---|---|---|
| 0–1 | Priority fee | u64 | Gesamt-lamports — beide Bits gemeinsam gesetzt |
| 2 | Compute-Unit-Limit | u32 | |
| 3 | Datenlimit für geladene Konten | u32 | |
| 4 | Angeforderter Heap-Speicher | u32 |
Unbekannte Bits werden abgelehnt. Da die Nachricht signiert ist, können nicht erkannte Konfigurationsfelder nicht stillschweigend verworfen werden.
Priority fee ist Gesamt-lamports, kein Preis
In legacy und v0 wird die priority fee über SetComputeUnitPrice als
Micro-lamports pro Compute Unit angegeben, multipliziert mit dem Compute-Unit-Limit. In
v1 handelt es sich um einen absoluten Gesamtbetrag in lamports — keine Multiplikation, keine Rundung.
Übertragen Sie die CU-basierte Berechnung nicht. Die Gesamtgebührenformel bleibt ansonsten
unverändert: (signatures × lamports_per_signature) + priority_fee.
ComputeBudget- Anweisungen sind No-Ops in v1
Eine v1-Transaktion lehnt ComputeBudget- Anweisungen nicht ab — sie ignoriert sie für die Konfiguration. Sie werden weiterhin als erfolgreiche No-Ops ausgeführt, verbrauchen 150 Compute Units und einen der 64 Anweisungs-slots, ohne Einfluss auf das Budget zu haben. Entfernen Sie sie beim Erstellen von v1-Transaktionen, und hören Sie beim Lesen von v1-Transaktionen auf, nach ihnen zu suchen: Die Werte befinden sich in der Nachrichtenkonfiguration.
Konfigurationsfelder müssen explizit gesetzt werden
Die wichtigste Verhaltensänderung für Sender: Im Gegensatz zu legacy und v0 müssen bei v1-Transaktionen das Compute-Unit-Limit und das Datenlimit für geladene Konten explizit gesetzt werden, sonst schlägt Ihre Transaktion fehl.
| Nicht gesetztes Feld | legacy / v0 | v1 | Symptom bei Weglassen |
|---|---|---|---|
| Compute-Unit-Limit | 200k pro Anweisung, max. 1,4 M | 0 CU | Schlägt sofort fehl, Budget erschöpft |
| Datengröße geladener Konten | 64 MiB | 0 Bytes | MaxLoadedAccountsDataSizeExceeded beim ersten geladenen Konto |
| Priority fee | 0 | 0 | — |
| Heap-Größe | 32 KiB | 32 KiB | — |
Die empfohlene Vorgehensweise ist, einmal mit maximierten Limits zu simulieren und dann die zurückgegebenen Werte unitsConsumed und loadedAccountsDataSize in die Konfiguration zu schreiben, wobei die Datengröße auf die nächste 32-KiB-Seite aufgerundet wird, um Puffer zu schaffen (das Blockkostenmodell rechnet in 32-KiB-Seiten, daher ist Puffer unterhalb der nächsten Seitengrenze kostenlos).
Eine v1-Transaktion erstellen
Eine v1-Nachricht wird über dieselbe Pipeline zusammengestellt wie eine legacy- oder v0-Nachricht. Der
Unterschied ist setTransactionMessageConfig: Die Ressourcenlimits sind eine Eigenschaft
der Nachricht, sodass die Liste der Anweisungen nur die Anweisungen der Programme enthält.
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 stellt außerdem für jedes Konfigurationsfeld einen eigenen Setter bereit —
setTransactionMessagePriorityFeeLamports,
setTransactionMessageComputeUnitLimit,
setTransactionMessageLoadedAccountsDataSizeLimit und
setTransactionMessageHeapSize —, um sie einzeln zu setzen.
Limits per Simulation bestimmen
Statt die beiden obligatorischen Limits zu schätzen, messen Sie sie. Die Simulation läuft mit beiden auf das Runtime-Maximum angehobenen Limits, sodass sie nicht aus Mangel an den Ressourcen scheitern kann, die sie misst. Der zurückgegebene Wert entspricht genau den Kosten eines simulierten Durchlaufs, fügen Sie daher einen Puffer hinzu, bevor Sie sich darauf verlassen.
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),);
Vorbereitung auf v1
v1 ändert das Lesen von Transaktionen, nicht nur das Senden. Jeder Client, der
getTransaction oder getBlock aufruft, ohne sich dafür zu entscheiden, schlägt bei v1-Transaktionen fehl:
- Übergeben Sie
maxSupportedTransactionVersion: 1— die JSON-Ganzzahl1, nicht den String"1"— angetTransactionundgetBlock. Die Übergabe von0schlägt bei v1-Transaktionen genauso fehl wie das Weglassen des Parameters. Eine Codebasis, die beim v0-Rollout aktualisiert wurde, muss den Wert daher noch ändern. getTransactionschlägt bei einer v1-Transaktion mit Fehler-32015fehl, und eine einzelne v1-Transaktion lässt eine gesamtegetBlock-Antwort mit demselben Fehler fehlschlagen — es gibt kein Teilergebnis.blockSubscribegibtblock: nullzurück und hört auf, fortzuschreiten. Ein Consumer, der dies als leeren Block interpretiert, fällt ab dem ersten v1-slot stillschweigend zurück.getSignaturesForAddressuntersucht Transaktionsinhalte nie, daher werden v1-Signaturen normal aufgelistet.- Antworten mit Opt-in enthalten bei v1-Transaktionen ein
transactionConfig-Objekt in der Nachricht (bei legacy und v0 fehlt es vollständig). Pipelines, die priority fee oder Compute-Limits durch das Scannen nach ComputeBudget- Anweisungen ableiten, melden stillschweigend für jede v1-Transaktion null. - Verwenden Sie
encoding: 'base64', wenn Sie Transaktionen clientseitig dekodieren, sowie fürsendTransaction/simulateTransactionbei Transaktionen über 1.232 Bytes — base58-Kodierung ist weiterhin auf die alte Größe begrenzt. - Die Unterstützung durch Client-Bibliotheken erfordert aktuelle Versionen:
@solana/kit8.0+, Agave 4.2.x-Generation Rust-Crates oder web3.js v3. web3.js v1 liest v1 ab 1.99.0, kann es aber weder erstellen noch senden.
Eine v1-Transaktion lesen
Das Opt-in erfolgt über die JSON-Ganzzahl 1. Das Dekodieren der zurückgegebenen Bytes liefert die
Version und die Konfiguration, in der die Ressourcenlimits liegen — das Durchsuchen der
Liste der Anweisungen nach ComputeBudget- Anweisungen findet nichts.
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);}
Den vollständigen Migrationsleitfaden — einschließlich Unterstützung durch Client-Bibliotheken, Versionserkennung beim Streaming (Geyser/gRPC) und Simulationsverhalten — finden Sie auf der Upgrade-Seite zu Transaction Format v1. Ausführbare Beispiele in TypeScript, Rust, Python und Go finden Sie in solana-foundation/transaction-v1-examples.
Is this page helpful?