Models the Ethereum Engine API protocol - the CL/EL interaction from the CL's perspective.
CL (consensus layer) EL (execution layer)
| |
|-- forkchoiceUpdated ---------> | "make this block canonical"
| <-------- VALID/INVALID/SYNC --|
| |
|-- forkchoiceUpdated + attrs -> | "build me a block"
| <-------- VALID + payload_id --|
|-- getPayload ----------------> |
| <-------- ExecutionPayloadV3 --|
| |
|-- newPayload ----------------> | "validate this block"
| <-------- VALID/INVALID ------|
make test SPEC=engine-api # run tests
make run SPEC=engine-api # random simulation
make verify SPEC=engine-api # check safety properties
make help SPEC=engine-api # see all commands| Variable | Type | Description |
|---|---|---|
cl_head |
BlockHash |
CL's view of canonical head |
el_head |
BlockHash |
EL's canonical head |
el_validated |
Set[BlockHash] |
EL's validated payloads pool |
el_building |
PayloadId -> BlockHash |
Blocks being built (parent hash) |
el_built |
PayloadId -> BlockHash |
Built blocks ready for retrieval |
last_response |
PayloadStatus |
Last EL response |
CL tells EL to make a block canonical.
Precondition: Block must be in el_validated
Effect: Both cl_head and el_head update to block_hash
CL references a block EL doesn't know.
Precondition: Block NOT in el_validated
Effect: Returns Syncing, no state change
CL asks EL to build a new block.
Precondition: Parent validated, payload_id not in use
Effect: EL starts building, returns payload_id
Internal EL action - block construction completes.
Effect: Block moves from el_building to el_built, added to el_validated
CL retrieves built block.
Precondition: Block must be in el_built
Effect: Read-only
CL sends block for validation, EL accepts.
Precondition: Parent validated
Effect: Block added to el_validated
CL sends block for validation, EL rejects.
Effect: Returns Invalid, no state change
EL's canonical head is always a validated block.
el_validated.contains(el_head)
Why it matters: If violated, EL could report an invalid block as canonical.
After a successful call, CL and EL agree on head.
last_response == Valid implies cl_head == el_head
Why it matters: CL and EL must have consistent views to avoid forks.
Genesis never leaves the validated set.
el_validated.contains(GENESIS)
Why it matters: Genesis is our trust anchor.
EL-built blocks are always in the validated pool.
el_built.keys().forall(pid => el_validated.contains(el_built.get(pid)))
Why it matters: EL trusts its own work.
The spec models el_validated as a Set[BlockHash], not an ordered chain. We're modeling what the EL knows about (which blocks have been validated), not the chain structure itself.
If we needed to model reorgs, fork choice, or block heights, we'd add structure:
var parent: BlockHash -> BlockHash // parent pointers
- Reorgs - Model CL switching to a different fork
- SYNCING resolution - Model EL catching up
- Multiple validators - Concurrent block proposals
- Timeouts - What happens when EL doesn't respond
- ACCEPTED status - For optimistic sync scenarios