Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,18 @@ fn can_shutdown_for(last_preconf_request: Option<Instant>) -> bool {
}
}

/// Clamp `tracked` down to `head`; never raise it, and leave it unchanged when `head`
/// is `None`.
/// Report `head` whenever it is known; fall back to `tracked` when it is `None`.
///
/// The tracked value only ever moves up, so after the head moves backward (e.g. an L1
/// reorg) it can be left above the head. Lowering it restores `tracked <= head` while
/// still preserving a value that legitimately trails the head.
/// The tracked value only moves on preconfirmation imports and local builds, so it can
/// drift from the head in both directions: an L1 reorg rewinds the head below the
/// counter, while canonical L1 derivation with no gossip traffic advances the head past
/// it. Every canonical block was inserted by this driver, so the head is always an
/// honest answer — and the Catalyst sidecar's sync gate requires the reported value to
/// equal the execution head exactly before it starts (or resumes) preconfirming. A
/// permanently lagging report would wedge the operator in a restart loop that only a
/// driver restart clears.
fn reconcile_highest_unsafe(tracked: u64, head: Option<u64>) -> u64 {
head.map_or(tracked, |h| tracked.min(h))
head.unwrap_or(tracked)
}

/// Implements whitelist preconfirmation API business logic.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ where
/// Build the current status snapshot served by the REST `/status` route.
pub(super) async fn get_status_snapshot(&self) -> Result<ApiStatus> {
// Current L2 execution head, best-effort: a failed read yields `None`, which skips
// the clamp below and leaves the reported value unchanged.
// the reconciliation below and reports the tracked counter unchanged.
let l2_head = self
.rpc
.l2_provider
Expand All @@ -19,22 +19,26 @@ where
.flatten()
.map(|block| block.header.number);

// The counter only moves up (on import/build), so after the head moves backward
// (e.g. an L1 reorg) it can be left above the head. Report it clamped to the head.
// The counter only moves on imports/builds, so it can drift from the head in both
// directions: an L1 reorg rewinds the head below it, and canonical L1 derivation
// with no gossip traffic advances the head past it. Report the head in both cases —
// canonical blocks were inserted by this driver too, and the Catalyst sync gate
// only opens when the reported value equals the execution head exactly.
//
// Report only — do NOT write the clamp back. `l2_head` is read without the lock, so
// it can already be stale; persisting `min(counter, stale_head)` would pin the
// stored counter too low until the next import/build. Clamping only the reported
// value keeps the counter intact, so the next poll recomputes against a fresh head
// and self-heals.
// Report only — do NOT write the reconciled value back. `l2_head` is read without
// the lock, so it can already be stale; persisting it could pin the stored counter
// wrong until the next import/build. Reconciling only the reported value keeps the
// counter intact, so the next poll recomputes against a fresh head and self-heals.
let tracked = self.state.highest_unsafe().await;
let highest_unsafe = reconcile_highest_unsafe(tracked, l2_head);
if highest_unsafe != tracked {
if highest_unsafe < tracked {
warn!(
tracked,
head = highest_unsafe,
"highest_unsafe ahead of head; reporting clamped value"
);
} else if highest_unsafe > tracked {
debug!(tracked, head = highest_unsafe, "highest_unsafe behind head; reporting head");
}

let current_epoch = self.beacon_client.current_epoch();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ fn shutdown_block_window_is_one_hundred_forty_four_seconds() {

#[test]
fn reconcile_clamps_down_when_counter_exceeds_reth_head() {
// The L1-reorg wedge: counter stuck above reth's rewound head -> clamp to head.
// The L1-reorg wedge: counter stuck above reth's rewound head -> report the head.
assert_eq!(reconcile_highest_unsafe(5_811_227, Some(5_811_208)), 5_811_208);
}

Expand All @@ -93,9 +93,12 @@ fn reconcile_keeps_counter_when_equal_to_reth_head() {
}

#[test]
fn reconcile_keeps_counter_when_below_reth_head() {
// Legitimate catch-up state (reth ahead via L1 derivation); must NOT be raised.
assert_eq!(reconcile_highest_unsafe(5_811_208, Some(5_811_227)), 5_811_208);
fn reconcile_reports_head_when_counter_below_reth_head() {
// The catch-up wedge: reth advanced via canonical L1 derivation while no gossip was
// flowing, so the counter was never raised. Catalyst's sync gate requires the
// reported value to equal the head exactly; a lagging report blocks preconfirmation
// (and triggers Catalyst self-restarts) until a driver restart re-seeds the counter.
assert_eq!(reconcile_highest_unsafe(5_811_208, Some(5_811_227)), 5_811_227);
}

#[test]
Expand Down
Loading