refactor(l1): simplify sync functionality - #1380
Conversation
|
cyclops audit fast |
|
cc @0xKitsune Cyclops audit event published. View workflow run Config: config: |
tempoxyz-bot
left a comment
There was a problem hiding this comment.
👁️ Cyclops Review
This PR keeps a single in-memory L1 sync cursor across reconnects and advances it after each finalized block is fully applied. The cursor-preservation path looks safer for partial retries, but one startup error path now bypasses retry handling, and there is one body-only defense-in-depth note below.
Body-only finding
🛡️ [DEFENSE-IN-DEPTH] Validate fetched header height before applying queue side effects
crates/l1/src/subscriber.rs:602 requests block block_number, but the returned header's own number() is not checked before receipts are fetched by the returned hash and before later side effects use header.number(). A hostile or misbehaving RPC endpoint can return a real finalized header for a later height when the queue is empty; the queue can admit that later height before the tracker rejects it for checkpoint discontinuity, leaving ingestion stuck on retryable out-of-order errors. This appears pre-existing and bounded to liveness because downstream consumers re-check checkpoint continuity.
Recommended Fix: Fail closed immediately after get_header_by_number(block_number.into()) unless header.number() == block_number. This also makes the progress logging arithmetic robust against headers above the intended to range.
Reviewer Callouts
- ⚡ L1 cursor invariant: The refactor removes the previous queue/tracker high-water recomputation and relies on a local
next_block. Consider adding a debug assertion nearcrates/l1/src/subscriber.rs:714tyingnext_blockto the queue/tracker tips so future changes cannot silently break the invariant. - ⚡ Backfill progress arithmetic:
remaining = to - block_numberatcrates/l1/src/subscriber.rs:727is safe only if returned headers cannot exceed the requested range. If height validation is not added, switch this toto.saturating_sub(block_number).
| /// Deterministic failures while applying a receipt-verified finalized block are fatal. | ||
| pub async fn run(self) { | ||
| pub async fn run(self) -> Result<(), L1SubscriberError> { | ||
| let mut next_block = self.resolve_start_block()?; |
There was a problem hiding this comment.
run() now calls self.resolve_start_block()? before entering the reconnect/retry loop. Errors from provider.latest() or tempo_num_hash() are classified as retryable Other errors elsewhere, but here they are returned directly to the caller, which panics the critical task. A transient state-provider/storage read failure at subscriber startup can therefore crash the node instead of being retried as it was before this refactor.
Recommended Fix:
Initialize next_block through the same retry policy used for connection/sync errors, or add a small pre-loop retry around resolve_start_block() that retries provider/storage errors while still treating configuration errors such as an unanchored genesis as fatal.
This PR simplifies finalized L1 synchronization by keeping one next-block cursor across RPC reconnects. It removes queue and tracker cursor reconstruction and keeps the subscription loop directly in
run, so partial backfills resume from the first unfinished block.