NEU: Helius übernimmt Light Protocol
Solana-Transaktionsversionierung: Legacy, v0 und v1
Blog/Grundlagen

Solana-Transaktionsversionierung: Legacy, v0 und v1

ForscherLostin auf X
17 Min. Lesezeit

Eine kurze Geschichte der Transaktionsformate von Solana

Solana hat drei Transaktionsformate: Legacy, v0 und v1. Im Wesentlichen erfüllen sie alle denselben Zweck. Sie bündeln Signaturen, Accounts und Anweisungen, die Validatoren für Onchain-Programme ausführen.

Im Laufe der Zeit hat sich das Wire-Format verändert, also die Art, wie diese Informationen serialisiert und über das Netzwerk übertragen werden.

Jedes neue Format wurde eingeführt, um Einschränkungen zu beheben, die mit zunehmend komplexen Solana-Anwendungen auftraten. Gleichzeitig blieb die Abwärtskompatibilität erhalten, damit ältere Formate weiter funktionieren.

Alle drei Formate existieren parallel im Netzwerk. Entwickler können das Format verwenden, das am besten zur jeweiligen Transaktion passt.

Legacy-Transaktionen

Das ursprüngliche Format heißt Legacy, weil es vor der Transaktionsversionierung entstand und keine Versionskennung enthält.

Die maximale Größe von 1.232 Byte geht auf das ursprüngliche Netzwerkdesign von Solana zurück: Transaktionen sollten in die minimale IPv6-MTU von 1.280 Byte passen. Nach Abzug des Netzwerk-Overheads blieben 1.232 Byte.

Für kleine Transaktionen funktionierte das gut. Mit dem Wachstum der Anwendungen wurde der Platz jedoch zunehmend knapp. Jede direkt in einer Transaktion enthaltene Account-Adresse belegt 32 Byte, jede Ed25519-Signatur weitere 64 Byte.

In der Praxis konnten Transaktionen mit vielen Accounts daher das Byte-Limit lange vor dem Account-Lock-Limit der Runtime erreichen.

Transaktion v0

Der erste Versuch, diesen Engpass zu beheben, war Transaktion v0. Sie ging im Oktober 2022 auf Mainnet-Beta live. Statt das Limit von 1.232 Byte zu erhöhen, führte v0 Address Lookup Tables (ALTs) ein.

Eine ALT speichert Account-Adressen onchain. Eine v0-Transaktion kann über Ein-Byte-Indizes auf sie verweisen, statt jeden öffentlichen 32-Byte-Schlüssel zu wiederholen. So erreichen Transaktionen das Runtime-Limit von 64 Accounts und bleiben zugleich innerhalb der bestehenden Paketgrößenbeschränkung.

v0 war eine inkrementelle Erweiterung des Legacy-Formats: Es ergänzte das Versionspräfix 0x80 und einen Abschnitt für Address Lookups, behielt aber die grundlegende Nachrichten- und Anweisungscodierung bei.

Mit der Versionierung entstand außerdem ein Framework, in dem neue Transaktionsformate neben Legacy-Transaktionen existieren können, statt sie zu ersetzen.

ALTs lösten das unmittelbare Problem der Account-Größe, brachten aber neue Komplexität mit sich. Anwendungen müssen Lookup Tables onchain erstellen und verwalten. Validatoren müssen deren Einträge vor der Ausführung auflösen. RPC-Dienste und Indexer benötigen die aufgelösten Adressen, um die vollständige Transaktion zu rekonstruieren.

Auch die historische Decodierung wird deutlich schwieriger, wenn die Lookup Table, auf die eine Rohtransaktion verweist, inzwischen geschlossen wurde. v0 komprimierte die Account-Liste also effektiv, führte dafür aber eine externe Onchain-Abhängigkeit in die Wire-Darstellung der Transaktion ein.

ALTs wurden von Solana-Entwicklern intensiv genutzt. Eine kurz vor der Einführung von v1-Transaktionen durchgeführte Untersuchung ergab, dass rund 62 % der v0-Transaktionen auf mindestens eine ALT verweisen.

Nachträgliche Integration von Priority Fees

