---
eip: 8256
title: Blob Streaming
description: Introduces ahead-of-time blob propagation via ticket-based capacity reservation.
author: Marios (@mariosioannou-create), Bharath (@bharath-123), Francesco D'Amato (@fradamt), Julian Ma (@ma-julian), Raúl Kripalani (@raulk), Bosul Mun (@healthykim), Csaba Kiraly (@cskiraly), Anders Elowsson (@anderselowsson)
discussions-to: https://ethereum-magicians.org/t/eip-8256-blob-streaming/28586
status: Draft
type: Standards Track
category: Core
created: 2026-05-06
requires: 2935, 4788, 4844, 7002, 7594, 7732, 7918
---

## Abstract

This EIP enshrines two types of blobs into the protocol, namely Ahead-Of-Time (AOT) and Just-In-Time (JIT) blobs. In practice these exist today already. However, enshrining them in the way we do, has major benefits vis-a-vis performance and security guarantees. As the name suggests, AOT blobs are propagated *ahead of time* (pre-propagated) using a ticket-based propagation mechanism while current Just-In-Time (JIT) blobs are propagated *on the critical path* as today's blobs. Blob data for either blob type is propagated through the CL subnets.



## Motivation

Ethereum's current blob data availability design, introduced in [EIP-4844](./eip-4844.md) and extended with PeerDAS ([EIP-7594](./eip-7594.md)), hinges on blob data propagation on the critical path. While pre-propagation of blob data through the EL blobpool is possible, it comes with no upper bounds on the load being handled and as such can lead to inconsistent local node views - which in turn translates to vulnerability to DOS attacks especially if pre-propagation is coupled with data-availability sampling (DAS). 

Ticket based, ahead-of-time propagation provides a way to overcome a number of issues associated with pre-propagation through the blobpool. Thus, it provides a reliable and secure way of combining DAS with pre-propagation. This in turn allows significantly higher blob throughput without increasing the block propagation window i.e extending the critical path - which comes with its own drawbacks e.g through the free option problem. The free option problem is understood as the ability of the builder to freely choose whether to invalidate a given block during the propagation window providing them the option of only following through when market conditions move in their favor resulting in potentially missed slots. The longer the propagation window, the more severe the problem can become.  

This ticket based mechanism is based on the principle that holding a ticket is a prerequisit for the network to propagate one's blob data. Tickets are acquired through a dedicated contract and as a result, there is at any time a globally agreed upon bounded set of eligible blob senders.  Holding a ticket gives the user the right to propagate blob data using the consensus layer gossip subnets, effectively moving pre-propagation from the EL blobpool to the CL - utilizing the existing network infrastructure. Further, since all users pay *in advance* for the network resources consumed, by acquiring the corresponding ticket, the cost for honest and dishonest usage is equal. This ensures DoS resistance and establishes a common set of blobs seen by nodes in each slot. In addition, the system allows flexible timing for data propagation within a bounded time window which reduces congestion and enables higher overall throughput.

JIT blobs are retained for use cases that require last-moment data commitment, such as L2 sequencers that finalize batch contents close to the inclusion deadline. The two channels share a unified fee mechanism so that total blob demand is accurately reflected in pricing.

A single blob base fee governs both AOT and JIT blobs, updated via the existing `fake_exponential` mechanism but driven by the combined demand from JIT usage and AOT ticket sales. Today's `excess_blob_gas` and `blob_gas_used` header fields are removed, with fee state moving entirely into the ticket contract. Separate per-block capacity limits (B1 for JIT, B2 total) allow flexible allocation between the two blob types while reserving minimum capacity ( R ) for JIT blobs.

## Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) and [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174).

### Parameters

| Constant | Value | Description |
| - | - | - |
| `TICKET_CONTRACT_ADDRESS` | `TBD` | System contract address for blob ticket operations |
| `ZERO_ADDRESS` | `0x0000000000000000000000000000000000000000` | Zero address used to "burn" ticket payments |
| `SYSTEM_ADDRESS` | `0xfffffffffffffffffffffffffffffffffffffffe` | Address used for system calls |
| `MAX_JIT_BLOBS_PER_BLOCK` | `TBD` | Maximum JIT blobs per block (B1) |
| `MAX_TOTAL_BLOBS_PER_BLOCK` | `TBD` | Maximum total blobs per block (B2) |
| `JIT_RESERVED` | `TBD` | Minimum reserved capacity for JIT blobs (R) |
| `MAX_AOT_BLOBS_PER_BLOCK` | `MAX_TOTAL_BLOBS_PER_BLOCK - JIT_RESERVED` | Derived AOT blob cap |
| `TICKET_LOOKAHEAD` | `TBD` | Number of slots between ticket purchase and target slot |
| `GAS_PER_BLOB` | `131072` | Gas per blob (2**17), same as [EIP-4844](./eip-4844.md) |
| `MIN_BLOB_BASE_FEE` | `1` | Minimum blob base fee |
| `BLOB_BASE_FEE_UPDATE_FRACTION` | `TBD` | Denominator for `fake_exponential` update |
| `TARGET_BLOB_GAS` | `TBD` | Target total blob gas per block for fee update |
| `MAX_TOTAL_BLOB_GAS` | `MAX_TOTAL_BLOBS_PER_BLOCK * GAS_PER_BLOB` | Maximum total blob gas per block |
| `BLOB_BASE_COST` | `8192` | Reserve price parameter from [EIP-7918](./eip-7918.md) (2**13) |
| `AOT_PROPAGATION_WINDOW_SLOTS` | `1` | Window (in slots) for accepting AOT data column gossip |
| `TICKET_RING_BUFFER_SIZE` | `TBD` | Ring buffer capacity for ticket storage |

### Helpers

The `fake_exponential` function is identical to the one defined in [EIP-4844](./eip-4844.md):

```python
def fake_exponential(factor: int, numerator: int, denominator: int) -> int:
    i = 1
    output = 0
    numerator_accum = factor * denominator
    while numerator_accum > 0:
        output += numerator_accum
        numerator_accum = (numerator_accum * numerator) // (denominator * i)
        i += 1
    return output // denominator
```

### Blob Transaction Classification

Blob transactions (Type 3, as defined in [EIP-4844](./eip-4844.md)) are classified into two categories based on their `max_fee_per_blob_gas` field:

- **JIT**: `max_fee_per_blob_gas >=  blob_base_fee` (where `blob_base_fee` is the current blob base fee). The transaction pays the blob base fee at inclusion. The transaction's blob data is propagated at block time.
- **AOT**: `max_fee_per_blob_gas == 0`. The transaction does not pay any blob fee at inclusion. It MUST be backed by a valid ticket. The blob data is propagated ahead of time via the consensus layer.

A blob transaction with `max_fee_per_blob_gas` between `1` and `blob_base_fee - 1` (inclusive) is invalid and MUST be rejected. A blob transaction MUST be entirely JIT or entirely AOT. The signed `max_fee_per_blob_gas == 0` for AOT transactions prevents reclassification by the builder: the execution layer rejects any transaction with `max_fee_per_blob_gas` below the current blob base fee as a JIT transaction, so an AOT transaction cannot be included as JIT.

> **Note**: Whether AOT blob transactions reuse Type 3 with `max_fee_per_blob_gas == 0` or use a new transaction type without the blob fee field is a deferred design decision. Both approaches are mechanically equivalent.

### Block Header Changes

The following fields, introduced in [EIP-4844](./eip-4844.md), are **deprecated** from the block header:

- `excess_blob_gas`
- `blob_gas_used`

The blob base fee state is maintained entirely within the ticket contract. Clients read the blob base fee from the contract at the start of each block and cache it for the duration of block processing.

### Block Validation

Block validation MUST enforce the following rules for blob transactions:

