Table of Contents generated with DocToc
ePBS introduces forward inclusion lists for proposers to guarantee censor resistanship of the network. We follow the design described in this post.
- Proposer for slot N submits a signed block and in parallel broadcasts pairs of
summaries
andtransactions
to be included at the beginning of slot N+1.transactions
are just list of transactions that this proposer wants included at the most at the beginning of N+1.Summaries
are lists consisting on addresses sending those transactions and their gas limits. The summaries are signed, the transactions aren't. An honest proposer is allowed to send many of these pairs that aren't committed to its beacon block so no double proposing slashing is involved. - Validators for slot N will consider the block for validation only if they have seen at least one pair (summary, transactions). They will consider the block invalid if those transactions are not executable at the start of slot N and if they don't have at least 12.5% higher
maxFeePerGas
than the current slot'smaxFeePerGas
. - The builder for slot N reveals its payload together with a signed summary of the proposer of slot N-1. Along the summary, the builder includes a list of transactions indices (in strictly increasing order) of the previous payload of slot N-1, that satisfy some entry in the signed inclusion list summary. The payload is considered only valid if the following applies
- For each index
i
in the payload'sinclusion_list_exclusions
, check that the ith transactiontx[i]
of the payload forN-1
satisfies some transactionT[i]
of the current inclusion list. HereT[i]
is the first entry in the payload's inclusion list that is satisfied bytx[i]
. ThisT[i]
is removed from the inclusion list summary. - The remaining transactions in the inclusion list summary, are all satisfied by the first transactions in the current payload, in increasing order, starting from the first transaction.
- The payload is executable, that is, it's valid from the execution layer perspective.
- For each index
Note: in the event that the payload for the canonical block in slot N is not revealed, then the summaries and transactions list for slot N-1 remains valid, the honest proposer for slot N+1 is not allowed to submit a new IL and any such message will be ignored. The builder for N+1 still has to satisfy the summary of N-1. If there are k slots in a row that are missing payloads, the next full slot will still need to satisfy the inclusion list for N-1.
In the usual case of LMD+Ghost we have a proof of the plausible liveness theorem, that is that supermajority links can always be added to produce new finalized checkpoints provided there exist children extending the finalized chain. Here we prove that the next builder can always produce a valid payload, in particular, a payload that can satisfy the pending inclusion list.
Let N be the last slot which contained a full execution payload, and let
We prove the following: the builder cannot force a transaction in the inclusion list to revert due to gas limit usage. A malicious builder that attempts to add in the exclusion list some transactions from an address with high gas limit but low usage, so that the remaining transactions in the summary have lower gas limit and the included transaction with higher gas usage reverts. However, this is impossible since any transaction in the exclusion list has to have lower nonce since it was already included in the previous block. That is, any attempt by the builder of changing the order in which to include transactions in the exclusion list, will result in its payload being invalid, and thus the inclusion lists transactions that haven't been invalidated on N will remain valid for the next block.
There is a new entity Builder
that is a glorified validator (they are simply validators with a different withdrawal prefix 0x0b
) required to have a higher stake and required to sign when producing execution payloads.
- Builders are also validators (otherwise their staked capital depreciates).
- There is nothing to be done to onboard builders as we can simply accept validators with the right
0x0b
withdrawal prefix before the fork. They will be builders automatically. We could however onboard builders by simply turning validators into builders if they achieve the necessary minimum balance and change their withdrawal prefix to be distinguished from normal validators at the fork. - We need to include several changes from the MaxEB PR in order to account with builders having an increased balance that would otherwise depreciate.
Payments are processed unconditionally when processing the signed execution payload header. There are cases to study for possible same-slot unbundling even by an equivocation. Same slot unbundling can happen if the proposer equivocates, and propagates his equivocation after seeing the reveal of the builder which happens at 8 seconds. The next proposer has to build on full which can only happen by being dishonest. Honest validators will vote for the previous block not letting the attack succeed. The honest builder does not lose his bid as the block is reorged.
Withdrawals are deterministic on the beacon state, so on a consensus layer block processing, they are immediately processed, then later when the payload appears we verify that the withdrawals in the payload agree with the already fulfilled withdrawals in the CL.
So when importing the CL block for slot N, we process the expected withdrawals at that slot. We save the list of paid withdrawals to the beacon state. When the payload for slot N appears, we check that the withdrawals correspond to the saved withdrawals. If the payload does not appear, the saved withdrawals remain, so any future payload has to include those.
- KZG Commitments are now sent on the Execution Payload envelope broadcasted by the EL and the EL block can only be valid if the data is available.
- Blobs themselves may be broadcasted by the builder below as soon as it sees the beacon block if he sees it's safe.
- PTC members are obtained as the first members from each beacon slot committee that are not builders.
- attesters are rewarded as a full attestation when they get the right payload presence: that is, if they vote for full (resp. empty) and the payload is included (resp. not included) then they get their participation bits (target, source and head timely) set. Otherwise they get a penalty as a missed attestation.
- Attestations to the CL block from these members are just ignored.
- The proposer for slot N+1 must include PTC attestations for slot N. There is no rewards (and therefore no incentive) for the proposer to include attestations that voted incorrectly, perhaps we can simply accept 1 PTC attestation per block instead of the current two.
There are two ways to import PTC attestations. CL blocks contain aggregates, called PayloadAttestation
in the spec. And committee members broadcast unaggregated PayloadAttestationMessage
s. The latter are only imported over the wire for the current slot, and the former are only imported on blocks for the previous slot.
There are significant design decisions to make due to the fact that a slot can have 3 different statuses:
- The CL block is not included (therefore no payload can be included). This is a skipped slot.
- The CL block is included and the payload is not revealed. This is an empty slot.
- The CL block and the payload are both included. This is a full slot.
Consider the following fork
graph RL
A[N-1, Full]
B[N, Empty] --> A
C[N, Full] --> A
D[N+1, Full] --> B
In this fork the proposer of N+1
is attempting to reorg the payload of N
that was seen by the majority of the PTC. Suppose that honest validators see that the PTC has voted N
to be full. Then because of proposer boost, the CL block of N+1
will have 40% of the committee to start with. Assuming perfect participation, honest validators should see a weight of 100
for (N, Full)
and a weight of 40
for N+1
(notice that they attest before seeing the payload). They should choose to vote for (N, Full)
instead of N+1
. The question is how do we account for all of this? A few initial comments are in line
- CL attestation do not mention full or empty they simply have a beacon block root. Honest validators will have already set their PTC vote during
N
thatN
was full. - The only changes to the view of
N
as empty/full could come only when importingN+1
, a beacon block that contains PTC Messages attesting for the payload ofN
. However, if honest validators have already achieved the threshold forfull
, they will consider the block full. - This is one design decision: instead of having a hard threshold on the PTC (50% in the current spec) we could have a relative one, say for example a simple majority of the counted votes. This has some minor engineering problems (like keeping track of who voted in forkchoice more than simply if they voted for present or not), but this can easily be changed if there are some liveness concerns.
- The honest builder for
N+1
would not submit a bid here, since honest builders would have seenN
as full also, they would only build on top of the blockhash included inN
. - The honest PTC members for
N+1
will vote forN, Full
they will be rewarded but they will not change the forkchoice view thatN
was already full. - PTC members voting for a previous blockroot cannot change the forkchoice view of the payload status either way.
So the questions is what changes do we need to make to our current weight accounting so that we have (N, Full)
and (N+1, Full)
as viable for head above, but not (N, Empty)
?. Moreover, we want (N, Full)
to be the winner in the above situation. Before dwelling in the justification, let me say right away that a proposer for N+2
would call get_head
and would get N.root
. And then he will call is_payload_present(N.root)
and he will get True
so he will propose based on (N, Full)
reorging back the dishonest (malinformed) proposer of N+1
. The implementation of is_payload_present
is trivial so the only question is how to do LMD counting so that N
beats N+1
in the head computation.
There are several notions that can be changed when we have empty or full slots.
- Checkpoints:
- we can consider checkpoints to be of the form
(Epoch, Root, bool)
where thebool
is to indicate if the Beacon block was full or not. - Another option is to consider checkpoints to be of the form
(Epoch, Root)
exactly as we do today, but only consider the last full block before or equal to theEpoch
start. Both have advantages and disadvantages, the second one allows for different contending states to be the first state of an epoch, but it keeps all implementations exactly as they are today. - A third approach, which seems the best so far, is to keep
(Epoch, Root)
and let head of the chain determine if it is Full or Empty as described below.
- we can consider checkpoints to be of the form
- Ancestry computations, as in
get_ancestor
.- We can change the signature of this function to be of the form
get_ancestor(Store, Root, slot) -> (Root, bool)
So that it returns the beacon block root and weather or not it is based on Full or Empty. - Otherwise we can simply return the last Full block in the line of ancestry. Again there are advantages and disadvantages. In this last case, it would be very hard to know if two given blocks with a given payload status are in the same chain or not.
- We can change the signature of this function to be of the form
The proposal I am considering at this moment is the following:
- Keep checkpoints as
(Epoch, Root)
and allow different start of epoch blocks. - An honest validator, when requesting the state at a given block root, will use its canonical state. That is computed as follows. In the example above, when requesting the state with block root
N
, if a call toget_head
returnedN+1
then the validator would return thestore.block_states[N.root]
which corresponds toN, Empty
. If instead returnedN
then it would return the statestore.execution_payload_states[N.root]
which corresponds toN, Full
. - Thus, when requesting the justified state for example, it will use the state that actually corresponds to its canonical chain and he needs to track only
Epoch, Root
for checkpoints, with minimal code changes. - For LMD accounting, the proposal is to keep weights exactly as today with one exception: direct votes for
N
are only counted in the chains supportingN, Full
orN, Empty
according to the PTC vote. So, in the fork above, any honest validator that voted forN
during slotN
will be counted in the chain forN, Full
, but not in the chain ofN+1, Full
. Honest validators duringN+1
will also vote forN
, and also counting their votes in forN, Full
and not the attacker'sN+1
. Suppose the chain advances with two more bad blocks as follows
graph RL
A[N-1, Full]
B[N, Empty] --> A
C[N, Full] --> A
D[N+1, Full] --> B
G[N+1, Empty] --> B
E[N+2, Full] --> D
F[N+3, Full] --> G
F ~~~ E
In this case all the attesters for N+1
will be counted depending on the PTC members that voted for (N+1, Full)
. Assuming honest PTC members, they would have voted for N
during N+1
so any CL attesters for N+1
would be voting for N+1, Empty
thus only counting for the head in (N+3, Full)
.
There is no current change in store.checkpoint_states[root]
. In principle the "checkpoint state" should correspond to either the checkpoint block being full or empty. However, payload status does not change any consensus value for the state at the given time, so it does not matter if we continue using store.block_states
which corresponds to the "empty" case.
Honest validators that vote for a parent block when a block is late, are contributing for this parent block support and are explicitly attesting that the current block is not present. This is taken into account in the new computation of get_head
. Consider the following situation
graph RL
A[N-1, Full]
B[N, Full] --> A
The block N
has arrived late and the whole committee sees A
as head and vote for N-1
. At the start of N+1
a call to get_head
will return N-1
as head and thus if the proposer of N+1
is honest it will base its block on N-1
. Suppose however that the proposer bases his block on top of N
. Then we see
graph RL
A[N-1, Full]
B[N, Full] --> A
C[N+1, Full] --> B
This block was timely so it gets proposer Boost. The real DAG is
graph RL
A[N-1, Full]
B[N, Full] --> A
C[N+1, Full] --> B
D[N-1, Full] --> A
E[N-1, Full] --> D
And honest validators should still see N-1
as head. The reason being that at the attestation deadline on N+1
validators have seen block N+1
appear, this block is valid and has 40% of a committee vote because of proposer boost. However, the branch for N-1
has the full committee from N
that has voted for it, and thus honest validators vote for N-1
as valid.
There is no need to do anything about proposer equivocations. Builders should reveal their block anyway.
- At the time of reveal, the builder already has counted attestations for the current CL blocks, even if there are or not equivocations. Any equivocation available at this time will not have transactions that can unbundle him since he hasn't revealed.
- If the original block to which the builder committed is included, then the builder doesn't lose anything, that was the original intent. So if the original block is the overwhelming winner at the time of reveal, the builder can simply reveal and be safe that if there are any equivocations anyway his block was included.
- If the builder reveals, he knows that he can never be unbundled unless the next committee has a majority of malicious validators: attestations will go for an empty block before a block that is revealed after 8 seconds.
- So since the builder cannot be unbundled, then he either doesn't pay if the block is not included, or pays and its included.
- The splitting grief, that is, the proposer's block has about 50% of the vote at 8 seconds, remains.
A little care has to be taken in the case of colluding proposers for N
and N+1
. Consider the example of the previous section. The malicious proposer of N
sends an early block to the builder and an equivocation after it has seen the payload. No honest validators will have voted for this equivocation. Suppose N
as head and N+1
proposes based on the equivocating block N
including some unbundled transactions. Because of the PTC vote, even the N
will not count for N+1
since it builds on full instead of empty. The weight of N+1
is only given by proposer boost. The weight of the early N
will be N+1
if N
that included the builders' payload. However, the early block itself may cause a split view, in this case some attesters may have voted for N-1
as head! in this situation we would have a DAG like this (we are not considering irrelevant branches)
graph RL
A[N-1, Full]
B[N, Full] --> A
H[N, Full] --> B
F[N', Full] --> A
I[N', Empty] --> A
C[N+1, Full] --> F
D[N-1, Full] --> A
E[N-1, Full] --> D
When recursing from the children of N-1
the weights for the three children are as follows (when computing after N+1
has been revealed and before validators for N+1
attested)
- (N, Full) has gotten some vote
$\gamma \leq 1 - \beta$ . - (N', Full) has zero weight. This is an important point. Proposer boost does not apply to it because even though
$N+1$ will get proposer boost, it is based on the wrongPTC
vote, and thus it does not count towards this node's weight. - (N', Empty) has
$\beta$ maximum. - (N-1, Full) has
$1 - \beta - \gamma$ .
Thus, supposing
Note however that if we were to apply proposer boost to (N', Full)
then we see that there's a split now between the three possible heads. N'
has proposer boost giving it
This PR includes the changes from this PR. In particular it includes execution layer triggerable withdrawals.
One of the main problems of the current design is that a builder can transfer arbitrary amounts to proposers by simply paying a large bid. This is dangerous from a forkchoice perspective as it moves weights from one branch to another instantaneously, it may prevent a large penalty in case of slashing, etc. In order to partially mitigate this, we churn the transfer overloading the deposit system of Max EB, that is we append a PendingBalanceDeposit
object to the beacon state. This churns the increase in the proposer's balance while it discounts immediately the balance of the builder. We may want to revisit this and add also an exit churn and even deal with equivocations on future iterations.