Priority Fees wurden 2022 ebenfalls auf Solana eingeführt, noch bevor v0-Transaktionen im Mainnet aktiviert wurden. Das v0-Format war zu diesem Zeitpunkt allerdings bereits entworfen. Statt eines der beiden Transaktionsformate neu zu gestalten, ergänzte Solana die Gebührenkonfiguration über das bestehende Compute Budget Program. Transaktionen konnten Anweisungen wie SetComputeUnitLimit und SetComputeUnitPrice enthalten. So konnten Nutzer ein Compute-Budget anfordern und eine zusätzliche Gebühr für eine bevorzugte Einplanung festlegen. Das funktionierte für Legacy- und v0-Transaktionen gleichermaßen und vermied einen separaten Gebührenmechanismus für jedes Format.

Das war eine pragmatische, abwärtskompatible, aber nicht besonders elegante Lösung. Priority Fees und Compute-Limits sind eigentlich Metadaten auf Transaktionsebene. Legacy und v0 hatten dafür jedoch keine eigenen Felder, sodass sie als Programmaufrufe nachträglich in den Anweisungsstream integriert wurden. Deshalb muss die Transaktion auf das Compute Budget Program verweisen, die Konfiguration belegt Anweisungsbytes und Validatoren müssen diese Anweisungen beim Einlesen der Transaktion prüfen, um die Gebühren- und Ressourceneinstellungen zu ermitteln.

v0 war daher keine umfassende Neugestaltung des Solana-Transaktionsformats. Das Hauptziel bestand darin, den Engpass bei Account-Adressen mithilfe von Address Lookup Tables zu beheben und den Großteil der Legacy-Nachrichtenstruktur beizubehalten. Da Compute-Budget-Anweisungen bereits eine kompatible Unterstützung von Priority Fees in beiden Formaten ermöglichten, gab es kaum einen Grund, den Umfang von v0 weiter auszudehnen.

Größere Transaktionen

Im Laufe der Zeit stellte Solana das Einlesen von Transaktionen von UDP auf QUIC um. Dessen Streams sind nicht mehr auf eine einzelne Nutzlast in MTU-Größe beschränkt. Die ursprüngliche Obergrenze von 1.232 Byte war damit keine Transportanforderung mehr. 2025 wurden zwei SIMDs für ein neues v1-Format vorgeschlagen. SIMD-0296 erhöhte die maximale Größe einer v1-Transaktion auf 4.096 Byte, etwa das 3,3-Fache des bisherigen Limits. SIMD-0385 definierte ein vollständig neues Wire-Format, das diesen zusätzlichen Platz nutzt und Validatoren ein effizienteres Einlesen ermöglicht.

Durch den zusätzlichen Platz kann v1 vollständig auf ALTs verzichten. Das Limit von 64 Account-Adressen mit je 32 Byte erfordert höchstens 2.048 Byte. Die vollständige Account-Liste lässt sich daher einfach inline übertragen.

Analysen der Solana Foundation deuten darauf hin, dass die Auswirkungen des Inline-Speicherns aller Adressen beim Upgrade der meisten bestehenden v0-Transaktionen moderat sind: 50 % wachsen um weniger als 420 Byte, 90 % um weniger als 1.400 Byte. Selbst Transaktionen, die ALTs intensiv nutzen, behalten damit innerhalb des v1-Limits von 4.096 Byte erheblichen Spielraum.

Noch wichtiger ist, dass v1 nicht bloß eine größere v0-Transaktion ist. Das Wire-Format wurde umfassend neu strukturiert: Signaturen stehen am Ende, die Ressourcenkonfiguration wandert aus den Compute-Budget-Anweisungen in einen Abschnitt für die Transaktionskonfiguration und Anweisungsnutzlasten variabler Länge werden von Headern fester Breite getrennt. Dadurch können Validatoren Transaktionen günstiger und einfacher parsen und priorisieren.

Das größere Limit von 4.096 Byte ermöglicht außerdem Workloads, bei denen eine reine Adresskomprimierung nie geholfen hätte. Zero-Knowledge-Beweise, verschachtelte Multisigs, ungekürzte Winternitz-Signaturen, BLS-Signaturen und andere kryptografische Operationen können Hunderte oder Tausende Byte an Anweisungsdaten erfordern. Vertrauliche Token-2022-Guthaben lassen sich nun beispielsweise als eine einzige atomare v1-Transaktion erstellen, statt sie auf mehrere Transaktionen aufzuteilen.

