Skip to content

fix: sync ream with lighthouse in a kurtosis devnet - #18

Open
perfogic wants to merge 17 commits into
developfrom
feat/kurtosis-integration
Open

fix: sync ream with lighthouse in a kurtosis devnet#18
perfogic wants to merge 17 commits into
developfrom
feat/kurtosis-integration

Conversation

@perfogic

@perfogic perfogic commented Aug 8, 2026

Copy link
Copy Markdown
Member

Summary

Enable Ream to interoperate with Lighthouse in a one-Ream/one-Lighthouse Kurtosis devnet.

  1. The main change keeps the execution layer aligned with consensus fork choice:
  • Keep the execution layer aligned with consensus by sending the current head, safe, and finalized execution hashes after block imports and other fork-choice changes.
  • Serialize forkchoice updates and recompute the state before each Engine API call, preventing older concurrent imports from overwriting a newer execution head.
  1. Additional interoperability fixes:
  • Carry prev_randao into the execution header mix_hash. (we have some mismatch in EIP)
  • Build the transactions trie from raw EIP-2718 transaction bytes.
  • Serialize B32 fork versions with exactly one 0x prefix.
  • Return spec-compliant responses from /node/syncing and /config/spec.
  • Support head and genesis block IDs.
  • Advertise the custody group count expected from a supernode.
  • Handle pre-genesis startup without time underflow or panics.
  • Install CA certificates for the runtime image - Dockerfile.
  • Add block-import, state-transition, and Engine API timing metrics.

Scope

This PR targets Lighthouse interoperability. The devnet runs without blob load, so data availability is not exercised. Import throughput and finality performance remain follow-up work.

Result

Ream running with Lighthouse

To-Do

@perfogic perfogic changed the title feat: kurtosis integration fix: sync ream with lighthouse in a kurtosis devnet Aug 8, 2026
@perfogic
perfogic marked this pull request as ready for review August 8, 2026 14:06
@perfogic
perfogic force-pushed the feat/kurtosis-integration branch from 434a1c7 to 7a7c9dc Compare August 12, 2026 08:34
@perfogic perfogic self-assigned this Aug 12, 2026
The `ubuntu` base ships no trust store, and reqwest panics while *building* a
client when no system roots load, so the node died right after database
initialization and never reached the network. This bites even on a plain-HTTP
deployment such as a local Kurtosis devnet, because the panic happens in
`Client::new()` rather than on a request.
EIP-4399 repurposed the execution header's `mixHash` slot to hold the beacon
chain's `prev_randao`; the two layers just spell the same value differently.
Rebuilding the header with a zero `mix_hash` made the recomputed hash differ
from every real block, so `is_valid_block_hash` rejected each payload before
the execution engine was ever consulted — the node imported nothing and its
EL never saw a single `newPayload`, while the logs showed only a generic
condition failure.

Add golden vectors built from a real block so a dropped or wrongly mapped
header field fails loudly instead of silently stalling the node. Existing
end-to-end tests could not catch this: they run against the mock execution
engine, which never exercises this path.
A devnet with a genesis delay routinely starts nodes ahead of genesis, and
two separate paths treated that as impossible:

`on_tick` computed `time - genesis_time` on unsigned values. In a release
build that wraps rather than panics, so `tick_slot` became ~u64::MAX and the
slot catch-up loop effectively never ended. The network manager task sat
inside `process_tick` forever: no gossip was processed at all, and the store's
slot climbed quadratically — `/eth/v1/node/syncing` reported a sync distance
in the tens of thousands on a chain that was a dozen slots long.

`slot_n_days_ago` panicked outright. It runs on the block range syncer's task,
and `current_epoch` now funnels through it as well, so the panic reaches
gossip topic construction and message validation rather than one lone caller.

Return early and clamp respectively, matching how other clients model
pre-genesis: a node that starts early is at slot 0, not in an error state.
Note that clamping is only safe here because `on_tick` returns before storing
anything — writing the pre-genesis time would walk the store's slot backwards
and hang startup.
Starting before genesis is normal and deliberate — p2p, discovery and the HTTP
API all stay up so the gossip mesh is formed by slot 0 — but until now the node
looked indistinguishable from one that was wedged.

The tick keeps doing its work while waiting: `on_tick` is already a no-op
before genesis, and skipping the rest freezes the store's slot clock, which
makes blocks arriving just after genesis look future-dated and drops them.

Read genesis time from the store rather than the spec's MIN_GENESIS_TIME, so
the countdown stays correct when GENESIS_DELAY is non-zero.
A fork digest that differs by one byte is invisible locally but fatal on the
wire: peers reject our Status and we subscribe to the wrong gossip topics.
Cover the EIP-7892 masking against Lighthouse's own vector, plus the two
boundary behaviours that are easy to regress — the digest only changing on a
BPO entry, and no masking at all before Fulu.
Payload transactions arrive as EIP-2718 envelopes and belong in the transactions
trie exactly as they are. Encoding them through `Encodable` wrapped each one in a
second RLP string header, so the computed root — and with it the header hash —
matched no other client.

Only blocks carrying at least one transaction were affected, which is why this
survived alongside the `mix_hash` fix: an empty block hashes to the empty-trie
root either way, so the node would import the first block of a devnet and then
reject every block after it, reporting nothing beyond a parent-not-found cascade.

Cover it with the transactions root of a real block, since the shape of the bug
is that our answer is self-consistent and simply not the one everyone else computes.
`/eth/v1/node/syncing` nested the status one level deeper than the Beacon API
spec, under an extra `sync_status` object. Spec-compliant consumers cannot parse
that: assertoor reported "get node syncing not supported" and listed the node as
offline while it was in fact fully synced and importing blocks.
`engine_newPayload` only hands a block to the execution layer for validation. Until
a forkchoice update arrives it keeps its previous head, reports itself as syncing
and answers `eth_*` queries from a chain that never advances — so a node that only
follows the chain, rather than proposing on it, left its execution layer parked at
genesis. Until now the call was made solely from the block production paths.

Notify on each of the three import paths (gossip, and both routes where a block
that was pending data availability becomes available). The forkchoice state is
built while the store guard is held and the guard released before the request, so
the lock is never held across the network call, and a failed notification only
logs: the block is already imported and reverting that would be worse.
The endpoint returned four hardcoded entries where other clients return ~190.
Tooling diffs this across clients to decide whether they are on the same chain, so
the gaps read as disagreement rather than omission: assertoor reported "invalid
node specs: spec mismatch" over a dozen fields and marked the node offline while
it was following the chain.

Serialize the configured network spec and add the preset and domain constants the
node was compiled against, quoting every integer the way the Beacon API does.
Gloas is reported as far future, which is how a client says "not scheduled" —
leaving it out reads as a disagreement instead.

Serializing the spec for the first time exposed `b32_hex` prefixing its output with
`0x` on top of the `0x` that `hex::encode` already adds. Fork versions went out as
`0x0x10000038`, which still passes for hex at a glance but is rejected wholesale.
Both were rejected with a 404, so `/eth/v1/beacon/headers/head` — the ordinary way
to ask a node where it is — returned "This ID type is currently not supported"
and any consumer polling it concluded the node was offline.

Head resolves through fork choice, which reads entirely out of the database, and
genesis through the slot index.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Improves Ream interoperability with Lighthouse in Kurtosis devnets.

Changes:

  • Fixes pre-genesis timing, custody metadata, and container trust-store setup.
  • Corrects execution payload hashing and adds execution fork-choice updates.
  • Aligns Beacon API responses with expected formats and adds regression vectors.

Reviewed changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
Dockerfile Installs CA certificates.
crates/rpc/beacon/src/handlers/syncing.rs Fixes syncing response shape.
crates/rpc/beacon/src/handlers/config.rs Expands specification response.
crates/rpc/beacon/src/handlers/block.rs Supports head and genesis identifiers.
crates/networking/p2p/src/network/beacon/mod.rs Advertises supernode custody.
crates/networking/manager/src/service.rs Configures custody and pre-genesis logging.
crates/common/network_spec/src/networks/beacon.rs Adds serialization and safe pre-genesis slots.
crates/common/network_spec/src/b32_hex.rs Fixes duplicated hexadecimal prefixes.
crates/common/network_spec/Cargo.toml Adds test dependency.
crates/common/fork_choice/beacon/src/store.rs Prevents pre-genesis underflow.
crates/common/fork_choice/beacon/src/handlers.rs Makes pre-genesis ticks no-ops.
crates/common/execution/rpc_types/src/electra/execution_payload.rs Corrects execution-header and transaction roots.
crates/common/execution/rpc_types/resources/transactions_root_block.txt Adds real transaction-root fixture.
crates/common/consensus/misc/src/fork_data.rs Adds fork-digest vectors.
crates/common/chain/beacon/src/beacon_chain.rs Sends execution fork-choice updates.
crates/common/chain/beacon/Cargo.toml Adds execution RPC types dependency.
Cargo.lock Records dependency changes.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread crates/rpc/beacon/src/handlers/config.rs
Comment thread crates/rpc/beacon/src/handlers/config.rs Outdated
Comment thread crates/common/chain/beacon/src/beacon_chain.rs Outdated
Comment thread crates/common/chain/beacon/src/beacon_chain.rs Outdated
tosynthegeek and others added 6 commits August 18, 2026 16:41
Start validators and voluntary-exit epoch calculation at slot zero when the current time is before genesis.
Expose SECONDS_PER_SLOT, serialize terminal difficulty as decimal, and quote nested integer values.
Recompute the latest state under an ordering guard and deduplicate updates from blocks, attestations, slashings, and slot ticks.
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.

3 participants