```python
def validate_blob_transactions(block: Block, bf_aot: int):
    jit_blob_count = 0
    aot_blob_count = 0

    for tx in block.transactions:
        if not is_blob_tx(tx):
            continue

        n_blobs = len(tx.blob_versioned_hashes)

        if tx.max_fee_per_blob_gas > 0:
            # JIT blob transaction
            assert tx.max_fee_per_blob_gas >= bf_aot, "JIT blob fee too low"
            # Deduct and burn blob fee: n_blobs * GAS_PER_BLOB * bf_aot
            blob_fee = n_blobs * GAS_PER_BLOB * bf_aot
            assert tx.sender.balance >= blob_fee
            tx.sender.balance -= blob_fee
            # blob_fee is burned (not transferred to coinbase)
            jit_blob_count += n_blobs
        else:
            # AOT blob transaction
            # No blob fee charged (already paid at ticket purchase)
            aot_blob_count += n_blobs

    # Capacity checks
    assert jit_blob_count <= MAX_JIT_BLOBS_PER_BLOCK
```

Where `blob_base_fee` is the blob base fee read from the ticket contract at the start of the block.

### EL Mempool Changes

Blob sidecars (the blob data alongside its KZG commitment and proof) are no longer propagated in the execution layer mempool. The EL mempool carries only blob transaction bodies (without blob data).

Propagation of any blob transaction in the EL mempool MUST be gated on ticket ownership. For each address, the mempool tracks a **blob allowance**: the sum of `blob_count` across all active tickets (i.e., tickets whose derived `target_slot` has not yet passed) owned by that address minus the sum of blob transactions corresponding to that address present in the mempool. A blob transaction is eligible for propagation only if that sender has a sufficient blob allowance.

```
blob_allowance(S) = total_tickets(S) - pending_blobs(S) 
```

Where `pending_blobs(S)` is the total blob count across all blob transactions from `S` already in the mempool, and `total_tickets(S)` is the sum of `blob_count` across all active tickets with `owner == S`. A mempool node SHOULD accept and relay a blob transaction from sender `S` only if it has sufficient blob allowance:

```
blob_allowance(S) > len(tx.blob_versioned_hashes) 
```

This gating applies regardless of whether the blob transaction is JIT or AOT (i.e., regardless of `max_fee_per_blob_gas`). Without a ticket, an address has zero blob allowance and its blob transactions cannot propagate through the public mempool. JIT blob transactions without a ticket are delivered directly to the builder out of protocol.

#### Replacements

Replacement of blob transactions with a valid ticket is possible but can be carried out only a limited number of times since, unlike regular txs, it might never get on chain if the corresponding data has not propagated. The user has the choice of either sending a replacement transaction with the same versioned hashes or also changing the versioned hashes - and thus the blob content. In the later case, for the transaction to be valid the corresponding blob data would have to be sent - potentially using another ticket.

### Opcode Changes

| Opcode | Byte | Change |
| - | - | - |
| `BLOBHASH` | `0x49` | No change. Per-transaction indexing works for both JIT and AOT. |
| `BLOBBASEFEE` | `0x4A` | Returns the blob base fee cached from the ticket contract at block start. |

The `BLOBBASEFEE` opcode continues to return the blob base fee, but the source changes from a header field computation to a contract state read cached at the beginning of block processing.

### System Call Ordering

Block processing follows this order:

1. **Start of block**: The client reads `blob_base_fee` from the ticket contract and caches it. Existing system calls execute (beacon root contract per [EIP-4788](./eip-4788.md), block hash history contract per [EIP-2935](./eip-2935.md)).
2. **Transaction processing**: JIT blob transactions are validated and charged the blob fee. AOT blob transactions are validated without blob fee charges. Ticket purchases are processed as normal transactions to the ticket contract.
3. **End of block**: The ticket contract system call executes with `jit_blob_gas_used` as input, updating the excess accumulator and blob base fee for the next block. Other end-of-block system calls follow (withdrawal request contract per [EIP-7002](./eip-7002.md), consolidation request contract per [EIP-7251](./eip-7251.md)).