Das anfängliche v1-Format erhöht insbesondere weder die Compute-, Signatur- und Anweisungsanzahl-Limits noch das Limit von 64 Accounts. Es beseitigt hauptsächlich den Größenengpass und gestaltet die Wire-Darstellung der Transaktion neu.

SIMD-0596, vorgeschlagen von Anza, würde das Account-Lock-Limit von v1 von 64 auf 96 erhöhen. Das umfasst alle Accounts, auf die die Transaktion verweist, darunter Signierer, Programm-IDs sowie schreibbare und schreibgeschützte Accounts. Zum Zeitpunkt der Veröffentlichung wird dieser Vorschlag noch geprüft. Legacy- und v0-Transaktionen blieben auf 64 begrenzt.

Formatspezifikationen und Vergleich

LimitLegacyv0v1
Maximale Größe1.232 Byte1.232 Byte4.096 Byte
Account-Adressen~32, größenbegrenzt64, über Lookup Tables64, inline
Address Lookup TablesNicht unterstütztUnterstütztNicht unterstützt
Doppelte Adressenzulässigzulässigabgelehnt
PräfixKeines0x800x81
Priority FeeSetComputeUnitPrice-Anweisung, Mikro-Lamports pro CUSetComputeUnitPrice-Anweisung, Mikro-Lamports pro CUconfig.priorityFeeLamports, gesamte Lamports
Compute-Unit-LimitSetComputeUnitLimit-AnweisungSetComputeUnitLimit-Anweisungconfig.computeUnitLimit
Heap-GrößeRequestHeapFrame-AnweisungRequestHeapFrame-Anweisungconfig.heapSize
Limit geladener AccountsSetLoadedAccountsDataSizeLimit-AnweisungSetLoadedAccountsDataSizeLimit-Anweisungconfig.loadedAccountsDataSizeLimit

Spezifikation von Legacy-Transaktionen

Eine serialisierte Legacy-Transaktion beginnt mit einer als compact-u16 codierten Signaturanzahl, gefolgt von den 64 Byte großen Ed25519-Signaturen.

Danach folgt die Nachricht. Sie enthält den Drei-Byte-Header, die Account-Liste mit vorangestellter kompakter Länge, den 32 Byte großen aktuellen Blockhash und ein Array kompilierter Anweisungen mit vorangestellter kompakter Länge.

Legacy-Transaktionen haben kein Versionspräfix.

Code
NumSignatures (compact-u16)

Signatures [[u8; 64]]
  -- Length = NumSignatures

MessageHeader (u8, u8, u8)
  -- (NumRequiredSignatures,
      NumReadonlySignedAccounts,
      NumReadonlyUnsignedAccounts)

NumAccountKeys (compact-u16)

AccountKeys [[u8; 32]]
  -- Length = NumAccountKeys

RecentBlockhash [u8; 32]

NumInstructions (compact-u16)

Instructions [CompiledInstruction]
  -- Length = NumInstructions

CompiledInstruction:
  ProgramIdIndex (u8)

  NumInstructionAccounts (compact-u16)

  InstructionAccountIndexes [u8]
    -- Length = NumInstructionAccounts

  InstructionDataLength (compact-u16)

  InstructionData [u8]
    -- Length = InstructionDataLength

Ein wichtiges Merkmal ist, dass jede CompiledInstruction vollständig serialisiert wird, bevor die nächste Anweisung beginnt.

Die Arrays für Account-Indizes und Daten haben eine variable Länge. compact-u16-Präfixe definieren ihre Größe.

Spezifikation von Transaktion v0

Transaktion v0 behält fast das gesamte Legacy-Wire-Format bei. Die Transaktion beginnt weiterhin mit der Signaturanzahl und den Signaturen. Die Nachricht beginnt jedoch mit dem Versionspräfix 0x80.

Danach verwendet die Nachricht denselben Drei-Byte-Header, dieselbe statische Account-Liste, denselben Blockhash und dasselbe Format für kompilierte Anweisungen wie Legacy. Anschließend wird ein Abschnitt für Address Lookup Tables angehängt.

So können v0-Transaktionen über ALTs zusätzliche Accounts laden und zugleich innerhalb des Transaktionslimits von 1.232 Byte bleiben.

Code
NumSignatures (compact-u16)

Signatures [[u8; 64]]
  -- Length = NumSignatures

