EIP-8256: Blob streaming

Discussion topic for EIP-8256 :
Initial commit: Add EIP: Blob Streaming by bharath-123 · Pull Request #11610 · ethereum/EIPs · GitHub

Abstract

This EIP introduces the basic elements of the blob streaming mechanism elaborated in https://ethresear.ch/t/scaling-the-da-layer-with-blob-streaming/24202. It enshrines rate-limited blob data pre-propagation through a ticket mechanism, allowing sampling to be reliably extended to the entire slot without extending the critical path - alleviating the free option problem. Concretely, it introduces ahead of time (AOT) blobs whose propagation happens on the CL throughout the entire slot. Propagation of AOT blobs is gated by the requirement to hold a valid ticket which can be acquired in advance through a corresponding ticket contract. Just in time (JIT) blobs on the other hand are propagated in the critical path (as today). This dual system provides a greater flexibility for end users as it allows for immediate and synchronous inclusion of time-sensitive blobs while providing the means of cost efficient, predictable (and potentially CR-resistant) inclusion of all remaining blobs. For a more in-depth discussion see https://ethresear.ch/t/scaling-the-da-layer-with-blob-streaming/24202.

Some comments on the EIP as of ref Add EIP: Blob Streaming · ethereum/EIPs@bdc1d09 · GitHub

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.

JIT transactions cannot have max_fee_per_blob_gas of 0as this is always below the blob gas price in current situation (clarification)

In the validate_blob_transactions, besides validating (when is this method called? Before any tx execution? This is not clear), it also has side effects:

        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

The JIT blobs immediately reduce the sender balance of the sender. Is this method intended as pre-validation of the block? In transaction execution, the fees get collected at the moment the transaction is executed. This thus allows that if the sender does not have sufficient funds at the start of the block, then another transaction can fund it, and it is thus tat that moment valid in the block, where this setup would reject it. It also only removes the blob fees and does not remove any fees related to the value of the transaction or the gas price (base fee + priority fee).

    # Capacity checks
    assert jit_blob_count <= MAX_JIT_BLOBS_PER_BLOCK

Shouldn’t this check aot_blob_count also? It gets changed in the validy method but it has no side effects.

EL Mempool changes

(i.e., tickets whose derived target_slot has not yet passed)

target_slot is not defined, it’s defined in Target slot section which should move before this section

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.

How are the other rules of inclusion validated (sender has enough funds to pay the other fees?). Is the blob fee paid at the ticket purchase? If the tx (without blob fee validation) is now invalid, is it also invalid in the block (sender does not have enough funds to cover value + gas*effective_gas_price of that tx?)

JIT blob transactions without a ticket are delivered directly to the builder out of protocol.

On first hand this sounds like an unwanted introduction, because we now censor (?) these JIT transactions from public mempool?

Ticket Contract

(same deployment method as EIP-4788 and EIP-7002).

Since we enshrine EIP-7997 in Glamsterdam we can change the deployment method to the CREATE2 factory, this ensures that it can be deployed on all chains with the CREATE2 factory (we are not limited by any environment factors such as gas prices or gas schedules; EIP-8037 makes it not possible in Glamsterdam to use the specific transactions used in the mentioned EIPs here because the gas price of deploying these contracts is higher than the gas limit (the txs will OOG once they attempt to create the code))

Each bucket occupies 2 metadata slots:

(it are 3 slots mentioned)

There are TICKET_RING_BUFFER_SIZE many buckets

Shouldn´t the TICKET_RING_BUFFER_SIZE be equal to TICKET_LOOKAHEAD? This allows that one always writes to a bucket which is free-to-use in that slot? If this is not the case then constraints for the TICKET_RING_BUFFER_SIZE should be defined such that it is clear what values are allowed here.

EDIT, it later seems that:

Because the ring buffer length is exactly TICKET_LOOKAHEAD

(Ticket expiry semantics). This should be made clear earlier to set these equal (depending on context we use one of the vars).

The storage base for bucket j is derived directly as:

Is j equal to bucket_index? What is j?

target_slot = slot(selling_block_timestamp) + TICKET_LOOKAHEAD

Note that we have SLOTNUM opcode in Glamsterdam (part of SFId EIPs), should we use that instead of timestamp here?

Code Paths

In the contract, can use SLOTNUM opcode directly.

    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.
    

The variables above the if statement should be updated if it changes in the sstore (bucket_blob_count is the value before the update and would thus check requirement of the previous slot, not current slot)

sstore(base + 2, msg.sender ++ blob_count)

The padding should be defined here, and this will not work if msg.sender ever becomes 32 bytes instead of the current 20. If blob_count for some reason grows over 255 then it should be clear what the padding here is (for the 32 byte sender reason you likely want to pack this in a different slot, e.g. pack ticket_id, block.timestamp (or slotnum) and blob_count together with explicit padding)

There is a possibility to also pre-commit to the blobs (hash of the list of blob hashes), likely not wanted but will just suggest here,

This requirement:

require(bucket_blob_count + blob_count <= MAX_AOT_BLOBS_PER_BLOCK, "AOT capacity exceeded") # restrict the number of AOT blobs in a slot.

Will make AOT senders likely want to first query if their ticket fits into the ticket contract before sending it (and then getting rejected later due to over capacity).
How does this work if there is high demand? Then it is up to the builder which tickets fit in and which will not fit in. Seems like this puts extra market demand (more priority fee or secondary fee routes/markets to ensure ticket gets included). Not sure if intended.

The fee mechanism also means that we pay the current fees for blobs included in the target slot, which on high demand thus underpays for blobs (if this block is full and max tickets are bought, then thus half of these blobs are underpaying the fee).

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

This wastes gas, I would not do it, the deposit contract also does not send to the zero address but just keeps it in the contract.

For the system contract:

MAX_TOTAL_BLOB_GAS

How do we change this if we allow more blobs via a BPO fork?

Furthermore, the contract is not initialized, what are the initial values? How does this work at the initial fork block where we start using this contract? The deprecated block header fields are these set to 0? Note that this does not work on the fork block itself (?) because then the blob base fee is not updated (so the fork block requires careful handling it seems). How do we prevent the contract being called before the fork block? (Can use the EXCESS_INHIBITOR from EIP-7002: Execution layer triggerable withdrawals)

Fee Mechanism

Note that the blob transaction which includes these blobs still has to pay the transaction base fee (and transaction priority fee).

Engine API Changes

The tickets look like it should use execution layer requests?

For the Execution Payload Modifications:

aot_blob_kzg_commitments_root

This seems to bind the aot blob commitments to specific blobs, correct? So I cannot pre-pay for X amount of txs and then change the order, change the blobs, or decrease the blob count in my later txs? Is the blob tx inclusion order guaranteed by the inclusion block or can we scramble this? (tx 2 blobs before tx 1 blobs?)


It feels to me that the censorship resistance or at least a “better” guarantee of inclusion should be created before this is used: for critical blobs the ticketing system seems to fragile, as it is up to the mercy of the next block creator to include our ticket blobs or not.


Backwards Compatibility

  • 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.

Earlier it talks about deprecating these fields, here these are removed. This is a big structural change, for previous deprecatings we just set these fields to a constant to avoid changing the structure.


It feels to me that the censorship resistance or at least a “better” guarantee of inclusion should be created before this is used: for critical blobs the ticketing system seems to fragile, as it is up to the mercy of the next block creator to include our ticket blobs or not.


Ok, a lot of comments, if wanted lets setup a call so we can over the EL changes which need to be addressed here, or maybe there might be some slight misunderstandings about EL tx inclusions / fee market, happy to discuss :smiley: :+1:

1 Like