feat(host-reth): replace ExEx backfill with direct DB reads#136
feat(host-reth): replace ExEx backfill with direct DB reads#136
Conversation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduces three types for reading host-chain blocks from the reth DB: - `DbBlock`: owned block+receipts pair from the provider - `DbChainSegment`: newtype over `Vec<DbBlock>` implementing `Extractable` using the same `RecoveredBlockShim` transmute pattern as `RethChain` - `DbBackfill<P>`: batch reader that walks from a cursor to the finalized block, recording metrics per batch via `crate::metrics` Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds `HostChain` enum with `Backfill(DbChainSegment)` and `Live(RethChain)` variants, both delegating to the inner `Extractable` impl. Promotes `DbChainSegment` to `pub` and re-exports both new types from the crate root. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace reth's built-in ExEx backfill with DbBackfill for startup catch-up. The notifier now runs a two-phase loop: phase 1 drains DB batches via DbBackfill, then phase 2 switches to live ExEx notifications. set_head initializes backfill instead of resolving a header directly, and set_backfill_thresholds configures DbBackfill batch size. Chain type changes from RethChain to HostChain enum. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| const { | ||
| assert!( | ||
| size_of::<RecoveredBlockShim>() == size_of::<RethRecovered>(), | ||
| "RecoveredBlockShim layout diverged from RethRecovered" | ||
| ); | ||
| assert!( | ||
| align_of::<RecoveredBlockShim>() == align_of::<RethRecovered>(), | ||
| "RecoveredBlockShim alignment diverged from RethRecovered" | ||
| ); | ||
| } |
| let backfill = self.backfill.take().expect("backfill was Some"); | ||
| let last_backfilled = backfill.cursor().saturating_sub(1); | ||
|
|
||
| let head = self |
There was a problem hiding this comment.
so, this behavior of fetching the last seen block, and if that fails, fall back to genesis, stems from a probably still unresolved bug where Reth starts the exex before its connected to its own db provider (that's roughly what I remember). This ofc complicates the logic quite a bit by adding the genesis fallback path.
Considering that most of the time we might be falling into phase 1 when restarting the node to catch up with a few blocks, maybe we can simplify this?
There was a problem hiding this comment.
[Claude Code]
Good call — ripped it out in ca2b6c2. A bit of digging in reth turned up #19665 / #22168 (merged Feb 2026), which fixed the backfill notification-channel stall that I believe was producing the symptom you remembered. And at this specific call site, DbBackfill has already successfully read last_backfilled from the same provider moments earlier, so if sealed_header returns None here it's DB corruption, not a startup race. Now returns RethHostError::MissingHeader instead of quietly pretending we're at genesis.
There was a problem hiding this comment.
💆💆💆💆💆 feels good to remove this piece of code, always made me feel bad having it
- `set_backfill_thresholds(None)` now resets the batch size to
`DEFAULT_BATCH_SIZE` via a new `DbBackfill::reset_batch_size`,
matching the trait contract ("`None` means use the backend's
default") and the `RpcHostNotifier` implementation.
- Remove the genesis fallback after backfill completion. The
documented ExEx startup race it was defending against
(reth #19665 / #22168) was fixed upstream, and in any case
`DbBackfill` just read `last_backfilled` from the same provider,
so a missing header at this point indicates DB-level failure.
Now returns `RethHostError::MissingHeader` instead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
DbBackfill<P>reads blocks+receipts in batches up to finalized, then hands off to the ExEx stream for live blocksHostChainenum unifies backfill and live chain segments behindExtractableset_headdocumented and enforced as once-only across both notifier implementationsDetails
The ExEx backfill mechanism re-executes historical blocks on startup, which is slow and has caused memory issues. The reth DB already contains executed results, making re-execution unnecessary.
New types (
signet-host-reth):DbBlock/DbChainSegment— owned block+receipts from DB, implementsExtractableDbBackfill<P>— batch reader usingspawn_blockingfor MDBX readsHostChain— enum wrappingDbChainSegment(backfill) andRethChain(live)Notifier changes:
RethHostNotifier::next_notificationis now two-phase: drain DB backfill, then switch to ExExset_headcreates aDbBackfillinstead of calling ExExset_with_headreth-stages-typesdependency removedCross-crate:
HostNotifier::set_headtrait doc clarified as once-onlyRpcHostNotifier::set_headguards against repeated callsReview follow-ups
set_backfill_thresholds(None)now resets toDEFAULT_BATCH_SIZEvia a newDbBackfill::reset_batch_size, matching the trait contract andRpcHostNotifier.DbBackfillhas just successfully readlast_backfilledfrom the same provider — so a missing header there now indicates DB-level failure and returnsRethHostError::MissingHeader.Closes ENG-1784
Test plan
cargo clippypasses (both--all-featuresand--no-default-features)RUSTDOCFLAGS="-D warnings" cargo docpassessignet-host-reth,signet-node-types,signet-host-rpc)signet-nodecompiles cleanly with newHostChaintype🤖 Generated with Claude Code