VersionByte (u8)
  -- 0x80

MessageHeader (u8, u8, u8)

NumStaticAccountKeys (compact-u16)

StaticAccountKeys [[u8; 32]]
  -- Length = NumStaticAccountKeys

RecentBlockhash [u8; 32]

NumInstructions (compact-u16)

Instructions [CompiledInstruction]
  -- Same encoding as legacy

NumAddressTableLookups (compact-u16)

AddressTableLookups [MessageAddressTableLookup]
  -- Length = NumAddressTableLookups

MessageAddressTableLookup:
  AccountKey [u8; 32]
    -- Address of the lookup table account

  NumWritableIndexes (compact-u16)

  WritableIndexes [u8]
    -- Indices into the lookup table

  NumReadonlyIndexes (compact-u16)

  ReadonlyIndexes [u8]
    -- Indices into the lookup table

Die Unterscheidung zwischen statischen Account-Schlüsseln und geladenen Adressen ist wichtig. Nur statische Schlüssel werden direkt in der Nachricht serialisiert. Über ALTs referenzierte Adressen werden zur Laufzeit aufgelöst und an die effektive Account-Liste angehängt.

Bei einer v0-Transaktion mit ALTs muss der Validator zunächst jede referenzierte Lookup Table aus Bank und AccountsDB laden. Dann prüft er, ob der Account eine gültige Address Lookup Table ist, deserialisiert ihren Zustand, löst die angeforderten Indizes in vollständige 32-Byte-Adressen auf und führt diese Adressen mit den statischen Account-Schlüsseln der Transaktion zusammen. Erst danach verfügt der Validator über die vollständige Account-Liste, mit der er Lese- und Schreibsperren sowie Konflikte mit anderen Transaktionen ermitteln kann.

Spezifikation von Transaktion v1

Eine serialisierte v1-Transaktion beginnt mit dem Versionsbyte 0x81. Danach folgen ein Drei-Byte-Nachrichtenheader im Legacy-Stil, eine 32-Bit-Maske für die Transaktionskonfiguration, eine 32 Byte große Gültigkeitsangabe, die Anzahl der Anweisungen und Adressen, das vollständige Array der 32-Byte-Adressen, Konfigurationswerte, Anweisungsheader fester Größe, zusammenhängende Anweisungsnutzlasten und schließlich die Signaturen.

Code
VersionByte (u8)
  -- 0x81

LegacyHeader (u8, u8, u8)

TransactionConfigMask (u32)
  -- Bitmask describing which configuration values are present

LifetimeSpecifier [u8; 32]

NumInstructions (u8)

NumAddresses (u8)

Addresses [[u8; 32]]
  -- Length = NumAddresses

ConfigValues [[u8; 4]]
  -- Length = popcount(TransactionConfigMask)
  -- Multi-word values, such as priority fee, consume multiple entries

InstructionHeaders [(u8, u8, u16)]
  -- Length = NumInstructions
  -- (ProgramAccountIndex,
      NumInstructionAccounts,
      NumInstructionDataBytes)

InstructionPayloads [InstructionPayload]
  -- Length = NumInstructions

InstructionPayload:
  InstructionAccountIndexes [u8]
    -- Length = NumInstructionAccounts

  InstructionData [u8]
    -- Length = NumInstructionDataBytes

Signatures [[u8; 64]]
  -- Length = LegacyHeader.NumRequiredSignatures

v1 unterscheidet sich daher in mehreren grundlegenden Punkten von den früheren Formaten. Die Versionskennung steht bei Byte null. Signaturen wandern ans Ende und benötigen kein eigenes Längenpräfix mehr. Die Anzahl der Accounts und Anweisungen wird zu u8-Werten fester Breite. Die Ressourcenkonfiguration wird direkt in der Transaktion codiert. Außerdem werden Anweisungsheader fester Größe von ihren Nutzlasten variabler Länge getrennt. Anders als Legacy und v0 verwendet v1 keine Address Lookup Tables.

Transaktion v1 ersetzt Konfigurationsanweisungen für das Compute Budget Program durch Felder im Transaktionsheader. Die anfängliche TransactionConfigMask kann Folgendes für die Transaktion deklarieren:

  • gesamte Priority Fee in Lamports
  • Compute-Unit-Limit
  • Größenlimit für Daten geladener Accounts
  • angeforderte Heap-Größe