### Ticket Contract 

The ticket contract is a system contract deployed at `TICKET_CONTRACT_ADDRESS` via the synthetic-sender technique (same deployment method as [EIP-4788](./eip-4788.md) and [EIP-7002](./eip-7002.md)).

#### Storage Layout

| Slot | Name | Description |
| - | - | - |
| 0 | `EXCESS_BLOB_GAS_SLOT` | Running excess blob gas accumulator |
| 1 | `CACHED_FEE_SLOT` | Cached `blob_base_fee` for current block |
| 2 | `TICKET_COUNT_SLOT` | Per-block ticket counter (reset each block) |
| 3 | `NEXT_TICKET_ID_SLOT` | Monotonically increasing ticket ID |
| 4 | `TICKET_RING_BUFFER_OFFSET` | Start of per-bucket metadata storage |
| 5 | `TICKET_DATA_OFFSET` | Start of ticket data storage |

The ring buffer is organized as a ring of per-slot buckets of length:

```python
TICKET_RING_BUFFER_SIZE = TICKET_LOOKAHEAD
```

In slot `s`, all tickets purchased are for target slot:

```python
target_slot = s + TICKET_LOOKAHEAD
```

Since the ring length is exactly `TICKET_LOOKAHEAD`, tickets purchased in slot `s` are always written into bucket:

```python
bucket_index = s % TICKET_RING_BUFFER_SIZE
```

This bucket is exactly the one that becomes reusable once slot `s` is reached. To avoid clearing the bucket on every purchase within the same slot, the contract tracks the last slot for which the current-slot bucket was cleared. Each bucket occupies 2 metadata slots:

| Offset | Field | Size |
| - | - | - |
| 0 | `bucket_blob_count` | `uint256` |
| 1 | `bucket_entry_count` | `uint256` |
| 2 | `bucket_current_slot` | `uint256` | 

There are `TICKET_RING_BUFFER_SIZE` many buckets. Each bucket tracks both the total reserved AOT blob capacity `bucket_blob_count` (< `MAX_AOT_BLOBS_PER_BLOCK`), the number of ticket records stored for that slot `bucket_entry_count` and the slot for which the tickets have been sold for`bucket_current_slot`. Each ticket occupies 5 storage slots:

| Offset | Field | Size |
| - | - | - |
| 0 | `ticket_id` | `uint64` |
| 1 | `selling_block_timestamp` | `uint256` |
| 2 | `owner` (address, 20 bytes) ++ `blob_count` (uint8) ++ `padding` | `bytes32` |
| 3 | `bls_pubkey` (bytes 0-31) | `bytes32` |
| 4 | `bls_pubkey` (bytes 32-47) ++ `padding` | `bytes32` |



The storage base for bucket `j` is derived directly as:

```python
bucket_base = TICKET_DATA_OFFSET + j * MAX_AOT_BLOBS_PER_BLOCK * 5
```


#### Target slot

The parameter `target_slot` determines the slot during which the ticket is valid and can be used to propagate a blob. The target slot for a ticket is derived as:

```python
target_slot = slot(selling_block_timestamp) + TICKET_LOOKAHEAD
```

where `TICKET_LOOKAHEAD` is a parameter, measured in slots and can be set to a value e.g. `1` epoch. A ticket is valid only during that exact `target_slot`.

#### Code Paths

The contract has three code paths, following the pattern established by [EIP-7002](./eip-7002.md):

##### 1. Buy Tickets (User Call with Value)

If the call has nonzero `msg.value` and calldata encoding `(blob_count, bls_pubkey)`:

