
Solana Transaction Versioning: Legacy, v0 and v1
Table of Contents
- A Brief History of Solana's Transaction Formats
- Legacy Transactions
- Transaction v0
- Retrofitting Priority Fees
- Larger Transaction Sizes
- Format Specs and Comparison
- Legacy Transaction Specification
- Transaction v0 Specification
- Transaction v1 Specification
- Example Transaction
- Legacy and v0 Transactions
- Resource Configurations
- Transaction v1
- Signature Verification
- Improved Instruction Parsing
- The Transaction Config Mask
- Conclusion
- Further Resources
A Brief History of Solana's Transaction Formats
Solana has three transaction formats: legacy, v0, and v1. At a high level, they all serve the same purpose. That is, they package signatures, accounts, and instructions for validators to execute against onchain programs.
What has changed over time is the wire format, meaning how those pieces of information are serialized and transmitted over the network.
Each new format was introduced to address limitations that emerged as Solana applications became more complex, while preserving backward compatibility so older formats could keep working.
All three formats coexist on the network, and developers can use whichever format best fits the transaction they are building.
Legacy Transactions
The original format is known as legacy because it predates transaction versioning and contains no version identifier.
Its 1,232-byte maximum size dates back to Solana's original networking design: transactions were sized to fit within the 1,280-byte IPv6 minimum MTU, leaving 1,232 bytes after network overhead.
This worked well for small transactions, but space became increasingly restrictive as applications grew. Every account address included directly in a transaction occupies 32 bytes, while every Ed25519 signature occupies another 64 bytes.
In practice, account-heavy transactions could therefore exhaust the byte limit well before reaching the runtime's account-lock limit.
Transaction v0
The first attempt to relieve this pressure was transaction v0, which went live on mainnet-beta in October 2022. Rather than increasing the 1,232-byte limit, v0 introduced Address Lookup Tables (ALTs).
An ALT stores account addresses onchain, allowing a v0 transaction to reference them using one-byte indices instead of repeating each 32-byte public key. This lets transactions reach the runtime's 64-account limit while staying within the existing packet-size constraint.
v0 was an incremental extension of the legacy format: it added the 0x80 version prefix and an address-lookup section while retaining the same basic message and instruction encoding.
The introduction of versioning also established a framework in which new transaction formats could coexist with legacy transactions rather than replacing them.
ALTs solved the immediate account-size problem, but introduced new complexities of their own. Applications need to create and maintain lookup tables onchain, validators must resolve their entries before execution, and RPC services and indexers need the resolved addresses to reconstruct the complete transaction.
Historical decoding also becomes much more difficult if the lookup table referenced by a raw transaction has since been closed. v0 therefore compressed the account list effectively, but did so by introducing an external onchain dependency into the transaction's wire representation.
ALTs became heavily adopted by Solana developers. Research conducted shortly before the introduction of v1 transactions found that around 62% of v0 transactions reference at least one ALT.
Retrofitting Priority Fees
Priority fees were introduced on Solana also in 2022, before v0 transactions were activated on mainnet, although the v0 format itself was already designed by that point. Rather than redesigning either transaction format, Solana added fee configuration through the existing Compute Budget Program. Transactions could include instructions such as SetComputeUnitLimit and SetComputeUnitPrice, allowing users to request a compute budget and attach an additional fee for scheduling priority. This worked equally well for legacy and v0 transactions while avoiding a separate fee mechanism for each format.
This was a pragmatic, backward-compatible solution, but not an especially elegant one. Priority fees and compute limits are really transaction-level metadata, yet legacy and v0 had no dedicated fields for them, so they were retrofitted into the instruction stream as program calls. That means the Compute Budget Program must be referenced by the transaction, the configuration consumes instruction bytes, and validators have to inspect those instructions during transaction ingestion to recover the fee and resource settings.
v0 was therefore not a broad redesign of Solana's transaction format. Its main objective was to solve the account-address bottleneck through Address Lookup Tables, while leaving most of the legacy message structure intact. Since Compute Budget instructions already provided a compatible way to support priority fees across both formats, there was little reason to expand v0's scope further.
Larger Transaction Sizes
Over time, Solana transitioned its transaction ingestion from UDP to QUIC, whose streams are no longer constrained to a single MTU-sized payload. The original 1,232-byte ceiling was therefore no longer a transport requirement. Two SIMDs were put forward in 2025 to introduce a new v1 format. SIMD-0296 raised the maximum size of a v1 transaction to 4,096 bytes, roughly 3.3× the previous limit, while SIMD-0385 defined an entirely new wire format designed around that additional space and more efficient validator ingestion.
The additional space allows v1 to eliminate ALTs entirely. The 64-account limit of 32-byte addresses requires at most 2,048 bytes, so the complete account list can simply be carried inline.
Solana Foundation analysis suggests the impact of inlining all addresses is moderate when upgrading most existing v0 transactions: 50% grow by less than 420 bytes, and 90% by less than 1,400 bytes, leaving substantial headroom within v1's 4,096-byte limit even for transactions that make heavy use of ALTs.
More importantly, v1 is not merely a larger v0 transaction. It substantially reorganizes the wire format: signatures move to the end, resource configuration moves out of Compute Budget instructions and into a transaction config section, and variable-length instruction payloads are separated from fixed-width headers. These changes make transactions cheaper and simpler for validators to parse and prioritize.
The larger 4,096-byte limit also unlocks workloads for which address compression alone could never help. Zero-knowledge proofs, nested multisigs, untruncated Winternitz signatures, BLS signatures, and other cryptographic operations can require hundreds or thousands of bytes of instruction data. Token-2022 Confidential Balances, for example, can now be constructed as a single atomic v1 transaction rather than split across several transactions.
Notably, the initial v1 format does not raise Solana's compute, signature, instruction-count, or 64-account limits; it primarily removes the size bottleneck and redesigns how the transaction is represented on the wire.
SIMD-0596, proposed by Anza, would raise the v1 account-lock limit from 64 to 96, covering all accounts referenced by the transaction, including signers, program IDs, and writable and read-only accounts. This proposal remains under review at the time of writing. Legacy and v0 transactions would remain capped at 64.
Format Specs and Comparison
| Limit | Legacy | v0 | v1 |
| Max size | 1,232 bytes | 1,232 bytes | 4,096 bytes |
| Account addresses | ~32, size-bound | 64, via lookup tables | 64, inline |
| Address lookup tables | Not supported | Supported | Not supported |
| Duplicate addresses | allowed | allowed | rejected |
| Prefix | None | 0x80 | 0x81 |
| Priority fee | SetComputeUnitPrice instruction, micro-lamports per CU | SetComputeUnitPrice instruction, micro-lamports per CU | config.priorityFeeLamports, total lamports |
| Compute unit limit | SetComputeUnitLimit instruction | SetComputeUnitLimit instruction | config.computeUnitLimit |
| Heap size | RequestHeapFrame instruction | RequestHeapFrame instruction | config.heapSize |
| Loaded accounts cap | SetLoadedAccountsDataSizeLimit instruction | SetLoadedAccountsDataSizeLimit instruction | config.loadedAccountsDataSizeLimit |
Legacy Transaction Specification
A serialized legacy transaction begins with a compact-u16 signature count, followed by the 64-byte Ed25519 signatures.
The message comes next and contains the three-byte header, compact-length-prefixed account list, 32-byte recent blockhash, and compact-length-prefixed array of compiled instructions.
Legacy transactions have no version prefix.
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 = InstructionDataLengthA key characteristic is that each CompiledInstruction is serialized in full before the next instruction begins.
The account-index and data arrays are variable length, with compact-u16 prefixes defining their sizes.
Transaction v0 Specification
Transaction v0 retains almost all of the legacy wire format. The transaction still begins with the signature count and signatures, but the message begins with the version prefix 0x80.
The message then uses the same three-byte header, static account list, blockhash, and compiled-instruction format as legacy before appending an Address Lookup Table section.
This is what allows v0 transactions to load additional accounts through ALTs while remaining within the 1,232-byte transaction limit.
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 tableThe distinction between static account keys and loaded addresses is important. Only static keys are serialized directly into the message; addresses referenced through ALTs are resolved at runtime and appended to the effective account list.
For a v0 transaction that uses ALTs, the validator must first load each referenced lookup table from the Bank and AccountsDB, verify that the account is a valid Address Lookup Table, deserialize its state, resolve the requested indices into full 32-byte addresses, and then merge those addresses with the transaction's static account keys. Only after this resolution step does the validator have the full account list needed to determine read/write locks and conflicts with other transactions.
Transaction v1 Specification
A serialized v1 transaction begins with the version byte 0x81. It is followed by a three-byte legacy-style message header, a 32-bit transaction configuration mask, a 32-byte lifetime specifier, instruction and address counts, the complete array of 32-byte addresses, configuration values, fixed-size instruction headers, contiguous instruction payloads, and finally the signatures.
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.NumRequiredSignaturesV1 therefore differs from the earlier formats in several fundamental ways. The version identifier is at byte zero, signatures move to the end and no longer need their own length prefix, account and instruction counts become fixed-width u8 values, resource configuration is encoded directly in the transaction, and fixed-size instruction headers are separated from their variable-length payloads. Unlike legacy and v0, v1 does not use Address Lookup Tables.
Transaction v1 replaces Compute Budget program configuration instructions with fields in the transaction header. The initial TransactionConfigMask can declare the transaction's:
- total priority fee in lamports
- compute-unit limit
- loaded-account-data-size limit
- requested heap size
Each set bit identifies an accompanying four-byte configuration value, with the 64-bit priority fee occupying two mask positions. The mask is designed to be extendable in future transaction versions.
Example Transaction
To contrast Solana's various transaction formats, we'll consider the simplest useful example: transferring SOL from one address to another.
Our sample transaction has a single signer (i.e., the sender) and invokes the System Program once to transfer 1,000 lamports to the recipient. It references three addresses: the sender, the recipient, and the System Program.
Legacy and v0 Transactions
Legacy and v0 transactions use almost the same top-level layout. Both begin with the number of signatures, followed immediately by the signatures themselves. The transaction's message comes afterward. For a single-signer transfer, the first byte is 01, specifying a single signature, followed by the sender's 64-byte Ed25519 signature.
The message contains a three-byte header, account addresses, a recent blockhash (the lifetime specifier), and compiled instructions.
The sender is address 0, the recipient is address 1, and the System Program is address 2. The transfer instruction references the program at index 2 and passes account indices 00 01 to the program.
Legacy and v0 formats differ only slightly:
- A legacy message begins directly with its three-byte message header, whereas a v0 message starts with the version byte
0x80, followed by the header. - v0 also appends an Address Lookup Table section after the instructions. Our simple example does not use a lookup table, so this section consists of a single
00byte indicating zero lookups.
This makes our example SOL transfer transaction 215 bytes as a legacy transaction and 217 bytes as v0. v0 adds 1 byte for the version prefix and 1 byte for the empty lookup-table array. The rest of the transaction is identical.
The sender signs only the message. In our legacy example, that means the signature covers bytes 65–214; for v0, it covers bytes 65–216, beginning with the 0x80 message-version prefix.
Below is a table detailing the byte layout for our example SOL transfer between two addresses, using the v0 transaction format.
| Offset | Size | Field | Example value | Description |
| 0 | 1 B | Signature count | 01 | One signature; compact-u16 |
| 1–64 | 64 B | Signature 0 | <Ed25519 signature> | Sender's signature |
| 65 | 1 B | Version prefix | 80 | Versioned message, version 0 |
| 66–68 | 3 B | Message header | 01 00 01 | 1 required signer, 0 read-only signed, 1 read-only unsigned |
| 69 | 1 B | Static account count | 03 | Three inline accounts |
| 70–101 | 32 B | Account 0 | <sender pubkey> | Sender/fee payer; writable signer |
| 102–133 | 32 B | Account 1 | <recipient pubkey> | Recipient; writable unsigned |
| 134–165 | 32 B | Account 2 | 11111111111111111111111111111111 | System Program; read-only unsigned |
| 166–197 | 32 B | Recent blockhash | <recent blockhash> | Transaction lifetime |
| 198 | 1 B | Instruction count | 01 | One instruction |
| 199 | 1 B | Program ID index | 02 | System Program |
| 200 | 1 B | Instruction account count | 02 | Two instruction accounts |
| 201–202 | 2 B | Account indices | 00 01 | Sender, recipient |
| 203 | 1 B | Data length | 0C | 12 bytes |
| 204–207 | 4 B | Transfer discriminator | 02 00 00 00 | SystemInstruction::Transfer |
| 208–215 | 8 B | Lamports | E8 03 00 00 00 00 00 00 | 1,000 lamports |
| 216 | 1 B | Address table lookup count | 00 | No ALT lookups |
| Total | 217 B |
Resource Configurations
In legacy and v0 transactions, resource configurations are expressed as instructions to the Compute Budget Program. Common ones include SetComputeUnitLimit and SetComputeUnitPrice. SetLoadedAccountsDataSizeLimit and RequestHeapFrame are also used when needed.
In our minimal transaction example, those instructions are absent. The runtime supplies defaults instead.
No compute-unit price means no priority fee, and the loaded-account-data limit defaults to the runtime maximum.
The inefficiency of validators having to scan the instruction list for Compute Budget instructions during transaction ingestion is one of the motivations for the updated v1 format where this information is moved into the TransactionConfigMask and ConfigValues instead.
There is also some inherent overhead in representing transaction-level configuration as instructions in the first place. Legacy and v0 had no dedicated fields for requested compute or priority fees, so these settings were retrofitted through the Compute Budget Program: its program ID must be referenced by the transaction, each setting consumes space in the instruction list, and the program invocation itself performs no application work while still consuming compute.
v1 gives these values a native place in the wire format instead, avoiding that extra instruction overhead and making resource configuration cheaper and easier for validators to inspect.
Transaction v1
Instead of beginning with a signature count and signatures, a v1 transaction begins directly with the version byte 0x81.
The three-byte header comes next, followed by a new four-byte TransactionConfigMask, the lifetime specifier (usually a blockhash, but could also be a nonce), instruction and address counts, account addresses, configuration values, instructions, and finally the signatures.
| Offset | Size | Field | Example value | Description |
| 0 | 1 B | Version byte | 81 | v1 transaction |
| 1–3 | 3 B | Legacy header | 01 00 01 | 1 required signature, 0 read-only signed accounts, 1 read-only unsigned account |
| 4–7 | 4 B | Transaction config mask | 0C 00 00 00 | 0x0000000C: bits 2 and 3 set, specifying two 4-byte config values |
| 8–39 | 32 B | Lifetime specifier | <recent blockhash> | Recent blockhash (or nonce) |
| 40 | 1 B | Number of instructions | 01 | 1 System Program instruction |
| 41 | 1 B | Number of addresses | 03 | Sender, recipient, System Program |
| 42–73 | 32 B | Address 0 | <sender pubkey> | Sender, fee payer, writable signer |
| 74–105 | 32 B | Address 1 | <recipient pubkey> | Recipient, writable unsigned account |
| 106–137 | 32 B | Address 2 | 11111111111111111111111111111111 | System Program, read-only, unsigned |
| 138–141 | 4 B | Config value: Compute-unit limit | 10 27 00 00 | 10,000 CU as little-endian u32 (corresponds to mask bit 2) |
| 142–145 | 4 B | Config value: Loaded accounts data size limit | 00 00 01 00 | 65,536 bytes / 64 KiB as little-endian u32 (corresponds to mask bit 3) |
| 146–149 | 4 B | Instruction: Header | 02 02 0C 00 | Program index 2, 2 accounts, 12 bytes of data |
| 150–151 | 2 B | Instruction: Account indices | 00 01 | Address 0 = sender, Address 1 = recipient |
| 152–155 | 4 B | Instruction: Transfer discriminator | 02 00 00 00 | SystemInstruction::Transfer |
| 156–163 | 8 B | Instruction: Lamports | e.g. E8 03 00 00 00 00 00 00 | 1,000 lamports as little-endian u64 |
| 164–227 | 64 B | Signature 0 | <Ed25519 signature> | Sender's signature over bytes 0–155 |
| Total | 228 B |
Signature Verification
Signature verification is one of the first operations performed when a transaction reaches a validator. Transactions with invalid signatures need to be filtered and dropped before reaching the scheduler. Adding signatures behind a variable-length message would seem to make them more difficult to access, yet in practice, it does not.
Before performing the expensive Ed25519 verification, the validator only needs to identify where each part of the transaction begins and ends. The v1 format is designed to make this cheap and deterministic.
The first byte, 0x81, immediately identifies the transaction as v1. The following header contains num_required_signatures, so the validator knows from the beginning how many 64-byte signatures it should find.
Agave's TransactionView parser then walks forward through the transaction while recording offsets. It reads the fixed-size fields and uses NumAddresses to skip past the address array.
It uses the config mask to determine the size of the configuration section, and reads each 4-byte instruction header to learn the size of the corresponding instruction payload.
Once the parser has advanced past the final instruction payload, its current position is recorded as the signature offset. From that position onward, Agave expects exactly num_required_signatures × 64 bytes.
The implementation stores a SignatureFrame containing the number of signatures and the byte offset of the first signature in the packet. The transaction's signed message is the byte range from the beginning of the v1 message up to the recorded signature offset. Thus, the validator can perform the expensive Ed25519 verification without executing the transaction or loading accounts.
v1 also forbids trailing bytes after the signature array. Once the parser reaches the signatures, the remaining bytes have an unambiguous size determined by the signature count. SIMD-0385 requires exactly one 64-byte signature for each required signer and no data after the final signature.
Improved Instruction Parsing
Transaction v1's layout makes instructions easier to parse. To locate a later instruction in legacy and v0 transactions, a parser must walk through the preceding instructions, decode their lengths, and skip over each variable-length payload.
v1 separates fixed-width instruction headers from the variable-length payloads. Each instruction first contributes a four-byte header containing the program account index, number of instruction accounts, and instruction data length. These headers are grouped together before the account-index and data payloads. This provides a compact description of every instruction up front. It makes it cheaper to frame and skip the instruction section and helps them determine the offset of the trailing signature array without parsing instruction data.
The Transaction Config Mask
The other major addition of the v1 format is the four-byte TransactionConfigMask.
Legacy and v0 transactions configure resources through instructions to the Compute Budget Program. For example, a transaction may contain SetComputeUnitLimit, SetComputeUnitPrice, or SetLoadedAccountsDataSizeLimit instructions. A validator interested in the transaction's resource requirements or priority must locate and interpret these instructions. Transaction v1 moves this information out of the instruction stream and into the transaction format itself.
The mask is a 32-bit little-endian bitfield. Each set bit corresponds to a four-byte word in the ConfigValues section later in the transaction:
- Bits 0 and 1: total priority fee in lamports (encoded as an 8-byte little-endian
u64) - Bit 2: compute-unit limit (a 4-byte
u32) - Bit 3: loaded-account-data-size limit (a 4-byte
u32) - Bit 4: requested heap size (a 4-byte
u32)
N.B. The initial v1 specification assigns meanings only to bits 0–4. The remaining bits are currently unassigned, leaving room for future transaction-level configuration fields.
The mask acts as a compact schema telling the parser both which fields exist and how many bytes to expect. For example, our simple SOL transfer needs a compute-unit limit and loaded-account-data-size limit but does not need a priority fee or custom heap size.
Because these two bits are set, exactly two 4-byte config values follow the transaction's address array, requesting, in our case, a 20,000 CU limit and a 64 KiB loaded-account-data limit.
The mask tells the validator that the first 4-byte config belongs to bit 2 (the compute-unit limit), and the second belongs to bit 3 (the loaded-account-data limit).
In legacy and v0 transactions, omitting a compute-unit-limit instruction gives the transaction an implicit compute budget. v1 does not use those same defaults. An unset compute-unit limit resolves to zero, as does an unset loaded-account-data-size limit; only the heap retains a 32 KiB default. A v1 transaction must therefore explicitly request the resources it needs.
Moving these settings into a well-defined configuration area gives validators direct access to the information they need during transaction ingestion, instead of requiring them to search the instructions. It also means Compute Budget Program instructions no longer configure v1 transactions. If included, they are ignored and processed as no-op instructions while still consuming compute units.
This also provides an additional space benefit. In legacy/v0 transactions, adding Compute Budget instructions means you generally need the 32-byte Compute Budget Program address in the account list, plus the serialized instructions themselves.
Conclusion
Solana's three transaction formats reflect the network's evolution. Legacy established the original model, v0 extended it with ALTs to support larger account sets within the 1,232-byte ceiling, and v1 rethinks the wire format more fundamentally around larger transactions, simpler parsing, inline addresses, and native resource configuration.
All three remain valid, but each captures a different stage in Solana's development. Together they show how the transaction format has adapted as the network's applications, fee markets, and validator requirements have evolved.
Further Resources
- v1 Transactions and the ALT Trade-off - Umberto Natale, Solana Foundation
- Versioned Transactions - Solana Docs
- Increase Transaction Size - SIMD Discussion Forums
- Larger Transaction Sizes - Solana Upgrades
Related Articles
Subscribe to Helius
Stay up-to-date with the latest in Solana development and receive updates when we post