Jedes gesetzte Bit kennzeichnet einen zugehörigen Vier-Byte-Konfigurationswert. Die 64-Bit-Priority-Fee belegt zwei Maskenpositionen. Die Maske ist so ausgelegt, dass sie in zukünftigen Transaktionsversionen erweitert werden kann.

Beispieltransaktion

Um die verschiedenen Transaktionsformate von Solana zu vergleichen, betrachten wir das einfachste nützliche Beispiel: die Übertragung von SOL von einer Adresse an eine andere.

Unsere Beispieltransaktion hat einen einzigen Signierer, also den Absender, und ruft das System Program einmal auf, um 1.000 Lamports an den Empfänger zu übertragen. Sie verweist auf drei Adressen: den Absender, den Empfänger und das System Program.

Legacy- und v0-Transaktionen

Legacy- und v0-Transaktionen verwenden nahezu dasselbe Layout auf oberster Ebene. Beide beginnen mit der Anzahl der Signaturen, unmittelbar gefolgt von den Signaturen selbst. Danach folgt die Nachricht der Transaktion. Bei einer Übertragung mit einem Signierer lautet das erste Byte 01 und gibt eine einzelne Signatur an. Darauf folgt die 64 Byte große Ed25519-Signatur des Absenders.

Die Nachricht enthält einen Drei-Byte-Header, Account-Adressen, einen aktuellen Blockhash als Gültigkeitsangabe und kompilierte Anweisungen.

Der Absender ist Adresse 0, der Empfänger Adresse 1 und das System Program Adresse 2. Die Übertragungsanweisung verweist auf das Programm an Index 2 und übergibt dem Programm die Account-Indizes 00 01.

Die Formate Legacy und v0 unterscheiden sich nur geringfügig:

  • Eine Legacy-Nachricht beginnt direkt mit ihrem Drei-Byte-Nachrichtenheader. Eine v0-Nachricht beginnt dagegen mit dem Versionsbyte 0x80, gefolgt vom Header.
  • v0 hängt nach den Anweisungen außerdem einen Abschnitt für Address Lookup Tables an. Unser einfaches Beispiel verwendet keine Lookup Table. Dieser Abschnitt besteht daher aus einem einzelnen Byte 00, das null Lookups angibt.

Dadurch ist unsere beispielhafte SOL-Übertragung als Legacy-Transaktion 215 Byte und als v0-Transaktion 217 Byte groß. v0 ergänzt 1 Byte für das Versionspräfix und 1 Byte für das leere Lookup-Table-Array. Der Rest der Transaktion ist identisch.

Der Absender signiert nur die Nachricht. In unserem Legacy-Beispiel deckt die Signatur somit die Bytes 65–214 ab. Bei v0 deckt sie die Bytes 65–216 ab, beginnend mit dem Nachrichtenversionspräfix 0x80.

Die folgende Tabelle zeigt das Byte-Layout unserer beispielhaften SOL-Übertragung zwischen zwei Adressen im v0-Transaktionsformat.

OffsetGrößeFeldBeispielwertBeschreibung
01 BSignaturanzahl01Eine Signatur; compact-u16
1–6464 BSignatur 0<Ed25519 signature>Signatur des Absenders
651 BVersionspräfix80Versionierte Nachricht, Version 0
66–683 BNachrichtenheader01 00 011 erforderlicher Signierer, 0 schreibgeschützte signierte, 1 schreibgeschützter unsignierter Account
691 BAnzahl statischer Accounts03Drei Inline-Accounts
70–10132 BAccount 0<sender pubkey>Absender/Gebührenzahler; schreibbarer Signierer
102–13332 BAccount 1<recipient pubkey>Empfänger; schreibbar, unsigniert
134–16532 BAccount 211111111111111111111111111111111System Program; schreibgeschützt, unsigniert
166–19732 BAktueller Blockhash<recent blockhash>Gültigkeitsdauer der Transaktion
1981 BAnweisungsanzahl01Eine Anweisung
1991 BProgramm-ID-Index02System Program
2001 BAnzahl der Anweisungs-Accounts02Zwei Anweisungs-Accounts
201–2022 BAccount-Indizes00 01Absender, Empfänger
2031 BDatenlänge0C12 Byte
204–2074 BÜbertragungsdiskriminator02 00 00 00SystemInstruction::Transfer
208–2158 BLamportsE8 03 00 00 00 00 00 001.000 Lamports
2161 BAnzahl der Address-Table-Lookups00Keine ALT-Lookups
Gesamt217 B

