-
Notifications
You must be signed in to change notification settings - Fork 33
refactor(l1): simplify sync logic #1380
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
96869ef
396a8d5
1f562d5
64835cb
0ec7f1e
6f517ae
6df5950
7af09b8
efe642b
2066135
53bd0bc
0df7fd0
77c9de1
e9be05d
3035c17
50d4741
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -517,79 +517,55 @@ where | |
|
|
||
| /// Determine the starting block number for backfill. | ||
| /// | ||
| /// The zone's persisted Tempo checkpoint is the authoritative source for | ||
| /// where ingestion resumes. A non-zero hash distinguishes an L1-anchored | ||
| /// block-zero genesis from the unanchored template. | ||
| /// Resume after the furthest block represented by durable Zone state or | ||
| /// the subscriber's in-memory queue and tracker. | ||
| pub(crate) fn resolve_start_block(&self) -> Result<u64, L1SubscriberError> { | ||
| let state = self.zone_provider.latest().map_err(eyre::Report::from)?; | ||
| let local_checkpoint = state.tempo_num_hash().map_err(eyre::Report::from)?; | ||
| if local_checkpoint.hash == B256::ZERO { | ||
| return Err(eyre::eyre!("zone genesis is not anchored to an L1 block").into()); | ||
| } | ||
| let local_tempo_block_number = local_checkpoint.number; | ||
| info!(local_tempo_block_number, "Resuming from local zone state"); | ||
| Ok(local_tempo_block_number + 1) | ||
| } | ||
|
|
||
| /// Resolve the first L1 block that has not already been ingested. | ||
| pub(crate) fn next_block_to_sync(&self) -> Result<u64, L1SubscriberError> { | ||
| let resolved = self.resolve_start_block()?; | ||
| let queued = self | ||
| .deposit_queue | ||
| .last_enqueued() | ||
| .map(|last| last.number.saturating_add(1)); | ||
| let observed = self | ||
| .block_tracker | ||
| .latest() | ||
| .map(|last| last.number.saturating_add(1)); | ||
|
|
||
| let next = [Some(resolved), queued, observed] | ||
| .into_iter() | ||
| .flatten() | ||
| .max() | ||
| .expect("resolved checkpoint is always present"); | ||
|
|
||
| // Only the persisted zone checkpoint proves consumption. | ||
| // Queue and observation cursors are fetch high-water marks. | ||
| self.block_tracker | ||
| .initialize_consumed_through(resolved.saturating_sub(1)); | ||
| Ok(next) | ||
| let next_block = local_checkpoint | ||
| .number | ||
| .saturating_add(1) | ||
| .max( | ||
| self.deposit_queue | ||
| .last_enqueued() | ||
| .map_or(0, |block| block.number.saturating_add(1)), | ||
| ) | ||
| .max(self.block_tracker.next_observation_number().unwrap_or(0)); | ||
| info!( | ||
| local_tempo_block_number = local_checkpoint.number, | ||
| next_block, "Resuming L1 sync" | ||
| ); | ||
| Ok(next_block) | ||
| } | ||
|
|
||
| /// Return the block number referenced by the L1 `finalized` tag. | ||
| async fn finalized_block_number( | ||
| /// Synchronize all missing blocks through the current finalized L1 head. | ||
| /// | ||
| /// The cursor advances after each block is fully applied. | ||
| pub(crate) async fn sync_finalized( | ||
| &self, | ||
| l1_provider: &impl Provider<TempoNetwork>, | ||
| ) -> Result<u64, L1SubscriberError> { | ||
| Ok(l1_provider | ||
| next_block: &mut u64, | ||
| ) -> Result<(), L1SubscriberError> { | ||
| let finalized = l1_provider | ||
| .get_header_by_number(BlockNumberOrTag::Finalized) | ||
| .await | ||
| .inspect_err(|_| self.subscriber_metrics.fetch_failures.increment(1))? | ||
| .map(|header| header.number()) | ||
| .ok_or_eyre("L1 finalized block is not available")?) | ||
| } | ||
| .ok_or_eyre("L1 finalized block is not available")?; | ||
|
|
||
| /// Synchronize all missing blocks through the current finalized L1 head. | ||
| /// | ||
| /// Callers provide the next block number and receive the next cursor after | ||
| /// a successful sync. | ||
| pub(crate) async fn sync_finalized_once( | ||
| &self, | ||
| l1_provider: &impl Provider<TempoNetwork>, | ||
| next_block: u64, | ||
| ) -> Result<u64, L1SubscriberError> { | ||
| let finalized = self.finalized_block_number(l1_provider).await?; | ||
| if next_block > finalized { | ||
| self.record_seen_block(finalized, 0); | ||
| return Ok(next_block); | ||
| let pending_blocks = finalized.saturating_sub(next_block.saturating_sub(1)); | ||
| self.record_seen_block(finalized, pending_blocks); | ||
| if pending_blocks == 0 { | ||
| return Ok(()); | ||
| } | ||
|
|
||
| let blocks = finalized - next_block + 1; | ||
| self.record_seen_block(finalized, blocks); | ||
| info!( | ||
| from = next_block, | ||
| from = *next_block, | ||
| to = finalized, | ||
| blocks, | ||
| blocks = pending_blocks, | ||
| "Synchronizing finalized L1 blocks" | ||
| ); | ||
|
|
||
|
|
@@ -599,29 +575,7 @@ where | |
| .backfill_duration_seconds | ||
| .record(start.elapsed().as_secs_f64()); | ||
| self.subscriber_metrics.current_l1_lag_blocks.set(0.0); | ||
| Ok(finalized.saturating_add(1)) | ||
| } | ||
|
|
||
| /// Follow finalized L1 using transport-specific head notifications as wakeups. | ||
| /// | ||
| /// Header contents are intentionally ignored. Canonical block selection is | ||
| /// always based on the `finalized` tag read by [`Self::sync_finalized_once`]. | ||
| pub(crate) async fn follow_finalized( | ||
| &self, | ||
| l1_provider: &impl Provider<TempoNetwork>, | ||
| mut stream: HeaderStream, | ||
| ) -> Result<(), L1SubscriberError> { | ||
| let mut next_block = self.next_block_to_sync()?; | ||
|
|
||
| // Subscribe before the initial sync so a head published while catching | ||
| // up remains queued in the stream. | ||
| next_block = self.sync_finalized_once(l1_provider, next_block).await?; | ||
|
|
||
| while stream.next().await.is_some() { | ||
| next_block = self.sync_finalized_once(l1_provider, next_block).await?; | ||
| } | ||
|
|
||
| Err(eyre::eyre!("L1 head notification stream ended").into()) | ||
| Ok(()) | ||
| } | ||
|
|
||
| /// Backfill L1 blocks from `from..=to` with pipelined RPC fetching. | ||
|
|
@@ -630,15 +584,16 @@ where | |
| /// parallel, then processes them sequentially (event extraction and enqueue). | ||
| /// Receipts are fetched by the corresponding block | ||
| /// hash and validated against the header's receipts root before processing. | ||
| #[instrument(skip(self, l1_provider), fields(from, to))] | ||
| #[instrument(skip(self, l1_provider, next_block), fields(from = *next_block, to))] | ||
| async fn backfill( | ||
| &self, | ||
| l1_provider: &impl Provider<TempoNetwork>, | ||
| from: u64, | ||
| next_block: &mut u64, | ||
| to: u64, | ||
| ) -> Result<(), L1SubscriberError> { | ||
| use futures::stream; | ||
|
|
||
| let from = *next_block; | ||
| let concurrency = self.config.l1_fetch_concurrency.max(1); | ||
| let subscriber_metrics = self.subscriber_metrics.clone(); | ||
| let block_tracker = self.block_tracker.clone(); | ||
|
|
@@ -766,6 +721,7 @@ where | |
| // configured retention sink and the contiguous observation tracker. | ||
| self.apply_enabled_token_events(&events); | ||
| self.update_l1_state_anchor(block_number, &invalidated); | ||
| *next_block = block_number.saturating_add(1); | ||
| if appended { | ||
| self.subscriber_metrics.blocks_enqueued.increment(1); | ||
| } | ||
|
|
@@ -804,16 +760,28 @@ where | |
| /// | ||
| /// Transport and ordinary failures reconnect after the configured retry interval. | ||
| /// Deterministic failures while applying a receipt-verified finalized block are fatal. | ||
| pub async fn run(self) { | ||
| pub async fn run(self) -> Result<(), L1SubscriberError> { | ||
| loop { | ||
| let result = async { | ||
| let result: Result<(), L1SubscriberError> = async { | ||
| let mut next_block = self.resolve_start_block()?; | ||
|
0xKitsune marked this conversation as resolved.
|
||
| self.block_tracker | ||
| .initialize_consumed_through(next_block.saturating_sub(1)); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚨 [POTENTIAL-VULNERABILITY] Reconnects can promote fetched L1 blocks to consumed and bypass the lookahead limit
Because Recommended Fix: |
||
|
|
||
| let provider = self.connect().await?; | ||
| let header_stream = self.subscribe_block_headers(&provider).await?; | ||
| let mut header_stream = self.subscribe_block_headers(&provider).await?; | ||
| info!( | ||
| portal = %self.config.portal_address, | ||
| "Following finalized L1 blocks" | ||
| ); | ||
| self.follow_finalized(&provider, header_stream).await | ||
|
|
||
| // Subscribe before the initial sync so a head published while catching | ||
| // up remains queued in the stream. | ||
| self.sync_finalized(&provider, &mut next_block).await?; | ||
| while let Some(()) = header_stream.next().await { | ||
| self.sync_finalized(&provider, &mut next_block).await?; | ||
| } | ||
|
|
||
| Err(eyre::eyre!("L1 head notification stream ended").into()) | ||
| } | ||
| .await; | ||
|
|
||
|
|
@@ -829,7 +797,7 @@ where | |
| ); | ||
| tokio::time::sleep(retry_interval).await; | ||
| } else { | ||
| panic!("{error}"); | ||
| return Err(error); | ||
| } | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This resolves to the block that the engine has processed successfully, but if there is a L1 disconnect/reconnect, because
next_blockis inside the loop, it will get reset everytime, re-fetching blocks it already fetched. It might be a good idea to move the next_block declaration outside the loop, so it remembers what the last block fetched was and continues from there. (Although this might complicate error handling, so it might not be worth it)