Skip to content
Merged
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
262 changes: 262 additions & 0 deletions proposals/0385-transaction-v1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
---
simd: '0385'
title: Transaction V1 Format
authors:
- jacobcreech
- apfitzge
category: Standard
type: Core
status: Review
created: 2025-10-24
feature: (fill in with feature key and github tracking issues once accepted)
---

## Summary

The current live transaction formats, v0 and legacy, have a number of
limitations that make them unsuitable for efficient ingestion, processing, and
unable to support developer features that are required for today's applications.

This proposal presents a new transaction format, `v1`, that is designed to
get rid of the need for compute budget instructions, address lookup tables,
and more to speed up transaction ingestion and processing.

## Motivation

The current live transaction formats, v0 and legacy, have a number of
inefficiencies and limitations that adversely impact the performance of the
cluster. Ingestion is slowed down when trying to prioritize transactions by
their Compute Budget instructions. Address lookup tables currently supported by
transaction format v0 are also a significant source of complexity for
validators to support.

By introducing a new transaction format, `v1`, we can address these
issues and improve the performance of transactions ingestion, processing, and
save developers space in their transaction by including compute budget
information in the transaction header itself.

## New Terminology

- 'v1 transaction' - A new transaction format that is designed to enable larger
transactions sizes while not having the address lookup table features
introduced in v0 transactions.

## Detailed Design

A new transaction format, `v1`, is proposed to enable larger transaction
sizes.

### Transaction V1 Specification

```
VersionByte (u8)
LegacyHeader (u8, u8, u8)
NumInstructions (u8)
TransactionConfigMask (u32) -- Bitmask of which config requests are present.
LifetimeSpecifier [u8; 32]
NumAddresses (u8)
Addresses [[u8; 32]] -- Length matches NumAddresses
ConfigValues [[u8; 4]] -- Length equal to the popcount (number of set bits)
of TransactionConfigMask. See section TransactionConfigMask for details.
InstructionHeaders [(u8, u8, u16)] -- Length of NumInstructions. Values are
(ProgramAccountIndex, NumInstructionAccounts, NumInstructionDataBytes)
InstructionPayloads [InstructionPayload] -- Length = NumInstructions.
Each InstructionPayload is the concatenation of the following byte arrays:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we anticipate validator or runtime code paths typically requiring to access InstructionAccountIndexes and InstructionData at the same time, or will they primarily consumed in separate phases (e.g., accounts-only pre-execution validation and lock planning vs. data access during execution)?

With v1 moving compute-budget configuration into the header and Alpenglow deprecating vote transactions, (removing one of the main pre-execution consumers of InstructionData as far as I can tell) would it make sense to split instruction payloads into separate contiguous regions for account indices and data? This could in theory improve cache locality and reduce unnecessary data fetches during ingestion without materially affecting execution

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InstructionAccountIndexes serves the same purpose as CompiledInstruction.accounts: it specifies which accounts are passed to the program for execution, together with InstructionData. Given that these two are always consumed together, it would be reasonable to place InstructionAccountIndexes and InstructionData in a contiguous memory region.

InstructionAccountIndexes [u8] -- Length = NumInstructionAccounts from the
corresponding InstructionHeader
InstructionData [u8] -- Length = NumInstructionDataBytes from the
corresponding InstructionHeader
Signatures [[u8; 64]]
```

#### VersionByte

The VersionByte MUST be set to `129` to distinguish v1 transactions from
legacy/v0 formats.

#### LegacyHeader

The LegacyHeader is unchanged from prior transaction formats.
For reference, it consists of three `u8` values with no padding. The values
(in order) are:

- `num_required_signatures`: The number of requires signatures
- `num_readonly_signed_accounts`: The number of the accounts with
signatures that are loaded as read-only
- `num_readonly_unsigned_accounts`: The number of accounts without
signatures that are loaded as read-only

As in prior formats, the transaction fails sanitization if
`num_readonly_signed_accounts >= num_required_signatures`. Note that
`num_readonly_signed_accounts == num_required_signatures` is a sanitization
failure because it would imply the fee payer account is readonly. Note that
this also implies `num_required_signatures >= 1`.

#### NumInstructions

NumInstructions is set to the number of instructions in the transaction. As
explained in the Transaction Constraints section, the transaction fails
sanitization if `NumInstructions > 64`.

#### TransactionConfigMask

TransactionConfigMask is explained below in detail.

#### LifetimeSpecifier

LifetimeSpecifier is the same as the Recent Blockhash field in prior
transaction formats. It has been renamed to clarify its use without changing
its meaning.

#### NumAddresses

NumAddresses is set to the number of account addresses the transaction
references. The transaction fails sanitization if `num_addresses <
num_required_signatures + num_readonly_unsigned_accounts`. The transaction
also fails sanitization if `num_addresses > 64`, as explained in the
Transaction Constraints section.

#### Addresses

Addresses is an array of all the account addresses that the transaction
references. It is a sanitization failure for this array to contain any
duplicates. This list has `NumAddresses` elements. The ordering of the
addresses is unchanged from prior transaction formats.
For reference, they are:

- `num_required_signatures-num_readonly_signed_accounts` additional addresses
for which the transaction contains signatures and are loaded as writable,
of which the first is the fee payer
- `num_readonly_signed_accounts` addresses for which the transaction contains
signatures and are loaded as readonly
- `num_addresses-num_required_signatures-num_readonly_unsigned_accounts`
addresses for which the transaction does not contain signatures and are
loaded as writable
- `num_readonly_unsigned_accounts` addresses for which the transaction does
not contain signatures and are loaded as readonly

Any section with 0 addresses is skipped.

#### ConfigValues