Ressourcenkonfigurationen

In Legacy- und v0-Transaktionen werden Ressourcenkonfigurationen als Anweisungen an das Compute Budget Program ausgedrückt. Häufig verwendet werden SetComputeUnitLimit und SetComputeUnitPrice. Bei Bedarf kommen auch SetLoadedAccountsDataSizeLimit und RequestHeapFrame zum Einsatz.

In unserem minimalen Transaktionsbeispiel fehlen diese Anweisungen. Stattdessen stellt die Runtime Standardwerte bereit.

Ohne Compute-Unit-Preis gibt es keine Priority Fee. Das Limit für Daten geladener Accounts entspricht standardmäßig dem Runtime-Maximum.

Dass Validatoren beim Einlesen einer Transaktion die Anweisungsliste nach Compute-Budget-Anweisungen durchsuchen müssen, ist einer der Gründe für das aktualisierte v1-Format. Dort werden diese Informationen stattdessen in TransactionConfigMask und ConfigValues abgelegt.

Die Darstellung einer Konfiguration auf Transaktionsebene als Anweisungen verursacht außerdem grundsätzlich Overhead. Legacy und v0 hatten keine eigenen Felder für angeforderte Compute-Ressourcen oder Priority Fees. Daher wurden diese Einstellungen nachträglich über das Compute Budget Program integriert: Die Transaktion muss auf dessen Programm-ID verweisen, jede Einstellung belegt Platz in der Anweisungsliste und der Programmaufruf selbst erledigt keine Anwendungsarbeit, verbraucht aber dennoch Compute-Ressourcen.

v1 gibt diesen Werten stattdessen einen nativen Platz im Wire-Format. Das vermeidet den zusätzlichen Anweisungs-Overhead und macht die Ressourcenkonfiguration günstiger und für Validatoren leichter prüfbar.

Transaktion v1

Statt mit einer Signaturanzahl und den Signaturen zu beginnen, startet eine v1-Transaktion direkt mit dem Versionsbyte 0x81.

Danach folgen der Drei-Byte-Header, eine neue vier Byte große TransactionConfigMask, die Gültigkeitsangabe — normalerweise ein Blockhash, aber auch eine Nonce ist möglich —, die Anzahl der Anweisungen und Adressen, Account-Adressen, Konfigurationswerte, Anweisungen und schließlich die Signaturen.

OffsetGrößeFeldBeispielwertBeschreibung
01 BVersionsbyte81v1-Transaktion
1–33 BLegacy-Header01 00 011 erforderliche Signatur, 0 schreibgeschützte signierte Accounts, 1 schreibgeschützter unsignierter Account
4–74 BMaske der Transaktionskonfiguration0C 00 00 000x0000000C: Bits 2 und 3 gesetzt, gibt zwei 4-Byte-Konfigurationswerte an
8–3932 BGültigkeitsangabe<recent blockhash>Aktueller Blockhash (oder Nonce)
401 BAnzahl der Anweisungen011 Anweisung des System Program
411 BAnzahl der Adressen03Absender, Empfänger, System Program
42–7332 BAdresse 0<sender pubkey>Absender, Gebührenzahler, schreibbarer Signierer
74–10532 BAdresse 1<recipient pubkey>Empfänger, schreibbarer unsignierter Account
106–13732 BAdresse 211111111111111111111111111111111System Program, schreibgeschützt, unsigniert
138–1414 BKonfigurationswert: Compute-Unit-Limit10 27 00 0010.000 CU als Little-Endian-u32 (entspricht Maskenbit 2)
142–1454 BKonfigurationswert: Größenlimit für Daten geladener Accounts00 00 01 0065.536 Byte / 64 KiB als Little-Endian-u32 (entspricht Maskenbit 3)
146–1494 BAnweisung: Header02 02 0C 00Programmindex 2, 2 Accounts, 12 Byte Daten
150–1512 BAnweisung: Account-Indizes00 01Adresse 0 = Absender, Adresse 1 = Empfänger
152–1554 BAnweisung: Übertragungsdiskriminator02 00 00 00SystemInstruction::Transfer
156–1638 BAnweisung: Lamportsz. B. E8 03 00 00 00 00 00 001.000 Lamports als Little-Endian-u64
164–22764 BSignatur 0<Ed25519 signature>Signatur des Absenders über die Bytes 0–155
Gesamt228 B