```python
def buy_tickets(blob_count: uint8, bls_pubkey: bytes48):
    fee = get_fee()
    total_cost = blob_count * GAS_PER_BLOB * fee
    require(msg.value >= total_cost, "Insufficient payment")

    current_slot = slot(block.timestamp)
    bucket_index = current_slot % TICKET_RING_BUFFER_SIZE
    meta_base = TICKET_RING_BUFFER_OFFSET + bucket_index * 3
    
    bucket_blob_count = sload(meta_base + 0)
    bucket_entry_count = sload(meta_base + 1)
    bucket_current_slot = sload(meta_base + 2)

    if bucket_current_slot != current_slot:
        sstore(meta_base + 0, 0)  # bucket_blob_count set to zero
        sstore(meta_base + 1, 0)  # bucket_entry_count set to zero
        sstore(meta_base + 2, current_slot)  # bucket_current_slot set to current_slot

    require(bucket_blob_count + blob_count <= MAX_AOT_BLOBS_PER_BLOCK, "AOT capacity exceeded") # restrict the number of AOT blobs in a slot.
    
    # Append ticket record to bucket
    ticket_id = sload(NEXT_TICKET_ID_SLOT)
    bucket_base = TICKET_DATA_OFFSET + bucket_index * MAX_AOT_BLOBS_PER_BLOCK * 5
    base = bucket_base + bucket_entry_count * 5

    sstore(base + 0, ticket_id)
    sstore(base + 1, block.timestamp)
    sstore(base + 2, msg.sender ++ blob_count)
    sstore(base + 3, bls_pubkey[0:32])
    sstore(base + 4, bls_pubkey[32:48])
    

    # Increment counters
    sstore(meta_base + 0, bucket_blob_count + blob_count)
    sstore(meta_base + 1, bucket_entry_count + 1)
    sstore(NEXT_TICKET_ID_SLOT, ticket_id + 1)

    count = sload(TICKET_COUNT_SLOT)
    sstore(TICKET_COUNT_SLOT, count + blob_count)

    # Burn payment (value stays in contract or sent to a burn address)
    transfer(to=ZERO_ADDRESS, value=msg.value)
```

No refunds are issued. This is the primary defense against capacity griefing.

##### 2. Fee Getter (Zero-Value Call with Empty Calldata)

When called with zero value and empty calldata, the contract returns the current blob base fee:

```python
def get_fee() -> int:
    return sload(CACHED_FEE_SLOT)
```

##### 3. System Call (End of Block)

Called as `SYSTEM_ADDRESS` with `jit_blob_gas_used` (uint256) as calldata at the end of each block:

```python
def system_call(jit_blob_gas_used: uint256):
    require(msg.sender == SYSTEM_ADDRESS)

    # Compute total blob gas for this block
    aot_blob_gas_sold = sload(TICKET_COUNT_SLOT) * GAS_PER_BLOB
    total_blob_gas = jit_blob_gas_used + aot_blob_gas_sold

    # Update excess accumulator (with EIP-7918 reserve price logic)
    old_excess = sload(EXCESS_BLOB_GAS_SLOT)
    old_fee = sload(CACHED_FEE_SLOT)

    if old_excess + total_blob_gas < TARGET_BLOB_GAS:
        new_excess = 0
    elif BLOB_BASE_COST * block.basefee > GAS_PER_BLOB * old_fee:
        # Below reserve price: do not subtract full target, only proportional
        new_excess = old_excess + total_blob_gas * (MAX_TOTAL_BLOB_GAS - TARGET_BLOB_GAS) // MAX_TOTAL_BLOB_GAS
    else:
        new_excess = old_excess + total_blob_gas - TARGET_BLOB_GAS

    sstore(EXCESS_BLOB_GAS_SLOT, new_excess)

    # Compute new blob base fee
    new_fee = fake_exponential(
        MIN_BLOB_BASE_FEE,
        new_excess,
        BLOB_BASE_FEE_UPDATE_FRACTION
    )

    sstore(CACHED_FEE_SLOT, new_fee)

    # Reset per-block ticket counter
    sstore(TICKET_COUNT_SLOT, 0)
```

#### Ticket expiry semantics

Ticket expiry is tied directly to slot progression.

A ticket sold in slot `s` is valid only in slot:

```python
slot(selling_block_timestamp) + TICKET_LOOKAHEAD
```

