Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions EIPS/eip-7805.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ created: 2024-11-01

FOCIL implements a robust mechanism to preserve Ethereum’s censorship resistance properties by guaranteeing timely transaction inclusion.

FOCIL (**Fo**rk-choice enforced **I**nclusion **L**ists) is built in a few simple steps:
FOCIL (**Fo**rk-**c**hoice enforced **I**nclusion **L**ists) is built in a few simple steps:

- In each slot, a set of validators is selected as inclusion list (IL) committee members. Each member builds and gossips one IL according to their subjective view of the mempool.
- The proposer and all attesters of the next slot monitor, store and forward available ILs.
Expand All @@ -40,7 +40,7 @@ This section outlines the workflow of FOCIL, detailing the roles and responsibil
- **`Slot N`, `t=0 to 8s`**:
IL committee members construct their ILs by including transactions pending in the public mempool, and broadcast them over the P2P network after processing the block for `slot N` and confirming it as the head. If no block is received by `t=7s`, they should run `get_head` and build and release their ILs based on the node’s local head.

By default, ILs are built by selecting raw transactions from the public mempool, ordered by priority fees, up to the IL’s maximum size in bytes of `MAX_BYTES_PER_INCLUSION_LIST = 8 KiB` per IL. Additional rules can be optionally applied to maximize censorship resistance, such as prioritizing valid transactions that have been pending in the mempool the longest.
IL committee members may follow different strategies for constructing their ILs as discussed in [IL Building](#il-building).

#### Validators

Expand Down Expand Up @@ -85,31 +85,35 @@ When validators receive ILs from the P2P network, they perform a series of valid

### Execution Layer

On the execution layer, the block validity conditions are extended such that, after all of the transactions in the block have been executed, we attempt to execute each valid transaction from ILs that was not present in the block.
If one of those transactions executes successfully, then the block is invalid.
On the execution layer, an additional check is introduced for new payloads. After all of the transactions in the payload have been executed, we check whether any transaction from ILs, that is not already present in the payload, could be validly included (i.e. nonce and balance checks pass). If that is the case for any transaction, then an error is returned to the CL. Although the block is valid, the CL will not attest to it.

Let `B` denote the current block.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let B denote the current block.
Let S denote the execution state following the execution of the last transaction in B.
Let gas_left be the gas remaining after execution of B.

For each transaction tx in ILs, perform the following:

  1. Check whether tx is present in B. If tx is present, then jump to the next transaction, else continue with next step.
  2. Validate tx against S.
  • If tx is invalid or tx.gas > gas_left, then continue to the next transaction.
  • If tx is valid and tx.gas <= gas_left, terminate process and return an error.

Copy link
Member

@jihoonsong jihoonsong Mar 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's more precise to view gas_left as a separate value rather than as a part of the execution state.


Let B denote the current block.
Let S denote the execution state following the execution of the last transaction in B.
Let gas_left be the gas remaining after execution of B.

For each transaction tx in ILs, perform the following:

  1. Check whether tx is present in B. If tx is present, then jump to the next transaction, else continue with next step.

  2. Check whether B has enough remaining gas to execute tx. If tx.gas > gas_left, then jump to the next transaction, else continue with next step.

  3. Let balance and nonce be the balance and nonce of tx.origin derived from S respectively. If the required funds for tx > balance or tx.nonce != nonce, then jump to the next transaction, else terminate process and return an error.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we may need more checks to make sure that a transaction is includable. As an example, 7702 SetCodeTransactions that have empty authorisation lists are invalid.

See comment.

Let `S` denote the execution state following the execution of the last transaction in `B`.
Let `gas_left` be the gas remaining after execution of B.

For each transaction `T` in ILs, perform the following:

1. Check whether `T` is present in `B`. If `T` is present, then jump to the next transaction, else continue with next step.

2. Validate `T` against `S`.
2. Check whether `B` has enough remaining gas to execute `T`. If `T.gas` > `gas_left`, then jump to the next transaction, else continue with next step.

3. Validate `T` against `S` by checking the nonce and balance of `T.origin`.

- If `T` is invalid, then continue to the next transaction.

- If `T` is valid, terminate process and assert block `B` as invalid.

3. Execute `T` on state `S`. Assert that the execution of `T` fails.
- If `T` is valid, terminate process and return an `INVALID_INCLUSION_LIST` error.

If `B` is full, the process terminates. Also note that we do not need to reset the state to `S`, since the only way for a transaction to alter the state is for it to execute successfully, in which case the block is invalid, and so the block will not be applied to the state.
#### Engine API Changes

We make the following changes to the engine API:

- Add `engine_getInclusionList` endpoint to retrieve an IL from the `ExecutionEngine`
- Modify `engine_newPayload` endpoint to include a parameter for transactions in ILs determined by the proposer
- Modify `engine_forkchoiceUpdated` endpoint to include a field in the payload attributes for transactions in ILs determined by the proposer
- Add `engine_getInclusionListV1` endpoint to retrieve an IL from the `ExecutionEngine`.
- Add `engine_updatePayloadWithInclusionListV1` endpoint to update a payload with the IL that should be used to build the block. This takes as an argument an 8-byte `payloadId` of the ongoing payload build process, along with the IL itself.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused by this. I might be totally getting the CL <-> EL communication wrong though, but I cannot wrap my head around it currently.

newPayload has a proposed new parameter for the IL. newPayload is used to verify the payload (but does not return a payloadId). The payloadId is returned by forkchoiceUpdated, but only if the CL is proposing a block (then CL will send the extra payloadAttributes param to instruct EL to start building). So newPayload can be used to verify if the block is valid given the IL, but this can only be done after the build process has finished (forkchoiceUpdated with a payloadAttributes returns payloadId, then getPayload to get the payload and then to verify newPayload with the IL). This also means that only after the build process has started (via forkchoiceUpdated) the updatePayloadWithInclusionList can be called (this should interrupt the process and start over by now including the IL).

I think that these changes should be made instead:

engine_newPayloadV5

params: copy from https://github.com/ethereum/execution-apis/blob/3779ff972be60cff5fec93e9b51b0ec24aa52e2d/src/engine/prague.md#engine_newpayloadv4 and include v. inclusionList param. This should also verify the MaxBytesPerInclusionList check (and spec how this size is measured, for instance the RLP of these txs?)

engine_forkchoiceUpdatedV4

Builds on forkchoiceUpdatedV3 https://github.com/ethereum/execution-apis/blob/3779ff972be60cff5fec93e9b51b0ec24aa52e2d/src/engine/cancun.md#engine_forkchoiceupdatedv3 and adds PayloadAttributesV4 with the inclusionList as extra attribute (this should also verify the MaxBytesPerInclusionList I think)

engine_updatePayloadWithInclusionListV1

I think this can be removed. To update the IL in the build process, just call forkchoiceUpdatedV4 again. The forkchoiceState will (should?) stay the same in the build process, so it is clear that the build process should be started over with these new payloadAttributes (new IL)

engine_getInclusionListV1

Keep as currently specced

The main point here is that when building the block we should be aware of the IL. Otherwise the verification process is: build the block, then verify it (newPayload). The build process returns the payloadId and the only way currently to set the IL is via the updatePayloadWithInclusionList, but this needs the payloadId.

Copy link
Member

@jihoonsong jihoonsong May 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding engine_updatePayloadWithInclusionListV1, the main reason I've added was because I wasn't sure how EL devs would feel about overloading forkchoice attribute. We can remove it and add engine_forkchoiceUpdatedV4, if that's what EL devs prefer. We just haven't had enough feedback from EL devs thus far.

Another reason of having engine_updatePayloadWithInclusionListV1 was that it's easier to implement and we wanted to make faster progress. The extra amount of work of merging it into engine_forkchoiceUpdatedV4 can be justified given where we're stand at the moment IMHO.

Block building is one thing and block verification is another. A block builder doesn't need to verify their block. They're the one who builds a block according to the IL constraints. Attesters are the ones who verify a block and attest to it, if it's legit.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you then show me how the current flow would go for building the block? How would the builder know the IL? I'd assume forkchoiceUpdated, and then updatePayloadWithInclusionList with the payloadId returned from the forkchoiceUpdated? This would thus chain 2 calls - right? I'd then really suggest to put this in forkchoiceUpdated 🤔

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd assume forkchoiceUpdated, and then updatePayloadWithInclusionList with the payloadId returned from the forkchoiceUpdated?

Yes, this is current implementation.

It's not guaranteed to have all the ILs until 9 seconds into the slot. So if we were to use forkchoiceUpdatedV4 and make EL call only once, you need to wait until 9 seconds into the slot before initiating the payload building process. I'd like to learn more about how people would think about this change.

I don't have a strong opinion on either way. Thank you for your feedback and I'd encourage more people to give us feedback to find a better solution :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree, the block builder can already start building the block immediately, it can just call forkchoiceUpdatedV4 and we thus assume the IL list is empty. If the IL list gets updated, then either the updatePayloadWithInclusionList could be called (if this is the approach) or if we insert it as param for forkchoiceUpdated then it would call it with exactly the same arguments, but with the updated IL.

I would assume that block builders start building the block immediately. I am not sure, but I think it is likely if the node is not censoring that in most cases, if it has received all ILs (or time to receive new ILs is over) then I think it is likely that the block already consists of most of these txs.

But this is a good point, I am not super familiar with the block building process which clients implement, and it would also be worth to study what "big block builders" implement and how they might run into build problems with the new ILs.

Note that the EIP-1559 fee market will target the blocks to be full on average of the gasTarget which is half the gasLimit. We can thus assume that a "normal block" is 50% full. If the IL then contains transactions which are NOT in the block yet, but are valid (sufficient balance, correct nonce, willing to pay enough for the baseFee and enough gas left in the block), then these can be added on top of the existing block (which was already built). On the other hand, if the block is rather full then the txs might not fit in anymore (due to gas limits). But this block is thus per this spec compliant with the ILs and the ELs will accept these blocks (and the CLs will thus attest).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you add a new engine API, which is forkchoiceUpdatedV4 and make EL call twice, what's the improvement?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The freedom that you can start the building process at any moment. The version with updatePayloadWithInclusionList makes it mandatory to start the build process with the empty list. Then one has to update/interrupt the build process with the inclusion list by calling updatePayloadWithInclusionList

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. You have the same freedom in updatePayloadWithInclusionList approach. In your scenario, the only difference is whether the new engine API is forkchoiceUpdatedV4 or updatePayloadWithInclusionList.

Also, the payload building usually finishes in 100ms. You don't interrupt the building process, you just initiate another round with ILs.

The only case forkchoiceUpdatedV4 approach is better is when you initiate the payload building process after 11 seconds into the previous slot. Given that most of blocks play timing games, I started to think that this would be better in that sense.

- Modify `engine_newPayload` endpoint to include a parameter for transactions in ILs determined by the IL committee member. If the IL is not satisfied an `INVALID_INCLUSION_LIST` error must be returned.

#### IL Building

The rules for building ILs are left to the discretion of implementers. For instance, they may select transactions from the public mempool in various ways such as at random, by priority fee, or based on how long they have been pending. The IL has a maximum size of `MAX_BYTES_PER_INCLUSION_LIST = 8 KiB` for all of the RLP encoded transactions.

### Consensus Layer

Expand Down
Loading