Signaturprüfung

Die Signaturprüfung ist eine der ersten Operationen, wenn eine Transaktion einen Validator erreicht. Transaktionen mit ungültigen Signaturen müssen herausgefiltert und verworfen werden, bevor sie den Scheduler erreichen. Signaturen hinter einer Nachricht variabler Länge zu platzieren scheint den Zugriff zu erschweren. In der Praxis ist das jedoch nicht der Fall.

Vor der aufwendigen Ed25519-Prüfung muss der Validator lediglich erkennen, wo die einzelnen Teile der Transaktion beginnen und enden. Das v1-Format macht dies günstig und deterministisch.

Das erste Byte, 0x81, kennzeichnet die Transaktion sofort als v1. Der folgende Header enthält num_required_signatures. Der Validator weiß daher von Anfang an, wie viele 64-Byte-Signaturen er finden muss.

Der Parser TransactionView von Agave durchläuft die Transaktion anschließend vorwärts und zeichnet dabei Offsets auf. Er liest die Felder fester Größe und verwendet NumAddresses, um das Adress-Array zu überspringen.

Anhand der Konfigurationsmaske bestimmt er die Größe des Konfigurationsabschnitts. Außerdem liest er jeden 4-Byte-Anweisungsheader, um die Größe der zugehörigen Anweisungsnutzlast zu ermitteln.

Sobald der Parser die letzte Anweisungsnutzlast passiert hat, wird seine aktuelle Position als Signatur-Offset gespeichert. Ab dieser Position erwartet Agave genau num_required_signatures × 64 Byte.

Die Implementierung speichert einen SignatureFrame, der die Anzahl der Signaturen und den Byte-Offset der ersten Signatur im Paket enthält. Die signierte Nachricht der Transaktion ist der Byte-Bereich vom Anfang der v1-Nachricht bis zum gespeicherten Signatur-Offset. So kann der Validator die aufwendige Ed25519-Prüfung durchführen, ohne die Transaktion auszuführen oder Accounts zu laden.

v1 verbietet außerdem nachgestellte Bytes hinter dem Signatur-Array. Sobald der Parser die Signaturen erreicht, haben die verbleibenden Bytes eine eindeutige, von der Signaturanzahl bestimmte Größe. SIMD-0385 verlangt für jeden erforderlichen Signierer genau eine 64-Byte-Signatur und keine Daten nach der letzten Signatur.

Verbessertes Parsen von Anweisungen

Das Layout von Transaktion v1 erleichtert das Parsen von Anweisungen. Um eine spätere Anweisung in Legacy- und v0-Transaktionen zu finden, muss ein Parser die vorherigen Anweisungen durchlaufen, ihre Längen decodieren und jede Nutzlast variabler Länge überspringen.

v1 trennt Anweisungsheader fester Breite von Nutzlasten variabler Länge. Jede Anweisung fügt zunächst einen Vier-Byte-Header hinzu. Dieser enthält den Index des Programm-Accounts, die Anzahl der Anweisungs-Accounts und die Länge der Anweisungsdaten. Diese Header werden vor den Account-Index- und Datennutzlasten gruppiert. So steht vorab eine kompakte Beschreibung jeder Anweisung bereit. Dadurch lassen sich der Anweisungsabschnitt günstiger erfassen und überspringen sowie der Offset des nachgestellten Signatur-Arrays bestimmen, ohne Anweisungsdaten zu parsen.

Die Maske der Transaktionskonfiguration

Die andere wichtige Ergänzung des v1-Formats ist die vier Byte große TransactionConfigMask.

Legacy- und v0-Transaktionen konfigurieren Ressourcen über Anweisungen an das Compute Budget Program. Eine Transaktion kann beispielsweise SetComputeUnitLimit-, SetComputeUnitPrice- oder SetLoadedAccountsDataSizeLimit-Anweisungen enthalten. Ein Validator, der die Ressourcenanforderungen oder Priorität der Transaktion ermitteln möchte, muss diese Anweisungen finden und interpretieren. Transaktion v1 verschiebt diese Informationen aus dem Anweisungsstream direkt in das Transaktionsformat.