Because the ring buffer length is exactly `TICKET_LOOKAHEAD`, the bucket used for purchases in slot `s` is reused only when slot `s + TICKET_LOOKAHEAD` arrives. The bucket is cleared once on the first purchase in slot `s`, so earlier tickets from the previous cycle are removed without erasing purchases already made in the current slot. The bucket metadata separately tracks reserved blob capacity (`bucket_blob_count`) and stored ticket records (`bucket_entry_count`), which avoids holes in the ticket storage layout when a single purchase reserves multiple blobs.

#### Fee Mechanism

A single blob base fee `base_fee` governs both JIT and AOT blobs:

| Blob type | When paid | Amount | Destination |
| - | - | - | - |
| JIT | At transaction inclusion | `blob_count * GAS_PER_BLOB * base_fee` | Burned |
| AOT base fee | At ticket purchase | `blob_count * GAS_PER_BLOB * base_fee` | Burned |
| AOT at inclusion | N/A | Nothing | N/A |

Both JIT blob gas usage and AOT ticket sales feed into the excess accumulator via the end-of-block system call. The fee responds to total blob demand across both channels.

The [EIP-7918](./eip-7918.md) reserve price mechanism is implemented inside the ticket contract's excess accumulator update. When `BLOB_BASE_COST * base_fee_per_gas > GAS_PER_BLOB * current_blob_base_fee` (i.e., the blob fee is below the reserve price), the accumulator subtracts less than the full `TARGET_BLOB_GAS`, keeping excess elevated and preventing the fee from falling further.


### Consensus Layer Changes

The following consensus layer changes are described at a high level. The full specification is provided in the companion consensus layer specification.

#### Execution Payload Modifications

Under [EIP-7732](./eip-7732.md) (ePBS), the `ExecutionPayloadBid` is modified:

- The existing `blob_kzg_commitments` field is replaced with:
  - `jit_blob_kzg_commitments`: List of KZG commitments for JIT blobs (included in the bid, needed for critical-path data column propagation before payload reveal).
  - `aot_blob_kzg_commitments_root`: The `hash_tree_root` of the AOT KZG commitments list (cryptographic binding in the bid; full list provided in the envelope since AOT data is already pre-propagated).

The `ExecutionPayloadEnvelope` is extended with:

- `aot_blob_kzg_commitments`: Full list of AOT KZG commitments.

Validation requires: `hash_tree_root(envelope.aot_blob_kzg_commitments) == bid.aot_blob_kzg_commitments_root`.

#### P2P Gossip

New gossip subnets `aot_data_column_sidecar_{subnet_id}` are introduced for AOT blob data propagation. A new container `AotDataColumnSidecar` carries the column data, KZG proofs, commitments, target slot, ticket ID, and BLS signature.

Validation rules for AOT gossip subnets include:

- The target slot MUST be within `[current_slot, current_slot + AOT_PROPAGATION_WINDOW_SLOTS]`.
- The ticket ID MUST correspond to a valid ticket (obtained via the engine API).
- The BLS signature MUST be valid for the ticket's registered public key over the commitments and ticket ID.
- KZG proof verification MUST pass.
- The sidecar MUST be on the correct subnet.
- Only the first valid sidecar per `(ticket_id, column_index)` is accepted.

JIT data columns continue to propagate on the existing `data_column_sidecar_{subnet_id}` subnets at block time, validated against the `jit_blob_kzg_commitments` from the `ExecutionPayloadBid`. Both JIT and AOT use a shared custody model (same column indices).

#### Data Availability

The `is_data_available` check is extended to verify availability of both JIT and AOT data columns. A single unified boolean is used for Payload Timeliness Committee (PTC) attestation.

### Engine API Changes

The following engine API changes are described at a high level. The full specification is provided in the companion engine API specification.

#### `engine_forkchoiceUpdatedV5`

