# How to Implement FOCIL in the CL
This article covers the changes that FOCIL would introduce to the CL. Additionally, it examines the compatibility between FOCIL and ePBS, as the CL client teams primarily support ePBS as the CL headliner for Glamsterdam. It aims to lay out technical aspects of FOCIL to provide solid ground for core devs to help estimate the scope of work.
FOCIL introduces the inclusion list (IL) to constrain a block for next slot such that the block should include IL transactions to satisfy the [IL constraints](https://hackmd.io/iqWhG99dTAKYTFoaTy1jxg?view#Modified-NewPayload). The following sections examine the major requirements through the lens of the IL lifecycle, describing the changes and the specifications.

## IL Production
An IL committee member constructs an IL and broadcasts it over the P2P network. The IL committee consists of 16 beacon committee members appointed each slot. A validator calls [`GetInclusionListCommittee` function](https://ethereum.github.io/consensus-specs/specs/_features/eip7805/beacon-chain/#new-get_inclusion_list_committee) to determine if it has the IL committee assignment.
```python=
def get_inclusion_list_committee(
state: BeaconState, slot: Slot
) -> Vector[ValidatorIndex, INCLUSION_LIST_COMMITTEE_SIZE]:
epoch = compute_epoch_at_slot(slot)
seed = get_seed(state, epoch, DOMAIN_INCLUSION_LIST_COMMITTEE)
indices = get_active_validator_indices(state, epoch)
start = (slot % SLOTS_PER_EPOCH) * INCLUSION_LIST_COMMITTEE_SIZE
end = start + INCLUSION_LIST_COMMITTEE_SIZE
return Vector[ValidatorIndex, INCLUSION_LIST_COMMITTEE_SIZE](
[
indices[compute_shuffled_index(uint64(i % len(indices)), uint64(len(indices)), seed)]
for i in range(start, end)
]
)
```
As the assigned slot approaches, an IL committee member calls the [`GetInclusionList` engine API](https://github.com/jihoonsong/execution-apis/blob/1c22e31fb7ffab128e48ba2b0b6da9d469382bbe/src/engine/experimental/eip7805.md#engine_getinclusionlistv1) to construct an [IL](https://ethereum.github.io/consensus-specs/specs/_features/eip7805/beacon-chain/#inclusionlist). The input parameter is `parentHash`, upon which the returned IL should be built, and the API returns IL transactions.
```python=
class InclusionList(Container):
slot: Slot
validator_index: ValidatorIndex
inclusion_list_committee_root: Root
transactions: List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD]
```
## IL Propagation
ILs are broadcast over the P2P network under the [global topic `inclusion_list`](https://ethereum.github.io/consensus-specs/specs/_features/eip7805/p2p-interface/#inclusion_list). Validators [store ILs received before the IL view freeze cutoff](https://ethereum.github.io/consensus-specs/specs/_features/eip7805/inclusion-list/#new-process_inclusion_list), i.e., 9 seconds into the slot. An IL equivocation occurs when an IL committee member produces multiple ILs for the same slot. If an IL committee member equivocates, all ILs from that member should be ignored for the slot.
```python=
def process_inclusion_list(
store: InclusionListStore, inclusion_list: InclusionList, is_before_view_freeze_deadline: bool
) -> None:
key = (inclusion_list.slot, inclusion_list.inclusion_list_committee_root)
# Ignore `inclusion_list` from equivocators.
if inclusion_list.validator_index in store.equivocators[key]:
return
for stored_inclusion_list in store.inclusion_lists[key]:
if stored_inclusion_list.validator_index != inclusion_list.validator_index:
continue
if stored_inclusion_list != inclusion_list:
store.equivocators[key].add(inclusion_list.validator_index)
store.inclusion_lists[key].remove(stored_inclusion_list)
# Whether it was an equivocation or not, we have processed this `inclusion_list`.
return
# Only store `inclusion_list` if it arrived before the view freeze deadline.
if is_before_view_freeze_deadline:
store.inclusion_lists[key].add(inclusion_list)
```
## IL Compliance
A proposer or builders should produce an execution payload that satisfies the [IL constraints](https://hackmd.io/iqWhG99dTAKYTFoaTy1jxg?view#Modified-NewPayload) with respect to the ILs gathered up to `PROPOSER_INCLUSION_LIST_CUTOFF` into the slot, i.e., 11 seconds into the slot. To that end, a proposer or builders call the [`ForkchoiceUpdated` engine API](https://github.com/jihoonsong/execution-apis/blob/1c22e31fb7ffab128e48ba2b0b6da9d469382bbe/src/engine/experimental/eip7805.md#engine_updatepayloadwithinclusionlistv1) with the `inclusionList` field of `PayloadAttributes` set to the collected ILs. The API returns an IL-compliant payload either by rebuilding the payload or by simply appending IL transactions at the end of the payload.
(Note that `UpdatePayloadWithInclusionList` was used previously but has been replaced with `ForkchoiceUpdated` because the IL is now part of the payload building process and hence `ForkchoiceUpdated` is semantically more appropriate.)
## IL Enforcement
Attesters validate whether the given payload satisfies the [IL constraints](https://hackmd.io/iqWhG99dTAKYTFoaTy1jxg?view#Modified-NewPayload) by calling the [`NewPayload` engine API](https://github.com/jihoonsong/execution-apis/blob/1c22e31fb7ffab128e48ba2b0b6da9d469382bbe/src/engine/experimental/eip7805.md#engine_newpayloadv5). Any non IL-compliant block will be reorged, although it remains a valid block. It [won't receive any proposer boost](https://ethereum.github.io/consensus-specs/specs/_features/eip7805/fork-choice/#modified-on_block), [attesters will vote for its parent](https://ethereum.github.io/consensus-specs/specs/_features/eip7805/fork-choice/#new-get_attester_head) and [proposers will extend its parent](https://ethereum.github.io/consensus-specs/specs/_features/eip7805/fork-choice/#modified-get_proposer_head).
To that end, now the fork choice `Store` tracks blocks that are not IL compliant.
```python=
@dataclass
class Store(object):
# [New in EIP7805]
unsatisfied_inclusion_list_blocks: Set[Root] = field(default_factory=Set)
```
Then, IL constraint validations are now added to `on_block`.
```python=
def validate_inclusion_lists(
store: Store, beacon_block_root: Root, execution_engine: ExecutionEngine
) -> bool:
inclusion_list_store = get_inclusion_list_store()
block = store.blocks[beacon_block_root]
state = store.block_states[beacon_block_root]
inclusion_list_transactions = get_inclusion_list_transactions(
inclusion_list_store, state, Slot(block.slot - 1)
)
return execution_engine.is_inclusion_list_satisfied(
block.body.execution_payload, inclusion_list_transactions
)
def on_block(store: Store, signed_block: SignedBeaconBlock) -> None:
...
# [New in EIP7805]
# Check if block satisfies the inclusion list constraints
# If not, add this block to the store as inclusion list constraints unsatisfied
is_inclusion_list_satisfied = validate_inclusion_lists(store, block_root, EXECUTION_ENGINE)
if not is_inclusion_list_satisfied:
store.unsatisfied_inclusion_list_blocks.add(block_root)
# Add proposer score boost if the block is timely, not conflicting with an existing block
# and satisfies the inclusion list constraints.
is_first_block = store.proposer_boost_root == Root()
# [Modified in EIP7805]
if is_timely and is_first_block and is_inclusion_list_satisfied:
store.proposer_boost_root = hash_tree_root(block)
...
```
If the block is not IL compliant, attesters will not vote for it and the proposer will not extend it, which will reorg the block.
```python=
def get_attester_head(store: Store, head_root: Root) -> Root:
if head_root in store.unsatisfied_inclusion_list_blocks:
head_block = store.blocks[head_root]
return head_block.parent_root
return head_root
```
```python=
def get_proposer_head(store: Store, head_root: Root, slot: Slot) -> Root:
...
# [New in EIP7805]
# Check that the head block is in the unsatisfied inclusion list blocks
inclusion_list_not_satisfied = head_root in store.unsatisfied_inclusion_list_blocks
if reorg_prerequisites and (head_late or inclusion_list_not_satisfied):
return parent_root
else:
return head_root
```
## FOCIL and ePBS
One of the synergies between FOCIL and ePBS comes from the separation of the beacon block and the execution payload. With ePBS, if the execution payload is not IL compliant, only the execution payload is reorged without affecting beacon chain stability.
The major changes are the introduction of the IL bitlist and the IL inclusivity check during the IL compliance phase, as well as the adaptation of IL validation logic to the separation of the beacon block and the execution payload during the IL enforcement phase.
### IL Production
No changes required.
### IL Propagation
No changes required.
### IL Compliance
When a proposer accepts a bid in ePBS, they need to know whether the bid would satisfy the IL constraints. For this reason, builders now include the IL bitlist in their bids, indicating which ILs they’ve considered. The proposer compares each bid’s IL bitlist against their subjective IL view and selects a bid from those whose IL bitlists are supersets.
Validators also compare the IL bitlist contained in the beacon block against their local IL view. One thing to note here is that they only check whether the IL bitlist is a superset of their IL view. Even if they’ve received the execution payload, they don’t validate whether the execution payload satisfies the IL constraints during the IL inclusivity check. The IL constraints are enforced by attesters for the next slot.
Either attesters or PTC members can perform this check with different trade-offs. When attesters take this into account during attestation, we leverage the full beacon committee, but failing the IL inclusivity check results in a beacon block reorg, affecting beacon chain stability. On the other hand, the PTC approach can preserve the beacon block, although it involves only a small set of validators. Whether to use attesters or PTC remains open to discussion.
First, the IL bitlist is added to the beacon block.
```python=
class BeaconBlockBody(Container):
...
# [New in EIP7805]
inclusion_list_bits: Bitvector[INCLUSION_LIST_COMMITTEE_SIZE]
```
Then, attesters or PTC memebers check IL inclusivity using the following functions.
```python=
def get_inclusion_list_bits(
store: InclusionListStore, state: BeaconState, slot: Slot
) -> Bitvector[INCLUSION_LIST_COMMITTEE_SIZE]:
inclusion_list_committee = get_inclusion_list_committee(state, slot)
inclusion_list_committee_root = hash_tree_root(inclusion_list_committee)
key = (slot, inclusion_list_committee_root)
validator_indices = [
inclusion_list.validator_index for inclusion_list in store.inclusion_lists[key]
]
return Bitvector[INCLUSION_LIST_COMMITTEE_SIZE](
validator_index in validator_indices for validator_index in inclusion_list_committee
)
def is_inclusion_list_bits_inclusive(
store: InclusionListStore,
state: BeaconState,
slot: Slot,
inclusion_list_bits: Bitvector[INCLUSION_LIST_COMMITTEE_SIZE],
) -> bool:
inclusion_list_store = get_inclusion_list_store()
local_inclusion_list_bits = get_inclusion_list_bits(inclusion_list_store, state, Slot(slot - 1))
return all(
inclusion_bit or not local_inclusion_bit
for inclusion_bit, local_inclusion_bit in zip(
inclusion_list_bits, local_inclusion_list_bits
)
)
```
### IL Enforcement
With the separation of the beacon block and the execution payload, IL enforcement now occurs in tandem—first when the execution payload arrives and then when the beacon block is processed.
Following the separation, the fork choice `Store` now tracks execution payloads that are not IL compliant.
```python=
@dataclass
class Store(object):
...
# [New in EIP7805]
unsatisfied_inclusion_list_payloads: Set[Hash32] = field(default_factory=Set)
```
When the execution payload is delivered, the validation logic stays the same. The CL relies on the EL for validation.
```python=
def is_inclusion_list_satisfied_payload(
store: Store,
block_root: Root,
payload: ExecutionPayload,
execution_engine: ExecutionEngine,
) -> bool:
inclusion_list_store = get_inclusion_list_store()
block = store.blocks[block_root]
state = store.block_states[block_root]
inclusion_list_transactions = get_inclusion_list_transactions(
inclusion_list_store, state, Slot(block.slot - 1)
)
return execution_engine.is_inclusion_list_satisfied(payload, inclusion_list_transactions)
def on_execution_payload(store: Store, signed_envelope: SignedExecutionPayloadEnvelope) -> None:
...
# [New in EIP7805]
# Check if payload satisfies the inclusion list constraints
# If not, add this payload to the store as inclusion list unsatisfied
is_inclusion_list_satisfied = is_inclusion_list_satisfied_payload(
store, envelope.beacon_block_root, envelope.payload, EXECUTION_ENGINE
)
if not is_inclusion_list_satisfied:
store.unsatisfied_inclusion_list_payloads.add(envelope.payload.block_hash)
...
```
When the beacon block arrives, attesters vote for it only if its IL bitlist is inclusive and it extends the IL compliant execution payload, if the parent block was full. In other words, if the payload was not IL compliant, the beacon block should reorg it in order to get attestations. Other cases follow the ePBS specification.
```python=
def is_inclusion_list_satisfied_block(store: Store, block_root: Root) -> bool:
inclusion_list_store = get_inclusion_list_store()
block = store.blocks[block_root]
state = store.block_states[block_root]
payload_header = block.body.signed_execution_payload_header.message
inclusion_list_bits_inclusive = is_inclusion_list_bits_inclusive(
inclusion_list_store, state, block.slot, block.body.inclusion_list_bits
)
inclusion_list_transactions_satisfied = (
not is_parent_node_full(store, block)
or payload_header.parent_block_hash not in store.unsatisfied_inclusion_list_payloads
)
return inclusion_list_bits_inclusive and inclusion_list_transactions_satisfied
def on_block(store: Store, signed_block: SignedBeaconBlock) -> None:
...
# Add proposer score boost if the block is timely and not conflicting with an existing block
is_first_block = store.proposer_boost_root == Root()
is_inclusion_list_satisfied = is_inclusion_list_satisfied_block(
store, block_root
) # [New in EIP-7805]
if is_timely and is_first_block and is_inclusion_list_satisfied: # [Modified in EIP-7805]
store.proposer_boost_root = hash_tree_root(block)
...
```
Same as before, if the beacon block is not IL compliant, attesters will not vote for it and the proposer will not extend it, which will reorg the block.
```python=
def get_attester_head(store: Store, head_root: Root) -> Root:
is_inclusion_list_satisfied = is_inclusion_list_satisfied_block(store, head_root)
if not is_inclusion_list_satisfied:
head_block = store.blocks[head_root]
return head_block.parent_root
return head_root
```
```python=
def get_proposer_head(store: Store, head_root: Root, slot: Slot) -> Root:
...
# [New in EIP7805]
# Check that the head block satisfies the inclusion list constraints
inclusion_list_satisfied = is_inclusion_list_satisfied_block(store, head_root)
if reorg_prerequisites and (head_late or not inclusion_list_satisfied):
return parent_root
else:
return head_root
```
## Closing Remarks
FOCIL has stable specifications and 4 out of 6 CL client teams already have interop prototypes. Rebasing FOCIL onto ePBS has also been examined from both research and specification perspectives.
FOCIL in Glamsterdam has received positive feedback from all core devs who built the prototypes. The author encourages more developers—either working on clients, devops or testing—to closely review the feasibility of FOCIL in Glamsterdam.
[*Prev: How to Implement FOCIL in the EL*](https://hackmd.io/@jihoonsong/BJpcaudvex)
## Links
* [EIP-7805](https://eips.ethereum.org/EIPS/eip-7805)
* [FOCIL Consensus Specification](https://ethereum.github.io/consensus-specs/specs/_features/eip7805/beacon-chain/)
* [FOCIL onto ePBS Consensus Specification](https://github.com/ethereum/consensus-specs/pull/4493)
* [Prysm Prototype](https://github.com/OffchainLabs/prysm/pull/14754)
* [Lodestar Prototype](https://github.com/ChainSafe/lodestar/tree/focil)
* [Teku Prototype](https://github.com/Consensys/teku/tree/focil)
* [Lighthouse Prototype](https://github.com/sigp/lighthouse/tree/electra-focil)
* [ePBS and FOCIL Compatibility](https://ethereum-magicians.org/t/epbs-focil-compatibility/24777)
* [A One-pager for Implementors](https://github.com/jihoonsong/focil-for-implementors/wiki/FOCIL-for-Implementors)