Die Maske ist ein 32-Bit-Bitfeld im Little-Endian-Format. Jedes gesetzte Bit entspricht einem Vier-Byte-Wort im späteren Abschnitt ConfigValues der Transaktion:

  • Bits 0 und 1: gesamte Priority Fee in Lamports (codiert als 8-Byte-Little-Endian-u64)
  • Bit 2: Compute-Unit-Limit (ein 4-Byte-u32)
  • Bit 3: Größenlimit für Daten geladener Accounts (ein 4-Byte-u32)
  • Bit 4: angeforderte Heap-Größe (ein 4-Byte-u32)

Hinweis: Die anfängliche v1-Spezifikation weist nur den Bits 0–4 Bedeutungen zu. Die übrigen Bits sind derzeit nicht belegt. So bleibt Platz für zukünftige Konfigurationsfelder auf Transaktionsebene.

Die Maske dient als kompaktes Schema. Sie teilt dem Parser mit, welche Felder vorhanden sind und wie viele Bytes er erwarten muss. Unsere einfache SOL-Übertragung benötigt beispielsweise ein Compute-Unit-Limit und ein Größenlimit für Daten geladener Accounts, aber weder eine Priority Fee noch eine benutzerdefinierte Heap-Größe.

Da diese beiden Bits gesetzt sind, folgen auf das Adress-Array der Transaktion genau zwei 4-Byte-Konfigurationswerte. In unserem Fall fordern sie ein Limit von 20.000 CU und ein Limit von 64 KiB für Daten geladener Accounts an.

Die Maske teilt dem Validator mit, dass der erste 4-Byte-Konfigurationswert zu Bit 2, dem Compute-Unit-Limit, und der zweite zu Bit 3, dem Größenlimit für Daten geladener Accounts, gehört.

Wenn eine Anweisung für das Compute-Unit-Limit in Legacy- und v0-Transaktionen fehlt, erhält die Transaktion ein implizites Compute-Budget. v1 verwendet nicht dieselben Standardwerte. Ein nicht festgelegtes Compute-Unit-Limit wird ebenso wie ein nicht festgelegtes Größenlimit für Daten geladener Accounts als null interpretiert. Nur der Heap behält einen Standardwert von 32 KiB. Eine v1-Transaktion muss die benötigten Ressourcen daher ausdrücklich anfordern.

Wenn diese Einstellungen in einen klar definierten Konfigurationsbereich verschoben werden, erhalten Validatoren beim Einlesen der Transaktion direkten Zugriff auf die benötigten Informationen und müssen die Anweisungen nicht mehr durchsuchen. Das bedeutet auch, dass Anweisungen des Compute Budget Program v1-Transaktionen nicht mehr konfigurieren. Falls sie enthalten sind, werden sie ignoriert und als No-op-Anweisungen verarbeitet, verbrauchen aber weiterhin Compute Units.

Das spart zusätzlich Platz. Wenn du Legacy-/v0-Transaktionen Compute-Budget-Anweisungen hinzufügst, benötigst du in der Regel die 32 Byte große Adresse des Compute Budget Program in der Account-Liste sowie die serialisierten Anweisungen selbst.

Fazit

Die drei Transaktionsformate von Solana spiegeln die Entwicklung des Netzwerks wider. Legacy etablierte das ursprüngliche Modell. v0 erweiterte es mit ALTs, um innerhalb der Obergrenze von 1.232 Byte größere Account-Gruppen zu unterstützen. v1 denkt das Wire-Format grundlegend neu: mit größeren Transaktionen, einfacherem Parsing, Inline-Adressen und nativer Ressourcenkonfiguration.

Alle drei bleiben gültig, repräsentieren aber jeweils eine andere Phase der Solana-Entwicklung. Zusammen zeigen sie, wie sich das Transaktionsformat an die wachsenden Anforderungen der Anwendungen, Gebührenmärkte und Validatoren im Netzwerk angepasst hat.

Weitere Ressourcen

Helius abonnieren

Bleib bei der Solana-Entwicklung auf dem Laufenden und erhalte Updates, wenn wir neue Beiträge veröffentlichen

Vergrößertes Bild