The response is extended with an `activeTickets` field: an array of `TicketInfoV1` objects containing `ticketId`, `sellingBlockTimestamp`, `owner` (address), `blsPubkey`, and `blobCount`. The CL uses the BLS pubkey to gate AOT gossip validation; the EL uses the owner address to gate blob transaction propagation in the mempool. Returned on every `forkchoiceUpdated` call when `payloadStatus` is `VALID`; `null` when syncing or pre-fork.

#### `PayloadAttributesV5`

Extended with `availableAotBlobCommitments`: an array of KZG commitments for which the CL has pre-propagated data available. The builder SHOULD only include AOT blob transactions whose commitments appear in this list.

#### `engine_newPayloadV6`

Accepts two separate versioned hash lists: `expectedJitBlobVersionedHashes` and `expectedAotBlobVersionedHashes`. The EL validates both against the blob transactions in the payload.

#### `engine_getPayloadV7`

The `blobsBundle` contains JIT blob data only. A new `aotBlobInfo` field provides an array of `AotBlobInfoV1` objects (versioned hash, KZG commitment, ticket ID) so the CL can match AOT blobs against its pre-propagated data cache.

#### `engine_getBlobsV3`

No structural change. Queries for AOT versioned hashes return `null` from `getBlobsV3` since the EL does not hold AOT blob data.

## Rationale

### Unified fee mechanism

A single blob base fee for both JIT and AOT channels ensures that total demand is accurately reflected in pricing. If JIT and AOT had separate fees, demand could shift between channels to exploit price differences, creating an inefficient two-tier market. By combining `jit_blob_gas_used` and `aot_blob_gas_sold` as inputs to the same excess accumulator, the fee mechanism treats all blob demand symmetrically.

### Fee state in a system contract

Moving the blob base fee from header fields (`excess_blob_gas`, `blob_gas_used`) into a system contract simplifies the fee update logic. The ticket contract naturally needs to compute the fee for ticket purchases, and co-locating the fee state avoids duplication. The `BLOBBASEFEE` opcode reads from a cached value set at block start, preserving identical EVM semantics for existing contracts.

### Non-refundable tickets

Ticket fees are burned at purchase time with no refund mechanism. This is the primary defense against capacity griefing: an attacker who buys tickets to reserve capacity but never propagates data still pays the full blob base fee. The cost of griefing thus scales with the cost of legitimate usage, making sustained attacks economically prohibitive.



### Separate JIT and AOT capacity limits

Three capacity parameters (B1, B2, R) provide flexibility:

- `MAX_JIT_BLOBS_PER_BLOCK` (B1) caps JIT blobs to bound critical-path propagation latency.
- `MAX_TOTAL_BLOBS_PER_BLOCK` (B2) caps total blobs for overall data availability.
- `JIT_RESERVED` ( R ) ensures minimum JIT capacity even when AOT demand is high, preventing AOT from fully crowding out JIT usage.

The AOT cap `B2 - R` can then be derived.

### BLS key commitment in tickets

Tickets commit to a BLS public key rather than directly to KZG commitments. This allows the ticket holder to defer data preparation until after purchase, while still preventing unauthorized use of the ticket. The BLS signature over `(kzg_commitment, ticket_id)` binds specific data to a specific ticket at propagation time.

### Ring buffer storage

Ring buffer storage for tickets follows the pattern established by [EIP-4788](./eip-4788.md) and [EIP-2935](./eip-2935.md). Tickets expire naturally as the ring buffer rotates, eliminating the need for explicit cleanup. The buffer size is set to accommodate the maximum number of concurrent active tickets: `TICKET_LOOKAHEAD * MAX_AOT_BLOBS_PER_BLOCK`.

### No target slot in contract storage

The ticket contract records the selling block's timestamp but not the target slot. The target slot `slot(selling_block_timestamp) + TICKET_LOOKAHEAD` is derived externally by the CL and EL mempool. This keeps the contract simpler and avoids coupling it to slot timing, which is a CL concept.

### Capacity parameter communication

