The core ideas are:
A blob ticket for slot N
gives you two things:
N-1
, including an associated blob transaction. We discuss this in detail laterN
. Meaning, in order to get a blob tx with k
blobs included in slot N
, you must have k
blob tickets for slot N
. No blob basefee is paid however, because the fee paid in the auction substitutes it. The only cost paid at execution time is the regular gas cost.For now let's not worry about how blob tickets are allocated, and let's just focus on how they are used to build a blob mempool. The blob mempool lives entirely on the CL layer p2p network, and consists of a single blob_transaction_envelope
global topic used for sharing blob transactions, and MAX_BLOBS_PER_BLOCK
cell_envelope
topics used for sharing the blob samples associated with them, together with proofs.
Sending messages in these topics at slot N
require holding a ticket for slot N+1
, and is enforced by having messages include a ticket_index
and be signed with blob_ticket.pubkey
, where blob_ticket
is the ticket with index ticket_index
at slot N+1
. As we see later when discussing the auction, the BeaconState
keeps a record of the blob tickets that have been sold, in state.blob_tickets
. In particular, state.blob_tickets[current_slot % SLOTS_PER_EPOCH]
records the tickets for current_slot
. Since the tickets used to access the mempool during slot N
are those from slot N+1
, we use the tickets recorded in state.blob_tickets[current_slot + 1 % SLOTS_PER_EPOCH]
.
blob_transaction_envelope
Note: we could remove kzg_commitments
from BlobTransactionEnvelope
if we had ssz encoded transactions, because we could just access the versioned hashes and check them against the kzg commitments in the cells. Without that, we will also have to give to the EL the versioned hashes corresponding to kzg_commitments
and have it check that they correspond to the ones in transaction
.
Let blob_ticket = state.blob_tickets[slot + 1 % SLOTS_PER_EPOCH][ticket_index]
. Before forwarding, we essentially just check that the message is correctly signed by the holder of an unused ticket:
ticket_indices
for slot
have to be the samesignature
is a valid signature of message
by blob_ticket.pubkey
ticket_index
cell_envelope_{subnet_id}
Note: with NUMBER_OF_COLUMNS = 256
, a Cell
is exactly 1 KB, while a SignedCellEnvelope
~1.2 KBs, or ~20% overhead. Overhead increases linearly with the NUMBER_OF_COLUMNS
.
Let blob_ticket = state.blob_tickets[slot + 1 % SLOTS_PER_EPOCH][ticket_index]
. Again, before forwarding we check that the message is correctly signed by the holder of an unused ticket:
compute_subnet_for_cell_index(cell_index) == subnet_id
signature
is a valid signature of message
by blob_ticket.pubkey
ticket_index
When gossiping, we do not require the following check, which is however required in order for a CellEnvelope
to be valid (together with the gossip checks):
cell
is a an opening of kzg_commitment
at cell_index
, through kzg_proof
The reason we do not do it is that verifying a single cell, though reasonably cheap (~3ms), is much less efficient than batched verification (~16ms for a whole blob for example). For the purpose of preventing DoS, checking the signature is enough. The full verification can be done later, once all cells for a certain blob have been retrieved (or anyway, a client is free to schedule this as it pleases). The cells are ultimately only considered valid once the proof is verified.
Note: since verifying the signature is "only" ~2-3x faster than verifying a cell proof, an alternative design could be to not sign the CellEnvelope
(which saves 96 KBs, ~10%) and to instead verify the proof immediately, without waiting for batch verification. However, a node would then only verify and forward a CellEnvelope
if it knows a corresponding BlobTransactionEnvelope
, because that's the only place where signature verification (i.e. gating of the mempool by tickets) would happen in this design.
The CL sends a newBlobTransactionRequest
to the EL when:
BlobTransactionEnvelope
is validCellEnvelope
are available for all cell indices we are samplingThe request is:
The EL validates it by checking that transaction
is valid and that versioned_hashes == transaction.blob_versioned_hashes
.
In order to for the execution payload of slot
to contain a blob tx with k
KZG commitments, tx.sender
must hold k
blob tickets for slot
, recorded in state.blob_tickets[slot % SLOTS_PER_EPOCH]
. If the CL could decode transactions (for example if ssz was used on both layers), we could directly verify that this is the case for all blob txs by accessing tx.sender
. Without that, we could add blob_sender_addresses: List[ExecutionAddress, MAX_BLOBS_PER_BLOCK]
to theExecutionPayload
, listing a sender address for each kzg commitment in the BeaconBlock
. Then we can just check that each address in blob_sender_addresses
is matched by a winning ticket.
The design space for how to allocate the tickets is very large. This is just one approach, as an example of what's possible. As another very different example, we could have 128 "permanent" blob tickets with an associated Harberger tax.
At a high level, the CL runsSLOTS_PER_EPOCH
parallel auctions, for slots current_slot + SLOTS_PER_EPOCH + [0, SLOTS_PER_EPOCH-1]
, whose bids are sent to an EL contract. One such auction is settled each slot, in particular the one for current_slot + SLOTS_PER_EPOCH - 1
, which is the earliest open one, having been opened for SLOTS_PER_EPOCH
slots . Bids for these auctions are sent to an EL contract and communicated to the CL, and the MAX_BLOBS_PER_BLOCK
best bids for a slot are awarded a blob ticket for it when its auction is settled.
The CL also keeps track of the outcome of the last SLOTS_PER_EPOCH
settled auctions, for slots current_slot + [0,SLOTS_PER_EPOCH-1]
(as mentioned above, the earliest still open auction is for slot current_slot + SLOTS_PER_EPOCH
). There's no reason to keep track of blob tickets beyond current_slot
, because they cannot be used anymore, neither for the mempool nor for inclusion.
We create an EL contract that receives bids in the form of calls with input [bid, quantity, slot, pubkey, address]
, and passes them to the CL, in the form of a BlobTicketRequest
.
The EL does not verify anything, and just passes the message to the CL, but having the bid process on the EL makes it by default spam-resistant and censorship-resistant.
These are introduced in the Beacon Chain:
Overall, blob_ticket_auctions
holds all bids for the SLOTS_PER_EPOCH
parallel auctions, each auction represented by one list. In particular, blob_ticket_auctions[i]
holds theMAX_BLOB_TICKETS_PER_SLOT
highest bids (if there are that many) for the unique slot
such that slot % SLOTS_PER_EPOCH == i
in the current auction period, current_slot + SLOTS_PER_EPOCH + [0, SLOTS_PER_EPOCH-1]
.
blob_tickets
holds the blob tickets for auctions that have already been settled and whose slot
is yet to come. In particular, blob_tickets[i]
holds the winning blob tickets for the slot
in current_slot + [0,SLOTS_PER_EPOCH-1]
such that slot % SLOTS_PER_EPOCH == i
. Therefore,blob_tickets[current_slot % SLOTS_PER_EPOCH]
holds the winning tickets for current_slot
, which we use to determine whether the blobs included in the current execution payload come with corresponding blob tickets owned by their senders. On the other hand,blob_tickets[current_slot+1 % SLOTS_PER_EPOCH]
holds the winning tickets for current_slot+1
, which we use to determine who can send blob txs in the mempool during current_slot
.
process_blob_ticket_request
:
current_slot + SLOTS_PER_EPOCH <= blob_ticket_request.slot < current_slot + 2*SLOTS_PER_EPOCH
. In other words, you can only bid for a slot which is between 32 and 64 slots in the future, because the auction for slots less than 32 slots in the future are already settled, and the ones for more than 64 slots in the future are not yet open.blob_ticket_request
to blob_ticket_auctions[blob_ticket_bid.slot % SLOTS_PER_EPOCH]
as long as either there's still space or blob_ticket_request.bid
is higher than the lowest currently stored bid, in which case it replaces it.process_slot
) to slot
:
s = slot + SLOTS_PER_EPOCH - 1
, by processing blob_ticket_auctions[s % SLOTS_PER_EPOCH]
, after which it resets it to []
blob_ticket_auctions[s % SLOTS_PER_EPOCH]
, it assigns blob tickets to the MAX_BLOBS_PER_BLOCK
highest bids (a bid with quantity > 1
counts as multiple identical bids), by putting them in blob_tickets[s % SLOTS_PER_EPOCH]