EIP-8256 - Blob Streaming

Created 2026-05-06
Status Draft
Category Core
Type Standards Track
Authors
Requires

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 and extended with PeerDAS (EIP-7594), 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 and RFC 8174.

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
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 (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:

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) are classified into two categories based on their max_fee_per_blob_gas field:

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, are deprecated from the block header:

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:

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, block hash history contract per EIP-2935).
  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, consolidation request contract per EIP-7251).

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 and EIP-7002).

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:

TICKET_RING_BUFFER_SIZE = TICKET_LOOKAHEAD

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

target_slot = s + TICKET_LOOKAHEAD

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

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 forbucket_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:

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:

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:

1. Buy Tickets (User Call with Value)

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

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:

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:

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:

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 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 (ePBS), the ExecutionPayloadBid is modified:

The ExecutionPayloadEnvelope is extended with:

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:

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:

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 and EIP-2935. 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, 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), 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:

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, 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 (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.