The capacity parameters `MAX_JIT_BLOBS_PER_BLOCK`, `MAX_TOTAL_BLOBS_PER_BLOCK`, and `JIT_RESERVED` follow the pattern established by [EIP-7742](./eip-7742.md), where blob count limits are communicated from the CL to the EL via the engine API rather than being hardcoded in the EL. This allows the CL to adjust parameters without EL client updates.

### Censorship resistance

This EIP does not include mechanisms for mandatory inclusion of AOT blob transactions. While ticket transactions can be included in [FOCIL ILs (EIP-7805)](./eip-7805.md), a builder can still censor by observing that AOT data is available yet choosing not to include the corresponding transaction. Addressing this requires forced inclusion lists and PTC availability voting, which are left to future work to avoid coupling this proposal to those designs.

## Backwards Compatibility

This EIP introduces several breaking changes:

- **Block header format**: The removal of `excess_blob_gas` and `blob_gas_used` fields changes the block header RLP encoding. All EL clients MUST be updated.
- **Blob fee computation**: Contracts and tooling that compute the blob base fee from header fields MUST be updated to read from the ticket contract or use the `BLOBBASEFEE` opcode.
- **Blob transaction processing**: The split into JIT and AOT classification changes how blob transactions are validated. Existing blob transactions with `max_fee_per_blob_gas > 0` continue to work as JIT blobs.
- **EL mempool**: The removal of blob sidecars from the EL mempool changes how blob data is submitted. Blob data MUST be sent directly to builders (JIT) or propagated via the CL (AOT).

Existing contracts that use `BLOBHASH` (0x49) or `BLOBBASEFEE` (0x4A) continue to function without modification. The opcodes' semantics are preserved.

## Security Considerations

### Capacity griefing via ticket purchase

An attacker could purchase AOT tickets without ever propagating the corresponding blob data. The non-refundable ticket fee is the primary mitigation: the cost of griefing equals the cost of legitimate usage. Additionally, since builders choose which AOT blob transactions to include, unfulfilled tickets (those without available data) are simply not included, and the capacity reverts to availability for JIT blobs up to the total cap B2. In the future we might want to introduce a deposit which gets returned uppon using a ticket thus having a cost of griefing that is greater than the cost of legitimate cost.

### Fee manipulation

Because both JIT usage and AOT ticket sales drive the fee update, an attacker could attempt to manipulate the blob base fee by purchasing tickets (inflating the fee) or withholding JIT transactions (deflating it). The combined demand signal makes this more expensive than manipulating either channel alone. The `fake_exponential` update function, inherited from [EIP-4844](./eip-4844.md), provides the same manipulation resistance properties as the current blob fee mechanism.

### Builder centralization

JIT blob transactions are sent directly to builders rather than propagated through the public mempool. This increases builders' role as gatekeepers for JIT blob inclusion. However, this reflects the existing trajectory under [EIP-7732](./eip-7732.md) (ePBS) and is mitigated by the AOT channel. In the future we might want to add  censorship-resistance mechanism to AOT blobs making these even less reliable on builder cooperation.

### AOT gossip validation

AOT data column sidecars require ticket-gated and BLS signature validation on the CL gossip layer. Invalid sidecars MUST be rejected to prevent amplification attacks. The validation cost (BLS signature verification plus KZG proof verification) is bounded by the maximum number of active tickets and the propagation window. Rate limiting by `(ticket_id, column_index)` prevents replay.

### Ticket front-running

A ticket purchase reveals the buyer's intent to use blob capacity at a future slot. Front-runners could observe pending ticket purchases and buy tickets first, increasing the fee. This risk is comparable to existing mempool front-running of any fee-paying transaction and is mitigated by the same techniques (private transaction submission, MEV-protection services).

### Reorg handling

On a chain reorganization, ticket state may change: tickets from reorged blocks are invalid, and the blob base fee may differ on the new chain. The engine API's `forkchoiceUpdated` response provides the updated ticket set, and CL nodes MUST refresh their ticket cache on reorgs. AOT data already propagated for tickets that become invalid is harmlessly ignored.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
