diff --git a/packages/taiko-client-rs/crates/whitelist-preconfirmation-driver/src/api/service/mod.rs b/packages/taiko-client-rs/crates/whitelist-preconfirmation-driver/src/api/service/mod.rs index 6730606378..21684065b1 100644 --- a/packages/taiko-client-rs/crates/whitelist-preconfirmation-driver/src/api/service/mod.rs +++ b/packages/taiko-client-rs/crates/whitelist-preconfirmation-driver/src/api/service/mod.rs @@ -71,14 +71,18 @@ fn can_shutdown_for(last_preconf_request: Option) -> 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 { - head.map_or(tracked, |h| tracked.min(h)) + head.unwrap_or(tracked) } /// Implements whitelist preconfirmation API business logic. diff --git a/packages/taiko-client-rs/crates/whitelist-preconfirmation-driver/src/api/service/status.rs b/packages/taiko-client-rs/crates/whitelist-preconfirmation-driver/src/api/service/status.rs index 1fd816f120..dda36525f2 100644 --- a/packages/taiko-client-rs/crates/whitelist-preconfirmation-driver/src/api/service/status.rs +++ b/packages/taiko-client-rs/crates/whitelist-preconfirmation-driver/src/api/service/status.rs @@ -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 { // 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 @@ -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(); diff --git a/packages/taiko-client-rs/crates/whitelist-preconfirmation-driver/src/api/service/tests.rs b/packages/taiko-client-rs/crates/whitelist-preconfirmation-driver/src/api/service/tests.rs index 760cdf2112..9f46155927 100644 --- a/packages/taiko-client-rs/crates/whitelist-preconfirmation-driver/src/api/service/tests.rs +++ b/packages/taiko-client-rs/crates/whitelist-preconfirmation-driver/src/api/service/tests.rs @@ -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); } @@ -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]