ConfigValues is explained below with TransactionConfigMask in detail.

#### InstructionHeaders

InstructionHeaders is an array with NumInstructions elements. Each element
consists of three fields with no padding:

- `ProgramAccountIndex: u8`: the index in the array of addresses of the
address of the program to invoke for this instruction. This field was known
as `program_id_index` in previous versions of the transaction format; it
has been renamed to clarify its use without changing its meaning.
- `NumInstructionAccounts: u8`: the number of addresses that will be passed
to the program when it is invoked. This field was implicitly represented in
a different encoding in a previous versions of the transaction format as
the element count of the `accounts` vector.
- `NumInstructionDataBytes: u16`: the size in bytes of the data that will be
passed as input to the program when it is invoked. This field was
implicitly represented in a different encoding in a previous versions of
the transaction format as the element count of the `data` vector.

There is also no padding between each 4-byte element.

#### InstructionPayloads

`InstructionPayloads` is a concatenation of byte arrays. For each of the
`NumInstructions` instructions, it consists of `NumInstructionAccounts` bytes
of account indices followed by `NumInstructionDataBytes` of instruction data.
For example, the InstructionPayloads for a two-instruction transaction looks
like:

- instruction 0's InstructionAccountIndices
(InstructionHeaders[0].NumInstructionAccounts bytes)
- instruction 0's InstructionData
(InstructionHeaders[0].NumInstructionDataBytes bytes)
- instruction 1's InstructionAccountIndices
(InstructionHeaders[1].NumInstructionAccounts bytes)
- instruction 1's InstructionData
(InstructionHeaders[1].NumInstructionDataBytes bytes)

The transaction fails sanitization if any element of
InstructionAccountIndices is greater than or equal to NumAddresses

#### Signatures

Signatures is an array with `num_required_signatures` elements, each
consisting of a 64-byte Ed25519 signature. Specifically, `Signatures[i]` is
the signature resulting from signing the portion of the transaction prior to
the Signatures field with the keypair associated with the public key at
`Addresses[i]`.

The transaction must not contain trailing data after the signatures field.
There are no padding fields.

This new `v1` transaction format notably does not include address lookup
tables.

### Transaction Constraints

Any transaction violating these constraints will be considered invalid and will
not be included in the chain. Violations are considered sanitization failures:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit - “invalid” and “sanitization failures” are mentioned in several places. It might help to consolidate all validation criteria into a single section for easier reference.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compiled the sanitization rules below, do I miss anything?

  • Version must be 129.
  • NumInstructions <= 64.
  • NumAddresses <= 64 and >= num_required_signatures + num_readonly_unsigned_accounts.
  • Signature count matches num_required_signatures and is capped (<= 12).
  • No duplicate addresses.
  • Each instruction account index must be < num_addresses.
  • No trailing data after signatures.
  • Transaction total size cap (<= 4096).

Copy link

@0xalpharush 0xalpharush Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think technically the duplicate address check is performed following sanitization and it also checks the lock limit, but I don't see why this couldn't be done earlier.

Legacy also requires the fee-payer to not be a program index and the fee-payer be RW.

Maybe beyond the scope of this proposal.... Would it be possible to demote additional executable-account write-locks by requiring the tx to statically provide indices of program keys only invoked in CPI and not top-level instructions? It would eliminate needless locking and JIT compilation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We definitely need to revisit write lock demotion but I'm not sure this list would make a big difference. I think it's usually a safe assumption to load the program cache for all programs included in a transaction because they'll probably be invoked.


| value | max specified by this SIMD | prior max | max implied by format |
| --- | --- | --- | --- |
| transaction size | 4096 | 1232 | 4096 |
| signatures per transaction | 12 | 12 | 42 |
| num accounts | 64 | 64 | 96 |
| num instructions | 64 | 64 | 255 |
| accounts/instruction | 255 | 255 | 255 |

### TransactionConfigMask

The transaction config mask is used to configure specific fee and resource
requests in a transaction.
It is intended to be flexible enough to add new fields in the future, and is
initially aimed at replacing the ComputeBudgetProgram instructions that are
currently used to configure transactions.
Each bit in the mask represents 4-bytes in the `ConfigRequests` array.
If a configured value, such as priority-fee, needs more than 4-bytes a field can
use 2 bits in the mask.

Initially supported fields and assigned bits:

- [0, 1] - total lamports for transaction priority-fee. 8-byte LE u64.
- [2] - compute-unit-limit. 4 byte LE u32.
- [3] - requested loaded accounts data size limit. 4 byte LE u32.
- [4] - requested heap size. 4 byte LE u32.

For 2 bit fields, such as priority-fee, both bits MUST be set. If only one of
the bits is set, the transaction is invalid and cannot be included in blocks.

For TxV1 transactions, any ComputeBudgetProgram instructions are ignored for
configuration, even if they are invalid.
The instructions will still consume compute-units if included and be processed
as a successful no-op instruction.

For the cost-model, all TxV1 transactions are treated as if requests are
present.
For all fields, if the bit(s) are not set, the minimum allowed value is used:

- If bits [0, 1] are not set, the priority-fee is 0.
- If bit 2 is not set, the requested compute-unit-limit is 0.
- If bit 3 is not set the requested accounts data size limit is 0.
- If bit 4 is not set the requested heap size is 32768 (MIN_HEAP_FRAME_BYTES).

## Alternatives Considered

None at this time.

## Impact

As developers adopt the new transaction format, validator clients accepting
their transactions will be much more efficient at ingestion. The transaction
format as currently defined may also support other features such as larger
transaction sizes.

Notably the proposed format does not support address lookup tables, which are
commonly used in the developer community today.

## Security Considerations

None at this time.
Loading