This is the second post on the series annotating the Gloas fork. In this post we will go over validation of objects over gossip. These paths are typically not executable on the spec and leave more slack for clients to design their validation pipeline. For example, changing the order of validation can lead a client to ignore messages that other client would reject.
The structure of this post is different than the beacon chain one. We will break the post in sections for each object. Special care will be taken with respect to DataColumnSidecar because this validation is very likely to change.
My gratitude goes to Manu Nalepa for continuing to send corrections.
Attestations
The only change to attestations in Gloas is that the committee index is overloaded to indicate the payload content when attesting to an old block. We thus added the following two validations
- [REJECT]
aggregate.data.index < 2. - [REJECT]
aggregate.data.index == 0ifblock.slot == aggregate.data.slot.
enforcing that the index can be either 0 or 1 and it must be zero for same slot attestations.
The exact same changes are needed both in the aggregation and proof topics and the single attestations topics. Notice that we removed the Electra condition forcing this index to be zero.
Beacon Blocks
Beacon blocks in Gloas are much lighter because the payload is missing. Instead, they only commit to a future payload via a signed bid. Thus, all validations about the execution payload are removed. Some of the information of the execution payload is still available in the signed bid, so those validations are now on the bid.
- If
execution_payloadverification of block’s execution payload parent by an execution node is complete:- [REJECT] The block’s execution payload parent (defined by
bid.parent_block_hash) passes all validation.
- [REJECT] The block’s execution payload parent (defined by
- [REJECT] The bid’s parent (defined by
bid.parent_block_root) equals the block’s parent (defined byblock.parent_root).
If we move full KZG commitments to the signed bid, then the following rule needs to be added to beacon block gossip validation:
- [REJECT] The length of KZG commitments is less than or equal to the
limitation defined in the consensus layer – i.e. validate that
len(bid.blob_kzg_commitments) <= get_blob_parameters(get_current_epoch(state)).max_blobs_per_block
Execution Payload
Execution payload validation is now a gossip validation because the paylaod travels independently as a SignedExecutionPayloadEnvelope. The checks are as follows:
- _[IGNORE]_ The envelope's block root `envelope.block_root` has been seen (via
gossip or non-gossip sources) (a client MAY queue payload for processing once
the block is retrieved).
That is, we ignore payloads for blocks that we haven’t seen yet. In implementations this cache for pending payloads was actually hard to deal with, this is the reason why the slot was added to the payload envelope to make it simpler to identify which blocks we may be missing. The situation of the payload arriving earlier than the beacon block could happen in local environments for self-built blocks, that are broadcast jointly, if the payload is essentially empty.
- _[IGNORE]_ The node has not seen another valid
`SignedExecutionPayloadEnvelope` for this block root from this builder.
- _[IGNORE]_ The envelope is from a slot greater than or equal to the latest
finalized slot -- i.e. validate that
`envelope.slot >= compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)`
These are standard.
When we have seen the block with the given beacon block root, the following are validity conditions
- _[REJECT]_ `block` passes validation.
- _[REJECT]_ `block.slot` equals `envelope.slot`.
- _[REJECT]_ `envelope.builder_index == bid.builder_index`
- _[REJECT]_ `payload.block_hash == bid.block_hash`
- _[REJECT]_ `signed_execution_payload_envelope.signature` is valid with respect
to the builder's public key.
The first one is to never accept payloads for invalid blocks and penalize peers that send those. The other three are there to make sure that the payload agrees with the committed bid, there are no checks with respect of parent blocks because those checks are done on processing the payload.
Notice that there could be equivocations on the builder side, execution requests for example are included in the envelope and are not checked. Nor even for length. Thus the condition on only forwarding one payload is necessary.
Proposer preferences
This topic is added so that proposers can broadcast information about their upcoming proposal slot to builders.
Proposers broadcast one epoch in advance (the lookahead is for two epochs), the following objects
class ProposerPreferences(Container):
proposal_slot: Slot
validator_index: ValidatorIndex
fee_recipient: ExecutionAddress
gas_limit: uint64
class SignedProposerPreferences(Container):
message: ProposerPreferences
signature: BLSSignature
These simply send the fee recipient and the gas limit as tweakable preferences, and the proposing slot expected for this proposer and the validator index that is used for signature verification. We could add other preferences like the min bid that the proposer would accept from an external builder and the such, we these were deemed premature, specially since they could break the p2p builder set from being a good fallback mechanism in case self-building is no longer possible.
The gossip validation in this topic is specified as
- _[IGNORE]_ `preferences.proposal_slot` is in the next epoch -- i.e.
`compute_epoch_at_slot(preferences.proposal_slot) == get_current_epoch(state) + 1`.
- _[REJECT]_ `preferences.validator_index` is present at the correct slot in the
next epoch's portion of `state.proposer_lookahead` -- i.e.
`is_valid_proposal_slot(state, preferences)` returns `True`.
- _[IGNORE]_ The `signed_proposer_preferences` is the first valid message
received from the validator with index `preferences.validator_index` and the given slot.
- _[REJECT]_ `signed_proposer_preferences.signature` is valid with respect to
the validator's public key.
We simply check that the proposer is indeed proposing in the given slot and that the proposal is in the next epoch, so that at the start of epoch N, proposers already find out that they will be proposing in N+1, they can start submitting these messages.
This topic is expected to have very few messages given that only 32 of them can possibly be valid.
Payload bids
This topic is still not completely specified and may suffer many changes. Mostly to try to throttle and avoid DOS on a global topic in which any builder can send messages.
The rules at the time of writing this post are divided in two groups:
- _[IGNORE]_ `bid.slot` is the current slot or the next slot.
- _[IGNORE]_ the `SignedProposerPreferences` where `preferences.proposal_slot`
is equal to `bid.slot` has been seen.
- _[REJECT]_ `bid.builder_index` is a valid/active builder index -- i.e.
`is_active_builder(state, bid.builder_index)` returns `True`.
- _[REJECT]_ `bid.execution_payment` is zero.
- _[REJECT]_ `bid.fee_recipient` matches the `fee_recipient` from the proposer's
`SignedProposerPreferences` associated with `bid.slot`.
- _[REJECT]_ `bid.gas_limit` matches the `gas_limit` from the proposer's
`SignedProposerPreferences` associated with `bid.slot`.
- _[IGNORE]_ this is the first signed bid seen with a valid signature from the
given builder for this slot.
- _[IGNORE]_ this bid is the highest value bid seen for the corresponding slot
and the given parent block hash.
- _[IGNORE]_ `bid.value` is less or equal than the builder's excess balance --
i.e. `can_builder_cover_bid(state, builder_index, amount)` returns `True`.
- _[IGNORE]_ `bid.parent_block_hash` is the block hash of a known execution
payload in fork choice.
- _[IGNORE]_ `bid.parent_block_root` is the hash tree root of a known beacon
block in fork choice.
- _[REJECT]_ `signed_execution_payload_bid.signature` is valid with respect to
the `bid.builder_index`.
These are validation rules, while the DOS prevention is left as an implementation detail to clients:
*Note*: Implementations SHOULD include DoS prevention measures to mitigate spam
from malicious builders submitting numerous bids with minimal value increments.
Possible strategies include: (1) only forwarding bids that exceed the current
highest bid by a minimum threshold, or (2) forwarding only the highest observed
bid at regular time intervals.
The idea behind leaving this up to clients, is that as long as they implement reasonable defaults, and are different enough, these rules will be hard to game due to client diversity.
On the validation list we see that we only accept bids for the current and next slot. Vanilla clients are expected to submit bids as soon as they have executed the previous payload and built the next block, this could be very early in the previous slot. Some more sophisticated builders that still don’t want to open servers for direct connections may want to wait as much as possile and submit bids during the slot, after having seen previous bids in the p2p network.
Other validations are standard, they are mostly to check that the bid is compatible with the proposer’s preferences that must have been seen before. One of the problems is builder activation. Since builders will be onboarded from pending deposits only at the fork, it is very unlikely that they will be active right after the fork (their deposit needs to have been already finalized, this could happen if there was a large pending queue at the fork and the builder deposited two epochs in advance).
We check that the builder actually can afford the bid, in particular, we only allow trustless payments in this topic, including trusted ones would be useless as anyone could promise the maximum value. We only allow bids that build on top of known block. Notice that we do not make any checks with respect of the EL block hash that the bid is referring to. We could in principle enforce that the parent hash is known or corresponds to the parent block’s bid or the parent hash of the parent block. But these checks are done when processing and would be inherently slower to be carried out in gossip.
Blob data
The topic of data column sidecar validation is in flux. I’ll discuss here the design decisions both in the current status and how it may change due to this open issue.
The current validation mechanism
Currently, the signed bid includes a hiding commitment to the blob KZG commitments by including their hash instead of the full list. This commitment is currently useless and it is there mostly to avoid equivocations from the builder when sending the sidecars. The idea is that a data column sidecar is always expected to be received after the beacon block, but not necessarily after the payload envelope. Both data column sidecars and the payload envelope include the full list of KZG commitments. The reason for the former is that they can be validated regardless of having seen the payload or not. In fact, a single data column sidecar may be much smaller than the payload, and the builder may choose to start broadcasting them earlier than the payload. So it is highly expected that we will see them before the payload often. The payload envelope also includes them so that it can be verified independently of having seen a valid data column sidecar: the KZG commitments are needed to send the versioned hashes to the engine on payload validation. So the current approach requires that the beacon block has been seen to validate both payload envelope and data column sidecars. But these objects sent by the builder can be verified independently of seeing any of them first.
A proposed variation
It has been proposed to instead move the full list of KZG commitments instead of their hash to the signed bid. This allows this list to be removed from the payload and from every data column sidecar. So it would be included only once in the block, and it can be removed up to 129 times from the broadcast data in the slot.
What this mechanism would allow is for attesters of N+1, to start requesting blobs from the engine as soon as they see the beacon block for N, even before the builder for that payload has even started assembling the data column sidecars! This could potentially lower bandwith consumption considerably. Another big gain for clients is on simplicity of implementation since currently both for Fulu and since Deneb, these commitments are assumed to be in the beacon block body.
The cost of making this change is that signed bids go from 316 bytes to 3772 bytes with 72 blobs. This could potentially make the DOS prevention tactics on the p2p bidding mechanism more strict and could play a role by adding latency to the centralized builder auction with direct connections to proposers.
Notice that this also doesn’t change the need for the beacon block to have been seen when validating the data column sidecar. We can remove the KZG commitments from the sidecar because the sidecar has the beacon block root, and thus we can take the commitments from the included bid. Something similar happens with the payload envelope.
Merkle Inclusion proof
In principle the builder could include the same Merkle system on the data column sidecars that shows that the sidecars are committed in the beacon block tied with a proposer’s signature. This would make it so that the data column sidecars could be validated without having seen the beacon block. This could actually be a case that happens if the builder gets the block from the proposer in a direct connection and immediately broadcast data columns. Attesters for N+1 can receive these columns before getting the beacon block for N. However, since these attesters have an entire slot to validate data, it doesn’t make much sense to go with this inclusion mechanism and bloat bandwidth for no reason. Data column validation is no longer in the hot path to validation, only attesters for the next slot are interested and not the current slot’s attesters.
The actual specification
The structure is currently modified to be
class DataColumnSidecar(Container):
index: ColumnIndex
column: List[Cell, MAX_BLOB_COMMITMENTS_PER_BLOCK]
kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK]
kzg_proofs: List[KZGProof, MAX_BLOB_COMMITMENTS_PER_BLOCK]
# [Modified in Gloas:EIP7732]
# Removed `signed_block_header`
# [Modified in Gloas:EIP7732]
# Removed `kzg_commitments_inclusion_proof`
# [New in Gloas:EIP7732]
slot: Slot
# [New in Gloas:EIP7732]
beacon_block_root: Root
If we do move KZG commitments to the bid, we would remove them from this structure. Notice that we have already removed the Merkle proofs for the reasons mentioned above.
The gossip validation is as follows
- _[IGNORE]_ The sidecar's `beacon_block_root` has been seen via a valid signed
execution payload bid. A client MAY queue the sidecar for processing once the
block is retrieved.
- _[REJECT]_ The sidecars's `slot` matches the slot of the block with root
`beacon_block_root`.
- _[REJECT]_ The hash of the sidecar's `kzg_commitments` matches the
`blob_kzg_commitments_root` in the corresponding builder's bid for
`sidecar.beacon_block_root`.
- _[REJECT]_ The sidecar is valid as verified by
`verify_data_column_sidecar(sidecar)`.
- _[REJECT]_ The sidecar is for the correct subnet -- i.e.
`compute_subnet_for_data_column_sidecar(sidecar.index) == subnet_id`.
- _[REJECT]_ The sidecar's column data is valid as verified by
`verify_data_column_sidecar_kzg_proofs(sidecar)`.
- _[IGNORE]_ The sidecar is the first sidecar for the tuple
`(sidecar.beacon_block_root, sidecar.index)` with valid kzg proof.
So we explicitly require the beacon block to be known and that the kzg commiment hash matches the committed one in the bid. verify_data_column_sidecar is modified as follows
def verify_data_column_sidecar(sidecar: DataColumnSidecar) -> bool:
"""
Verify if the data column sidecar is valid.
"""
# The sidecar index must be within the valid range
if sidecar.index >= NUMBER_OF_COLUMNS:
return False
# A sidecar for zero blobs is invalid
if len(sidecar.kzg_commitments) == 0:
return False
# [Modified in Gloas:EIP7732]
# Check that the sidecar respects the blob limit
epoch = compute_epoch_at_slot(sidecar.slot)
if len(sidecar.kzg_commitments) > get_blob_parameters(epoch).max_blobs_per_block:
return False
# The column length must be equal to the number of commitments/proofs
if len(sidecar.column) != len(sidecar.kzg_commitments) or len(sidecar.column) != len(
sidecar.kzg_proofs
):
return False
return True
We now need to check that the sidecar’s KZG commitments respect the blob limit using the sidecar’s slot and not the block header that is no longer present.
If we move to remove the KZG commitments from the sidecars as explained above, this helper will take them as an input, passed from the beacon block’s committed bid.
