Skip to content

fix(focil): FOCIL interop fixes for Besu/Lodestar devnet#35

Merged
eserilev merged 13 commits into
focilfrom
focil-interop-fixes
May 17, 2026
Merged

fix(focil): FOCIL interop fixes for Besu/Lodestar devnet#35
eserilev merged 13 commits into
focilfrom
focil-interop-fixes

Conversation

@eserilev

@eserilev eserilev commented May 6, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes multiple interop issues discovered while running a FOCIL devnet with Lighthouse + Besu + Lodestar.

Engine API fixes

  • Force V6 for Heze blocks: Besu supports engine_newPayloadV6 but capability detection was falling through to V5 (without il_transactions), causing -38005 Unsupported fork errors.
  • Pass parentHash to engine_getInclusionListV1: Was sending empty params, but Besu/Lodestar expect [parentHash].
  • Skip engine_isInclusionListSatisfiedV1: Besu doesn't implement this method. Lodestar handles IL satisfaction via the il_transactions param in newPayloadV6 instead.

BN inclusion list production fix

  • Use fork choice head_hash() for IL production: In ePBS, the execution payload is in the envelope, not the beacon block. produce_inclusion_list was trying to extract parent_hash from the beacon block's payload (which doesn't exist). Now uses cached_head.head_hash() which correctly tracks envelope imports.

VC timing fix

  • Payload envelope SSE event trigger for IL service: Added a payload envelope monitor (following the existing beacon_head_monitor pattern) that races the IL deadline (66.67% of slot) against the ExecutionPayloadAvailable SSE event. Matches Lodestar's Promise.race approach.

Results

Before: Lighthouse couldn't produce any blocks post-Heze (100% miss rate)
After: Devnet stable, finalizing, ILs being produced, ~18% miss rate comparable to Lodestar on same hardware

Note

First commit includes pre-existing uncommitted WIP (early envelope reprocessing, is_heze_fork plumbing) that was on the focil branch working tree.

Devnet Bot added 5 commits May 6, 2026 13:16
- Force engine_newPayloadV6 for Heze blocks (Besu supports V6 but
  capability detection was falling through to broken V5 path)
- Pass parentHash to engine_getInclusionListV1 (was sending empty
  params, Besu/Lodestar expect the parent block hash)
Besu does not implement this method. Lodestar handles IL satisfaction
checking via the il_transactions param in newPayloadV6 instead of a
separate engine call. Skip the call and assume satisfied for now.
In ePBS/Gloas, the execution payload is delivered via envelope, not
embedded in the beacon block. produce_inclusion_list was trying to
extract parent_hash from the beacon block's execution payload (which
doesn't exist), causing every IL fetch to fail.

Use cached_head.head_hash() which correctly tracks the execution
block hash through fork choice, including envelope imports.
Add a payload envelope monitor that subscribes to the
ExecutionPayloadAvailable SSE event (following the existing
beacon_head_monitor pattern). The inclusion list service now races
the IL deadline (66.67% into slot) against the payload envelope
event, matching Lodestar's Promise.race approach.

This ensures IL production fires as soon as the envelope is imported
(when the EL has fresh state) rather than at a fixed offset that may
be too early or too late.
@eserilev eserilev force-pushed the focil-interop-fixes branch from 56702a5 to 2388acc Compare May 6, 2026 13:22
Devnet Bot and others added 8 commits May 6, 2026 14:29
When engine_newPayloadV6 fails with a transient error (e.g. Besu's
ConcurrentModificationException), queue the envelope for retry instead
of permanently rejecting it. Matches Lodestar's behavior of retrying
on the next BlockImported event.

- Add RetryEnvelope variant to ReprocessQueueMessage
- On BlockImported, immediately dispatch any pending retry envelopes
- Fallback timeout of 1 slot in case no block arrives
- Max 3 retries per envelope to prevent infinite loops
- Only retry non-penalizing EL errors (transient failures)
During fork transitions, the head state may not yet reflect the new
fork version. Use spec.fork_at_epoch() instead of state.fork().
Remove non-spec attestation deadline check that was incorrectly
rejecting ILs arriving in the first 1/3 of the slot. Per the heze
p2p-interface spec, ILs are valid when message.slot == current_slot
(with MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance).

Also accept current_slot + 1 for clock disparity tolerance.
@eserilev eserilev merged commit a67985d into focil May 17, 2026
27 of 34 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant