Skip to main content

tycho_client/feed/
mod.rs

1use std::{
2    collections::{HashMap, HashSet},
3    fmt::{Display, Formatter},
4    time::Duration,
5};
6
7use chrono::{Duration as ChronoDuration, Local, NaiveDateTime};
8use futures03::{future::join_all, stream::FuturesUnordered, FutureExt, StreamExt};
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11use tokio::{
12    sync::{
13        mpsc::{self, Receiver},
14        oneshot,
15    },
16    task::JoinHandle,
17    time::timeout,
18};
19use tracing::{debug, error, info, trace, warn};
20use tycho_common::{
21    display::opt,
22    models::{blockchain::BlockAggregatedChanges, ExtractorIdentity},
23    Bytes,
24};
25
26use crate::feed::{
27    block_history::{BlockHistory, BlockHistoryError, BlockPosition},
28    synchronizer::{StateSyncMessage, StateSynchronizer, SyncResult, SynchronizerError},
29};
30
31mod block_history;
32pub mod component_tracker;
33pub mod dto;
34pub mod synchronizer;
35
36/// Number of block headers retained by the `BlockHistory` ring buffer. Bounds how deep a revert
37/// can unwind before the fork point is evicted. Used both on startup and on reinitialization so
38/// the retained depth stays consistent across an `Advanced`-triggered reinit.
39const BLOCK_HISTORY_SIZE: usize = 15;
40
41/// A trait representing a minimal interface for types that behave like a block header.
42///
43/// This abstraction allows working with either full block headers (`BlockHeader`)
44/// or simplified structures that only provide a timestamp (e.g., for RFQ logic).
45pub trait HeaderLike {
46    fn block(self) -> Option<BlockHeader>;
47    fn block_number_or_timestamp(self) -> u64;
48}
49
50#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, Eq, Hash)]
51pub struct BlockHeader {
52    pub hash: Bytes,
53    pub number: u64,
54    pub parent_hash: Bytes,
55    pub revert: bool,
56    pub timestamp: u64,
57    /// Index of a partial block update within a block. None for full blocks.
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    pub partial_block_index: Option<u32>,
60}
61
62impl BlockHeader {
63    fn is_partial(&self) -> bool {
64        self.partial_block_index.is_some()
65    }
66}
67
68impl Display for BlockHeader {
69    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
70        // Take first 6 hex chars of the hash for readability
71        let short_hash = if self.hash.len() >= 4 {
72            hex::encode(&self.hash[..4]) // 4 bytes → 8 hex chars
73        } else {
74            hex::encode(&self.hash)
75        };
76
77        match self.partial_block_index {
78            Some(idx) => write!(f, "Block #{} [0x{}..] (partial {})", self.number, short_hash, idx),
79            None => write!(f, "Block #{} [0x{}..]", self.number, short_hash),
80        }
81    }
82}
83
84impl From<&BlockAggregatedChanges> for BlockHeader {
85    fn from(block_changes: &BlockAggregatedChanges) -> Self {
86        let block = &block_changes.block;
87        Self {
88            hash: block.hash.clone(),
89            number: block.number,
90            parent_hash: block.parent_hash.clone(),
91            revert: block_changes.revert,
92            timestamp: block.ts.and_utc().timestamp() as u64,
93            partial_block_index: block_changes.partial_block_index,
94        }
95    }
96}
97
98impl HeaderLike for BlockHeader {
99    fn block(self) -> Option<BlockHeader> {
100        Some(self)
101    }
102
103    fn block_number_or_timestamp(self) -> u64 {
104        self.number
105    }
106}
107
108#[derive(Error, Debug)]
109pub enum BlockSynchronizerError {
110    #[error("Failed to initialize extractor '{extractor}': {source}")]
111    InitializationError {
112        extractor: ExtractorIdentity,
113        #[source]
114        source: SynchronizerError,
115    },
116
117    #[error("Failed to process new block: {0}")]
118    BlockHistoryError(#[from] BlockHistoryError),
119
120    #[error("Not a single synchronizer was ready: {0}")]
121    NoReadySynchronizers(String),
122
123    #[error("No synchronizers were set")]
124    NoSynchronizers,
125
126    #[error("Failed to convert duration: {0}")]
127    DurationConversionError(String),
128}
129
130type BlockSyncResult<T> = Result<T, BlockSynchronizerError>;
131
132/// Aligns multiple StateSynchronizers on the block dimension.
133///
134/// ## Purpose
135/// The purpose of this component is to handle streams from multiple state synchronizers and
136/// align/merge them according to their blocks. Ideally this should be done in a fault-tolerant way,
137/// meaning we can recover from a state synchronizer suffering from timing issues. E.g. a delayed or
138/// unresponsive state synchronizer might recover again, or an advanced state synchronizer can be
139/// included again once we reach the block it is at.
140///
141/// ## Limitations
142/// - Supports only chains with fixed blocks time for now due to the lock step mechanism.
143///
144/// ## Initialisation
145/// Queries all registered synchronizers for their first message and evaluates the state of each
146/// synchronizer. If a synchronizer's first message is an older block, it is marked as delayed.
147// TODO: what is the startup timeout
148/// If no message is received within the startup timeout, the synchronizer is marked as stale and is
149/// closed.
150///
151/// ## Main loop
152/// Once started, the synchronizers are queried concurrently for messages in lock step:
153/// the main loop queries all synchronizers in ready for the last emitted data, builds the
154/// `FeedMessage` and emits it, then it schedules the wait procedure for the next block.
155///
156/// ## Synchronization Logic
157///
158/// To classify a synchronizer as delayed, we need to first define the current block. The highest
159/// block number of all ready synchronizers is considered the current block.
160///
161/// Once we have the current block we can easily determine which block we expect next. And if a
162/// synchronizer delivers an older block we can classify it as delayed.
163///
164/// If any synchronizer is not in the ready state we will try to bring it back to the ready state.
165/// This is done by trying to empty any buffers of a delayed synchronizer or waiting to reach
166/// the height of an advanced synchronizer (and flagging it as such in the meantime).
167///
168/// Of course, we can't wait forever for a synchronizer to reply/recover. All of this must happen
169/// within the block production step of the blockchain:
170/// The wait procedure consists of waiting for any of the individual ProtocolStateSynchronizers
171/// to emit a new message (within a max timeout - several multiples of the block time). Once a
172/// message is received a very short timeout starts for the remaining synchronizers, to deliver a
173/// message. Any synchronizer failing to do so is transitioned to delayed.
174///
175/// ### Note
176/// The described process above is the goal. It is currently not implemented like that. Instead we
177/// simply wait `block_time` + `wait_time`. Synchronizers are expected to respond within that
178/// timeout. This is simpler but only works well on chains with fixed block times.
179pub struct BlockSynchronizer<S> {
180    synchronizers: Option<HashMap<ExtractorIdentity, S>>,
181    /// Time to wait for a block usually
182    block_time: std::time::Duration,
183    /// Added on top of block time to account for latency
184    latency_buffer: std::time::Duration,
185    /// Time to wait for the full first message, including snapshot retrieval
186    startup_timeout: std::time::Duration,
187    /// Optionally, end the stream after emitting max messages
188    max_messages: Option<usize>,
189    /// Amount of blocks a protocol can be delayed for, before it is considered stale
190    max_missed_blocks: u64,
191}
192
193#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
194#[serde(tag = "status", rename_all = "lowercase")]
195pub enum SynchronizerState {
196    /// Initial state, assigned before trying to receive any message
197    Started,
198    /// The synchronizer emitted a message for the block as expected
199    Ready(BlockHeader),
200    /// The synchronizer is on a previous block compared to others and is expected to
201    /// catch up soon.
202    Delayed(BlockHeader),
203    /// The synchronizer hasn't emitted messages for > `max_missed_blocks` or has
204    /// fallen far behind. At this point we do not wait for it anymore but it can
205    /// still eventually recover.
206    Stale(BlockHeader),
207    /// The synchronizer is on future not connected block.
208    // For this to happen we must have a gap, and a gap usually means a new snapshot from the
209    // StateSynchronizer. This can only happen if we are processing too slow and one or all of the
210    // synchronizers restarts e.g. due to websocket connection drops.
211    Advanced(BlockHeader),
212    /// The synchronizer ended with an error.
213    Ended(String),
214}
215
216impl Display for SynchronizerState {
217    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
218        match self {
219            SynchronizerState::Started => write!(f, "Started"),
220            SynchronizerState::Ready(b) => write!(f, "Started({})", b.number),
221            SynchronizerState::Delayed(b) => write!(f, "Delayed({})", b.number),
222            SynchronizerState::Stale(b) => write!(f, "Stale({})", b.number),
223            SynchronizerState::Advanced(b) => write!(f, "Advanced({})", b.number),
224            SynchronizerState::Ended(reason) => write!(f, "Ended({})", reason),
225        }
226    }
227}
228
229pub struct SynchronizerStream {
230    extractor_id: ExtractorIdentity,
231    state: SynchronizerState,
232    error: Option<SynchronizerError>,
233    modify_ts: NaiveDateTime,
234    rx: Receiver<SyncResult<StateSyncMessage<BlockHeader>>>,
235}
236
237impl SynchronizerStream {
238    fn new(
239        extractor_id: &ExtractorIdentity,
240        rx: Receiver<SyncResult<StateSyncMessage<BlockHeader>>>,
241    ) -> Self {
242        Self {
243            extractor_id: extractor_id.clone(),
244            state: SynchronizerState::Started,
245            error: None,
246            modify_ts: Local::now().naive_utc(),
247            rx,
248        }
249    }
250
251    /// Advance a synchronizer by one step.
252    ///
253    /// - `block_history`: validated chain of recent blocks used to classify incoming headers.
254    /// - `block_time`: expected time between blocks; sets the base timeout for Ready streams.
255    /// - `latency_buffer`: added on top of `block_time` to absorb network/processing jitter.
256    /// - `stale_threshold`: how long a stream can make no progress before it is marked Stale and
257    ///   skipped.
258    /// - `skip_wait`: skip the blocking wait for all protocol state synchronizers - only advance
259    ///   those that have waiting messages.
260    async fn try_advance(
261        &mut self,
262        block_history: &BlockHistory,
263        block_time: std::time::Duration,
264        latency_buffer: std::time::Duration,
265        stale_threshold: std::time::Duration,
266        skip_wait: bool,
267    ) -> BlockSyncResult<Option<StateSyncMessage<BlockHeader>>> {
268        let extractor_id = self.extractor_id.clone();
269        let latest_block = block_history.latest();
270
271        match &self.state {
272            SynchronizerState::Started | SynchronizerState::Ended(_) => {
273                warn!(state=?&self.state, "Advancing Synchronizer in this state not supported!");
274                Ok(None)
275            }
276            SynchronizerState::Advanced(b) => {
277                let future_block = b.clone();
278                // Transition to ready once we arrived at the expected height
279                self.transition(future_block, block_history, stale_threshold)?;
280                Ok(None)
281            }
282            SynchronizerState::Ready(previous_block) => {
283                // Try to recv the next expected block, update state accordingly.
284                self.try_recv_next_expected(
285                    block_time + latency_buffer,
286                    block_history,
287                    previous_block.clone(),
288                    stale_threshold,
289                )
290                .await
291            }
292            SynchronizerState::Delayed(old_block) => {
293                // try to catch up all currently queued blocks until the expected block
294                debug!(
295                    %old_block,
296                    latest_block=opt(&latest_block),
297                    %extractor_id,
298                    "Trying to catch up to latest block"
299                );
300                let timeout =
301                    if skip_wait { std::time::Duration::ZERO } else { block_time + latency_buffer };
302                self.try_catch_up(block_history, timeout, stale_threshold)
303                    .await
304            }
305            SynchronizerState::Stale(old_block) => {
306                // try to catch up all currently queued blocks until the expected block
307                debug!(
308                    %old_block,
309                    latest_block=opt(&latest_block),
310                    %extractor_id,
311                    "Trying to catch up stale synchronizer to latest block"
312                );
313                let timeout = if skip_wait { std::time::Duration::ZERO } else { block_time };
314                self.try_catch_up(block_history, timeout, stale_threshold)
315                    .await
316            }
317        }
318    }
319
320    /// Standard way to advance a well-behaved state synchronizer.
321    ///
322    /// Will wait for a new block on the synchronizer within a timeout. And modify its
323    /// state based on the outcome.
324    ///
325    /// ## Note
326    /// This method assumes that the current state is `Ready`.
327    async fn try_recv_next_expected(
328        &mut self,
329        max_wait: std::time::Duration,
330        block_history: &BlockHistory,
331        previous_block: BlockHeader,
332        stale_threshold: std::time::Duration,
333    ) -> BlockSyncResult<Option<StateSyncMessage<BlockHeader>>> {
334        let extractor_id = self.extractor_id.clone();
335        match timeout(max_wait, self.rx.recv()).await {
336            Ok(Some(Ok(msg))) => {
337                self.transition(msg.header.clone(), block_history, stale_threshold)?;
338                Ok(Some(msg))
339            }
340            Ok(Some(Err(e))) => {
341                // The underlying synchronizer exhausted its retries
342                self.mark_errored(e);
343                Ok(None)
344            }
345            Ok(None) => {
346                // This case should not happen, as we shouldn't poll the synchronizer after we
347                // closed it or after it errored.
348                warn!(
349                    %extractor_id,
350                    "Tried to poll from closed synchronizer.",
351                );
352                self.mark_closed();
353                Ok(None)
354            }
355            Err(_) => {
356                // trying to advance a block timed out
357                debug!(%extractor_id, %previous_block, "No block received within time limit.");
358                // No need to consider state since we only call this method if we are
359                // in the Ready state.
360                self.state = SynchronizerState::Delayed(previous_block.clone());
361                self.modify_ts = Local::now().naive_utc();
362                Ok(None)
363            }
364        }
365    }
366
367    /// Tries to catch up a delayed state synchronizer.
368    ///
369    /// If a synchronizer is delayed, this method will try to catch up to the next expected block
370    /// by consuming all waiting messages in its queue and waiting for any new block messages
371    /// within a timeout. Finally, all update messages are merged into one and returned.
372    async fn try_catch_up(
373        &mut self,
374        block_history: &BlockHistory,
375        max_wait: std::time::Duration,
376        stale_threshold: std::time::Duration,
377    ) -> BlockSyncResult<Option<StateSyncMessage<BlockHeader>>> {
378        let mut results = Vec::new();
379        let extractor_id = self.extractor_id.clone();
380
381        // Set a deadline for the overall catch-up operation
382        let deadline = std::time::Instant::now() + max_wait;
383
384        while std::time::Instant::now() < deadline {
385            match timeout(
386                deadline.saturating_duration_since(std::time::Instant::now()),
387                self.rx.recv(),
388            )
389            .await
390            {
391                Ok(Some(Ok(msg))) => {
392                    debug!(%extractor_id, block=%msg.header, "Received new message during catch-up");
393                    let block_pos = block_history.determine_block_position(&msg.header)?;
394                    results.push(msg);
395                    if matches!(block_pos, BlockPosition::NextExpected | BlockPosition::NextPartial)
396                    {
397                        break;
398                    }
399                }
400                Ok(Some(Err(e))) => {
401                    // Synchronizer errored during catch up
402                    self.mark_errored(e);
403                    return Ok(None);
404                }
405                Ok(None) => {
406                    // This case should not happen, as we shouldn't poll the synchronizer after we
407                    // closed it or after it errored.
408                    warn!(
409                        %extractor_id,
410                        "Tried to poll from closed synchronizer during catch up.",
411                    );
412                    self.mark_closed();
413                    return Ok(None);
414                }
415                Err(_) => {
416                    debug!(%extractor_id, "Timed out waiting for catch-up");
417                    break;
418                }
419            }
420        }
421
422        let merged = results
423            .into_iter()
424            .reduce(|l, r| l.merge(r));
425
426        if let Some(msg) = merged {
427            // we were able to get at least one block out
428            debug!(%extractor_id, "Delayed extractor made progress!");
429            self.transition(msg.header.clone(), block_history, stale_threshold)?;
430            Ok(Some(msg))
431        } else {
432            // No progress made during catch-up, check if we should go stale
433            self.check_and_transition_to_stale_if_needed(stale_threshold, None)?;
434            Ok(None)
435        }
436    }
437
438    /// Helper method to check if synchronizer should transition to stale based on time elapsed
439    fn check_and_transition_to_stale_if_needed(
440        &mut self,
441        stale_threshold: std::time::Duration,
442        fallback_header: Option<BlockHeader>,
443    ) -> Result<bool, BlockSynchronizerError> {
444        let now = Local::now().naive_utc();
445        let wait_duration = now.signed_duration_since(self.modify_ts);
446        let stale_threshold_chrono = ChronoDuration::from_std(stale_threshold)
447            .map_err(|e| BlockSynchronizerError::DurationConversionError(e.to_string()))?;
448
449        if wait_duration > stale_threshold_chrono {
450            let header_to_use = match (&self.state, fallback_header) {
451                (SynchronizerState::Ready(h), _) |
452                (SynchronizerState::Delayed(h), _) |
453                (SynchronizerState::Stale(h), _) => h.clone(),
454                (_, Some(h)) => h,
455                _ => BlockHeader::default(),
456            };
457
458            warn!(
459                extractor_id=%self.extractor_id,
460                last_message_at=?self.modify_ts,
461                "SynchronizerStream transition to stale due to timeout."
462            );
463            self.state = SynchronizerState::Stale(header_to_use);
464            self.modify_ts = now;
465            Ok(true)
466        } else {
467            Ok(false)
468        }
469    }
470
471    /// Logic to transition a state synchronizer based on newly received block
472    ///
473    /// Updates the synchronizer's state according to the position of the received block:
474    /// - Next expected block -> Ready state
475    /// - Latest/Delayed block -> Either Delayed or Stale (if >60s since last update)
476    /// - Advanced block -> Advanced state (block ahead of expected position)
477    fn transition(
478        &mut self,
479        latest_retrieved: BlockHeader,
480        block_history: &BlockHistory,
481        stale_threshold: std::time::Duration,
482    ) -> Result<(), BlockSynchronizerError> {
483        let extractor_id = self.extractor_id.clone();
484        let last_message_at = self.modify_ts;
485        let block = &latest_retrieved;
486
487        match block_history.determine_block_position(&latest_retrieved)? {
488            BlockPosition::NextExpected | BlockPosition::NextPartial => {
489                self.state = SynchronizerState::Ready(latest_retrieved.clone());
490                trace!(
491                    next = %latest_retrieved,
492                    extractor = %extractor_id,
493                    "SynchronizerStream transition to next expected"
494                )
495            }
496            BlockPosition::Latest | BlockPosition::Delayed => {
497                if !self.check_and_transition_to_stale_if_needed(
498                    stale_threshold,
499                    Some(latest_retrieved.clone()),
500                )? {
501                    warn!(
502                        %extractor_id,
503                        ?last_message_at,
504                        %block,
505                        "SynchronizerStream transition transition to delayed."
506                    );
507                    self.state = SynchronizerState::Delayed(latest_retrieved.clone());
508                }
509            }
510            BlockPosition::Advanced => {
511                info!(
512                    %extractor_id,
513                    ?last_message_at,
514                    latest = opt(&block_history.latest()),
515                    %block,
516                    "SynchronizerStream transition to advanced."
517                );
518                self.state = SynchronizerState::Advanced(latest_retrieved.clone());
519            }
520        }
521        self.modify_ts = Local::now().naive_utc();
522        Ok(())
523    }
524
525    /// Marks this stream as errored
526    ///
527    /// Sets an error and transitions the stream to Ended, correctly recording the
528    /// time at which this happened.
529    fn mark_errored(&mut self, error: SynchronizerError) {
530        self.state = SynchronizerState::Ended(error.to_string());
531        self.modify_ts = Local::now().naive_utc();
532        self.error = Some(error);
533    }
534
535    /// Marks a stream as closed.
536    ///
537    /// If the stream has not been ended previously, e.g. by an error it will be marked
538    /// as Ended without error. This should not happen since we should stop consuming
539    /// from the stream if an error occured.
540    fn mark_closed(&mut self) {
541        if !matches!(self.state, SynchronizerState::Ended(_)) {
542            self.state = SynchronizerState::Ended("Closed".to_string());
543            self.modify_ts = Local::now().naive_utc();
544        }
545    }
546
547    /// Marks a stream as stale.
548    fn mark_stale(&mut self, header: &BlockHeader) {
549        self.state = SynchronizerState::Stale(header.clone());
550        self.modify_ts = Local::now().naive_utc();
551    }
552
553    /// Marks this stream as ready.
554    fn mark_ready(&mut self, header: &BlockHeader) {
555        self.state = SynchronizerState::Ready(header.clone());
556        self.modify_ts = Local::now().naive_utc();
557    }
558
559    fn has_ended(&self) -> bool {
560        matches!(self.state, SynchronizerState::Ended(_))
561    }
562
563    fn is_stale(&self) -> bool {
564        matches!(self.state, SynchronizerState::Stale(_))
565    }
566
567    fn is_advanced(&self) -> bool {
568        matches!(self.state, SynchronizerState::Advanced(_))
569    }
570
571    /// Gets the streams current header from active streams.
572    ///
573    /// A stream is considered as active unless it has ended or is stale.
574    fn get_current_header(&self) -> Option<&BlockHeader> {
575        match &self.state {
576            SynchronizerState::Ready(b) |
577            SynchronizerState::Delayed(b) |
578            SynchronizerState::Advanced(b) => Some(b),
579            _ => None,
580        }
581    }
582}
583
584#[derive(Debug, PartialEq, Clone)]
585pub struct FeedMessage<H = BlockHeader>
586where
587    H: HeaderLike,
588{
589    pub state_msgs: HashMap<String, StateSyncMessage<H>>,
590    pub sync_states: HashMap<String, SynchronizerState>,
591}
592
593impl<H> FeedMessage<H>
594where
595    H: HeaderLike,
596{
597    fn new(
598        state_msgs: HashMap<String, StateSyncMessage<H>>,
599        sync_states: HashMap<String, SynchronizerState>,
600    ) -> Self {
601        Self { state_msgs, sync_states }
602    }
603}
604
605impl<S> BlockSynchronizer<S>
606where
607    S: StateSynchronizer,
608{
609    pub fn new(
610        block_time: std::time::Duration,
611        latency_buffer: std::time::Duration,
612        max_missed_blocks: u64,
613    ) -> Self {
614        Self {
615            synchronizers: None,
616            max_messages: None,
617            block_time,
618            latency_buffer,
619            startup_timeout: block_time.mul_f64(max_missed_blocks as f64),
620            max_missed_blocks,
621        }
622    }
623
624    /// Limits the stream to emit a maximum number of messages.
625    ///
626    /// After the stream emitted max messages it will end. This is only useful for
627    /// testing purposes or if you only want to process a fixed amount of messages
628    /// and then terminate cleanly.
629    pub fn max_messages(&mut self, val: usize) {
630        self.max_messages = Some(val);
631    }
632
633    /// Sets timeout for the first message of a protocol.
634    ///
635    /// Time to wait for the full first message, including snapshot retrieval.
636    pub fn startup_timeout(mut self, val: Duration) {
637        self.startup_timeout = val;
638    }
639
640    pub fn register_synchronizer(mut self, id: ExtractorIdentity, synchronizer: S) -> Self {
641        let mut registered = self.synchronizers.unwrap_or_default();
642        registered.insert(id, synchronizer);
643        self.synchronizers = Some(registered);
644        self
645    }
646
647    #[cfg(test)]
648    pub fn with_short_timeouts() -> Self {
649        Self::new(Duration::from_millis(10), Duration::from_millis(10), 3)
650    }
651
652    /// Cleanup function for shutting down remaining synchronizers when the nanny detects an error.
653    /// Sends close signals to all remaining synchronizers and waits for them to complete.
654    async fn cleanup_synchronizers(
655        mut state_sync_tasks: FuturesUnordered<JoinHandle<()>>,
656        sync_close_senders: Vec<oneshot::Sender<()>>,
657    ) {
658        // Send close signals to all remaining synchronizers
659        for close_sender in sync_close_senders {
660            let _ = close_sender.send(());
661        }
662
663        // Await remaining tasks with timeout
664        let mut completed_tasks = 0;
665        while let Ok(Some(_)) = timeout(Duration::from_secs(5), state_sync_tasks.next()).await {
666            completed_tasks += 1;
667        }
668
669        // Warn if any synchronizers timed out during cleanup
670        let remaining_tasks = state_sync_tasks.len();
671        if remaining_tasks > 0 {
672            warn!(
673                completed = completed_tasks,
674                timed_out = remaining_tasks,
675                "Some synchronizers timed out during cleanup and may not have shut down cleanly"
676            );
677        }
678    }
679
680    /// Starts the synchronization of streams.
681    ///
682    /// Will error directly if the startup fails. Once the startup is complete, it will
683    /// communicate any fatal errors through the stream before closing it.
684    pub async fn run(
685        mut self,
686    ) -> BlockSyncResult<(JoinHandle<()>, Receiver<BlockSyncResult<FeedMessage<BlockHeader>>>)>
687    {
688        trace!("Starting BlockSynchronizer...");
689        let state_sync_tasks = FuturesUnordered::new();
690        let mut synchronizers = self
691            .synchronizers
692            .take()
693            .ok_or(BlockSynchronizerError::NoSynchronizers)?;
694        // init synchronizers; unknown extractors are warned about and skipped rather than
695        // crashing the whole client, so a misconfigured protocol doesn't take down valid ones.
696        let init_results = join_all(synchronizers.iter_mut().map(|(id, s)| {
697            s.initialize()
698                .map(|res| (id.clone(), res))
699        }))
700        .await;
701        let mut to_skip = Vec::new();
702        for (extractor_id, result) in init_results {
703            match result {
704                Ok(()) => {}
705                Err(SynchronizerError::RPCError(crate::rpc::RPCError::UnknownExtractor(
706                    reason,
707                ))) => {
708                    warn!(%extractor_id, %reason, "Extractor not recognised by server, skipping");
709                    to_skip.push(extractor_id);
710                }
711                Err(e) => {
712                    return Err(BlockSynchronizerError::InitializationError {
713                        extractor: extractor_id,
714                        source: e,
715                    })
716                }
717            }
718        }
719        for id in &to_skip {
720            synchronizers.remove(id);
721        }
722        if synchronizers.is_empty() {
723            return Err(BlockSynchronizerError::NoSynchronizers);
724        }
725
726        let mut sync_streams = Vec::with_capacity(synchronizers.len());
727        let mut sync_close_senders = Vec::new();
728        for (extractor_id, synchronizer) in synchronizers.drain() {
729            let (handle, rx) = synchronizer.start().await;
730            let (join_handle, close_sender) = handle.split();
731            state_sync_tasks.push(join_handle);
732            sync_close_senders.push(close_sender);
733
734            sync_streams.push(SynchronizerStream::new(&extractor_id, rx));
735        }
736
737        // startup, schedule first set of futures and wait for them to return to initialise
738        // synchronizers.
739        debug!("Waiting for initial synchronizer messages...");
740        let mut startup_futures = Vec::new();
741        for synchronizer in sync_streams.iter_mut() {
742            let fut = async {
743                let res = timeout(self.startup_timeout, synchronizer.rx.recv()).await;
744                (synchronizer, res)
745            };
746            startup_futures.push(fut);
747        }
748
749        let mut ready_sync_msgs = HashMap::new();
750        let initial_headers = join_all(startup_futures)
751            .await
752            .into_iter()
753            .filter_map(|(synchronizer, res)| {
754                let extractor_id = synchronizer.extractor_id.clone();
755                match res {
756                    Ok(Some(Ok(msg))) => {
757                        debug!(%extractor_id, height=?&msg.header.number, "Synchronizer started successfully!");
758                        // initially default all synchronizers to Ready
759                        synchronizer.mark_ready(&msg.header);
760                        let header = msg.header.clone();
761                        ready_sync_msgs.insert(extractor_id.name.clone(), msg);
762                        Some(header)
763                    }
764                    Ok(Some(Err(e))) => {
765                        synchronizer.mark_errored(e);
766                        None
767                    }
768                    Ok(None) => {
769                        // Synchronizer closed channel. This can only happen if the run
770                        // task ended, before this, the synchronizer should have sent
771                        // an error, so this case we likely don't have to handle that
772                        // explicitly
773                        warn!(%extractor_id, "Synchronizer closed during startup");
774                        synchronizer.mark_closed();
775                        None
776                    }
777                    Err(_) => {
778                        // We got an error because the synchronizer timed out during startup
779                        warn!(%extractor_id, "Timed out waiting for first message");
780                        synchronizer.mark_stale(&BlockHeader::default());
781                        None
782                    }
783                }
784            })
785            .collect::<HashSet<_>>() // remove duplicates
786            .into_iter()
787            .collect::<Vec<_>>();
788
789        // Fail fast if no synchronizer produced a ready first message.
790        Self::require_active_stream(&sync_streams)?;
791        let mut block_history = BlockHistory::new(initial_headers, BLOCK_HISTORY_SIZE)?;
792        // Determine the starting header for synchronization.
793        // Safe: require_active_stream above guarantees at least one Ready stream,
794        // so initial_headers is non-empty and block_history.latest() is Some.
795        let start_header = block_history
796            .latest()
797            .ok_or(BlockHistoryError::EmptyHistory)?;
798        info!(
799            start_block=%start_header,
800            n_healthy=ready_sync_msgs.len(),
801            n_total=sync_streams.len(),
802            "Block synchronization started successfully!"
803        );
804
805        // Determine correct state for each remaining synchronizer, based on their header vs the
806        // latest one
807        for stream in sync_streams.iter_mut() {
808            if let SynchronizerState::Ready(header) = stream.state.clone() {
809                if header.number < start_header.number {
810                    debug!(
811                        extractor_id=%stream.extractor_id,
812                        synchronizer_block=header.number,
813                        current_block=start_header.number,
814                        "Marking synchronizer as delayed during initialization"
815                    );
816                    stream.state = SynchronizerState::Delayed(header);
817                }
818            }
819        }
820
821        let (sync_tx, sync_rx) = mpsc::channel(30);
822        let main_loop_jh = tokio::spawn(async move {
823            let mut n_iter = 1;
824            loop {
825                // Send retrieved data to receivers.
826                let msg = FeedMessage::new(
827                    std::mem::take(&mut ready_sync_msgs),
828                    sync_streams
829                        .iter()
830                        .map(|stream| (stream.extractor_id.name.to_string(), stream.state.clone()))
831                        .collect(),
832                );
833                if sync_tx.send(Ok(msg)).await.is_err() {
834                    info!("Receiver closed, block synchronizer terminating..");
835                    return;
836                };
837
838                // Check if we have reached the max messages
839                if let Some(max_messages) = self.max_messages {
840                    if n_iter >= max_messages {
841                        info!(max_messages, "StreamEnd");
842                        return;
843                    }
844                }
845                n_iter += 1;
846
847                let res = self
848                    .handle_next_message(
849                        &mut sync_streams,
850                        &mut ready_sync_msgs,
851                        &mut block_history,
852                    )
853                    .await;
854
855                if let Err(e) = res {
856                    // Communicate error to clients, then end the loop
857                    let _ = sync_tx.send(Err(e)).await;
858                    return;
859                }
860            }
861        });
862
863        // We await the main loop and log any panics (should be impossible). If the
864        // main loop exits, all synchronizers should be ended or stale. So we kill any
865        // remaining stale ones just in case. A final error is propagated through the
866        // channel to the user.
867        let nanny_jh = tokio::spawn(async move {
868            // report any panics
869            let _ = main_loop_jh.await.map_err(|e| {
870                if e.is_panic() {
871                    error!("BlockSynchornizer main loop panicked: {e}")
872                }
873            });
874            debug!("Main loop exited. Closing synchronizers");
875            Self::cleanup_synchronizers(state_sync_tasks, sync_close_senders).await;
876            debug!("Shutdown complete");
877        });
878        Ok((nanny_jh, sync_rx))
879    }
880
881    /// Retrieves next message from synchronizers
882    ///
883    /// The result is written into `ready_sync_messages`. Errors only if there is a
884    /// non-recoverable error or all synchronizers have ended.
885    async fn handle_next_message(
886        &self,
887        sync_streams: &mut [SynchronizerStream],
888        ready_sync_msgs: &mut HashMap<String, StateSyncMessage<BlockHeader>>,
889        block_history: &mut BlockHistory,
890    ) -> BlockSyncResult<()> {
891        // If any synchronizer already has a future block, Delayed/Stale streams should not
892        // wait their full catch-up timeout — a reinit is about to fire and the wait would
893        // only lock-step the consumer further behind the chain head.
894        let any_advanced = sync_streams
895            .iter()
896            .any(SynchronizerStream::is_advanced);
897        let mut recv_futures = Vec::new();
898        for stream in sync_streams.iter_mut() {
899            // If stream is in ended state, do not check for any messages (it's receiver
900            // is closed), but do check stale streams.
901            if stream.has_ended() {
902                continue;
903            }
904            // Here we simply wait block_time + max_wait. This will not work for chains with
905            // unknown block times but is simple enough for now.
906            // If we would like to support unknown block times we could: Instruct all handles to
907            // await the max block time, if a header arrives within that time transition as
908            // usual, but via a select statement get notified (using e.g. Notify) if any other
909            // handle finishes before the timeout. Then await again but this time only for
910            // max_wait and then proceed as usual. So basically each try_advance task would have
911            // a select statement that allows it to exit the first timeout preemptively if any
912            // other try_advance task finished earlier.
913            recv_futures.push(async {
914                let res = stream
915                    .try_advance(
916                        block_history,
917                        self.block_time,
918                        self.latency_buffer,
919                        self.block_time
920                            .mul_f64(self.max_missed_blocks as f64),
921                        any_advanced,
922                    )
923                    .await?;
924                Ok::<_, BlockSynchronizerError>(
925                    res.map(|msg| (stream.extractor_id.name.clone(), msg)),
926                )
927            });
928        }
929        ready_sync_msgs.extend(
930            join_all(recv_futures)
931                .await
932                .into_iter()
933                .collect::<Result<Vec<_>, _>>()?
934                .into_iter()
935                .flatten(),
936        );
937
938        // Check if we have any active synchronizers (Ready, Delayed, or Advanced)
939        // If all synchronizers have been purged (Stale/Ended), exit the main loop
940        Self::check_streams(sync_streams)?;
941
942        // if we have any advanced header, we reinit the block history,
943        // else we simply advance the existing history
944        if sync_streams
945            .iter()
946            .any(SynchronizerStream::is_advanced)
947        {
948            *block_history = Self::reinit_block_history(sync_streams, block_history)?;
949        } else if let Some(header) = sync_streams
950            .iter()
951            .filter_map(SynchronizerStream::get_current_header)
952            .max_by_key(|b| b.number)
953        {
954            block_history.push(header.clone())?;
955        }
956        // If all synchronizers are stale (e.g. WS reconnect in progress), skip block
957        // history update and continue waiting for recovery.
958        Ok(())
959    }
960
961    /// Reinitialise block history and reclassifies active synchronizers states.
962    ///
963    /// We call this if we detect a future detached block. This usually only happens if
964    /// a synchronizer has a restart.
965    ///
966    /// ## Note
967    /// This method assumes that at least one synchronizer is in Advanced, Ready or
968    /// Delayed state, it will return an error in case this is not the case.
969    fn reinit_block_history(
970        sync_streams: &mut [SynchronizerStream],
971        block_history: &mut BlockHistory,
972    ) -> Result<BlockHistory, BlockSynchronizerError> {
973        let previous = block_history
974            .latest()
975            // Old block history should not be empty, startup should have populated it at this point
976            .ok_or(BlockHistoryError::EmptyHistory)?
977            .clone();
978        // Preserve the previously retained history so a revert to a block below the advanced tip
979        // can still find its fork point. Seeding only from current stream headers roots the new
980        // history at the oldest header the streams happen to hold, dropping the ancestors a later
981        // revert needs: a revert targeting that root drains the deque looking for its parent and
982        // fails with `RevertPositionNotFound` ("History exceeded"). `BlockHistory::new` keeps the
983        // connected chain ending at the highest-numbered header, so detached older blocks (a
984        // genuine gap, e.g. after a restart) are still discarded.
985        let mut blocks: Vec<BlockHeader> = block_history
986            .blocks()
987            .cloned()
988            .collect();
989        blocks.extend(
990            sync_streams
991                .iter()
992                .filter_map(SynchronizerStream::get_current_header)
993                .cloned(),
994        );
995        let new_block_history = BlockHistory::new(blocks, BLOCK_HISTORY_SIZE)?;
996        let latest = new_block_history
997            .latest()
998            // Block history should not be empty, we just populated it.
999            .ok_or(BlockHistoryError::EmptyHistory)?;
1000        info!(
1001             %previous,
1002            %latest,
1003            "Advanced synchronizer detected. Reinitialized block history."
1004        );
1005        sync_streams
1006            .iter_mut()
1007            .for_each(|stream| {
1008                // we only get headers from advanced, ready and delayed so stale
1009                // or ended streams are not considered here
1010                if let Some(header) = stream.get_current_header() {
1011                    if header.number < latest.number {
1012                        stream.state = SynchronizerState::Delayed(header.clone());
1013                    } else if header.number == latest.number {
1014                        stream.state = SynchronizerState::Ready(header.clone());
1015                    }
1016                }
1017            });
1018        Ok(new_block_history)
1019    }
1020
1021    /// Startup check: fails if no synchronizer is active (all Stale or Ended at init time).
1022    ///
1023    /// Used once before entering the main loop, where all-Stale is a fatal configuration
1024    /// problem (nothing to sync from), not a temporary disconnect.
1025    fn require_active_stream(sync_streams: &[SynchronizerStream]) -> BlockSyncResult<()> {
1026        if sync_streams
1027            .iter()
1028            .any(|s| !s.has_ended() && !s.is_stale())
1029        {
1030            return Ok(());
1031        }
1032        let reason: Vec<String> = sync_streams
1033            .iter()
1034            .map(|s| format!("{} reported as {} at {}", s.extractor_id, s.state, s.modify_ts))
1035            .collect();
1036        Err(BlockSynchronizerError::NoReadySynchronizers(reason.join(", ")))
1037    }
1038
1039    /// Checks if the main loop should continue or exit.
1040    ///
1041    /// Returns `Ok` if:
1042    /// - At least one synchronizer is active (Ready, Delayed, or Advanced), OR
1043    /// - All synchronizers are stale but none have ended — temporary disconnect (e.g. WS reconnect)
1044    ///   where recovery is still possible.
1045    ///
1046    /// Returns `Err` if at least one synchronizer has permanently ended while all
1047    /// remaining ones are stale — no recovery path exists.
1048    fn check_streams(sync_streams: &[SynchronizerStream]) -> BlockSyncResult<()> {
1049        let mut has_any_ended = false;
1050        let mut latest_ended_stream: Option<&SynchronizerStream> = None;
1051
1052        for stream in sync_streams.iter() {
1053            // If we have at least one active stream (ready, delayed, or advanced), continue.
1054            if !stream.has_ended() && !stream.is_stale() {
1055                return Ok(());
1056            }
1057
1058            if stream.has_ended() {
1059                has_any_ended = true;
1060                if latest_ended_stream.is_none() ||
1061                    stream.modify_ts >
1062                        latest_ended_stream
1063                            .as_ref()
1064                            .unwrap()
1065                            .modify_ts
1066                {
1067                    latest_ended_stream = Some(stream);
1068                }
1069            }
1070        }
1071
1072        // All streams are stale or ended. If none have ended, all synchronizers are
1073        // temporarily disconnected — wait for recovery without exiting the main loop.
1074        if !has_any_ended {
1075            return Ok(());
1076        }
1077
1078        // At least one synchronizer has permanently ended while all others are stale.
1079        let last_error_reason = if let Some(stream) = latest_ended_stream {
1080            if let Some(err) = &stream.error {
1081                format!("Synchronizer for {} errored with: {err}", stream.extractor_id)
1082            } else {
1083                format!("Synchronizer for {} became: {}", stream.extractor_id, stream.state)
1084            }
1085        } else {
1086            return Err(BlockSynchronizerError::NoSynchronizers);
1087        };
1088
1089        let mut reason = vec![last_error_reason];
1090
1091        sync_streams.iter().for_each(|stream| {
1092            reason.push(format!(
1093                "{} reported as {} at {}",
1094                stream.extractor_id, stream.state, stream.modify_ts
1095            ))
1096        });
1097
1098        Err(BlockSynchronizerError::NoReadySynchronizers(reason.join(", ")))
1099    }
1100}
1101
1102#[cfg(test)]
1103mod tests {
1104    use std::sync::Arc;
1105
1106    use async_trait::async_trait;
1107    use test_log::test;
1108    use tokio::sync::{oneshot, Mutex};
1109    use tycho_common::models::Chain;
1110
1111    use super::*;
1112    use crate::feed::synchronizer::{SyncResult, SynchronizerTaskHandle};
1113
1114    #[derive(Clone, Debug)]
1115    enum MockBehavior {
1116        Normal,          // Exit successfully when receiving close signal
1117        IgnoreClose,     // Ignore close signals and hang (for timeout testing)
1118        ExitImmediately, // Exit immediately after first message (for quick failure testing)
1119    }
1120
1121    type HeaderReceiver = Receiver<SyncResult<StateSyncMessage<BlockHeader>>>;
1122
1123    #[derive(Clone)]
1124    struct MockStateSync {
1125        header_tx: mpsc::Sender<SyncResult<StateSyncMessage<BlockHeader>>>,
1126        header_rx: Arc<Mutex<Option<HeaderReceiver>>>,
1127        close_received: Arc<Mutex<bool>>,
1128        behavior: MockBehavior,
1129        // For testing: store the close sender so tests can trigger close signals
1130        close_tx: Arc<Mutex<Option<oneshot::Sender<()>>>>,
1131    }
1132
1133    impl MockStateSync {
1134        fn new() -> Self {
1135            Self::with_behavior(MockBehavior::Normal)
1136        }
1137
1138        fn with_behavior(behavior: MockBehavior) -> Self {
1139            let (tx, rx) = mpsc::channel(1);
1140            Self {
1141                header_tx: tx,
1142                header_rx: Arc::new(Mutex::new(Some(rx))),
1143                close_received: Arc::new(Mutex::new(false)),
1144                behavior,
1145                close_tx: Arc::new(Mutex::new(None)),
1146            }
1147        }
1148
1149        async fn was_close_received(&self) -> bool {
1150            *self.close_received.lock().await
1151        }
1152
1153        async fn send_header(&self, header: StateSyncMessage<BlockHeader>) -> Result<(), String> {
1154            self.header_tx
1155                .send(Ok(header))
1156                .await
1157                .map_err(|e| format!("sending header failed: {e}"))
1158        }
1159
1160        // For testing: trigger a close signal to make the synchronizer exit
1161        async fn trigger_close(&self) {
1162            if let Some(close_tx) = self.close_tx.lock().await.take() {
1163                let _ = close_tx.send(());
1164            }
1165        }
1166    }
1167
1168    #[async_trait]
1169    impl StateSynchronizer for MockStateSync {
1170        async fn initialize(&mut self) -> SyncResult<()> {
1171            Ok(())
1172        }
1173
1174        async fn start(
1175            mut self,
1176        ) -> (SynchronizerTaskHandle, Receiver<SyncResult<StateSyncMessage<BlockHeader>>>) {
1177            let block_rx = {
1178                let mut guard = self.header_rx.lock().await;
1179                guard
1180                    .take()
1181                    .expect("Block receiver was not set!")
1182            };
1183
1184            // Create close channel - we need to store one sender for testing and give one to the
1185            // handle
1186            let (close_tx_for_handle, close_rx) = oneshot::channel();
1187            let (close_tx_for_test, close_rx_for_test) = oneshot::channel();
1188
1189            // Store the test close sender
1190            {
1191                let mut guard = self.close_tx.lock().await;
1192                *guard = Some(close_tx_for_test);
1193            }
1194
1195            let behavior = self.behavior.clone();
1196            let close_received_clone = self.close_received.clone();
1197            let tx = self.header_tx.clone();
1198
1199            let jh = tokio::spawn(async move {
1200                match behavior {
1201                    MockBehavior::IgnoreClose => {
1202                        // Infinite loop to simulate a hung synchronizer that doesn't respond to
1203                        // close signals
1204                        loop {
1205                            tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
1206                        }
1207                    }
1208                    MockBehavior::ExitImmediately => {
1209                        // Exit immediately with error to simulate immediate task failure
1210                        tx.send(SyncResult::Err(SynchronizerError::ConnectionError(
1211                            "Simulated immediate task failure".to_string(),
1212                        )))
1213                        .await
1214                        .unwrap();
1215                    }
1216                    MockBehavior::Normal => {
1217                        // Wait for close signal from either handle or test, then respond based on
1218                        // behavior
1219                        let _ = tokio::select! {
1220                            result = close_rx => result,
1221                            result = close_rx_for_test => result,
1222                        };
1223                        let mut guard = close_received_clone.lock().await;
1224                        *guard = true;
1225                    }
1226                }
1227            });
1228
1229            let handle = SynchronizerTaskHandle::new(jh, close_tx_for_handle);
1230            (handle, block_rx)
1231        }
1232    }
1233
1234    fn header_message(block: u8) -> StateSyncMessage<BlockHeader> {
1235        StateSyncMessage {
1236            header: BlockHeader {
1237                number: block as u64,
1238                hash: Bytes::from(vec![block]),
1239                parent_hash: Bytes::from(vec![block - 1]),
1240                revert: false,
1241                timestamp: 1000,
1242                partial_block_index: None,
1243            },
1244            ..Default::default()
1245        }
1246    }
1247
1248    /// Creates a partial block header message with an ephemeral hash encoding block number and
1249    /// partial index.
1250    fn partial_header_message(block: u8, partial_idx: u32) -> StateSyncMessage<BlockHeader> {
1251        // Ephemeral hash encodes block number and partial index for uniqueness
1252        let hash_bytes =
1253            [(block as u64).to_be_bytes().as_slice(), partial_idx.to_be_bytes().as_slice()]
1254                .concat();
1255        StateSyncMessage {
1256            header: BlockHeader {
1257                number: block as u64,
1258                hash: Bytes::from(hash_bytes),
1259                parent_hash: Bytes::from(vec![block - 1]),
1260                revert: false,
1261                timestamp: 1000,
1262                partial_block_index: Some(partial_idx),
1263            },
1264            ..Default::default()
1265        }
1266    }
1267
1268    fn revert_header_message(block: u8) -> StateSyncMessage<BlockHeader> {
1269        StateSyncMessage {
1270            header: BlockHeader {
1271                number: block as u64,
1272                hash: Bytes::from(vec![block]),
1273                parent_hash: Bytes::from(vec![block - 1]),
1274                revert: true,
1275                timestamp: 1000,
1276                partial_block_index: None,
1277            },
1278            ..Default::default()
1279        }
1280    }
1281
1282    /// Builds a full (non-partial) block header. Hash and parent_hash are derived from the given
1283    /// numbers so that a chain can be built by setting `parent = number - 1`.
1284    fn full_header(number: u64, hash: u64, parent: u64) -> BlockHeader {
1285        BlockHeader {
1286            number,
1287            hash: Bytes::from(hash.to_be_bytes()),
1288            parent_hash: Bytes::from(parent.to_be_bytes()),
1289            revert: false,
1290            timestamp: 1000,
1291            partial_block_index: None,
1292        }
1293    }
1294
1295    /// Builds a partial block header with an ephemeral hash derived from number + partial index.
1296    /// Flashblocks only carry the correct block hash on the last partial (`is_last_partial`), so
1297    /// mid-block partials have hashes that no later header can link to.
1298    fn partial_header(number: u64, partial_idx: u32, parent: u64) -> BlockHeader {
1299        BlockHeader {
1300            number,
1301            hash: Bytes::from(
1302                [number.to_be_bytes().as_slice(), partial_idx.to_be_bytes().as_slice()].concat(),
1303            ),
1304            parent_hash: Bytes::from(parent.to_be_bytes()),
1305            revert: false,
1306            timestamp: 1000,
1307            partial_block_index: Some(partial_idx),
1308        }
1309    }
1310
1311    /// Builds the last partial of a block: still a partial, but carrying the sealed block hash
1312    /// (same 8-byte scheme as `full_header`).
1313    fn sealed_partial_header(number: u64, partial_idx: u32, parent: u64) -> BlockHeader {
1314        BlockHeader {
1315            number,
1316            hash: Bytes::from(number.to_be_bytes()),
1317            parent_hash: Bytes::from(parent.to_be_bytes()),
1318            revert: false,
1319            timestamp: 1000,
1320            partial_block_index: Some(partial_idx),
1321        }
1322    }
1323
1324    /// Builds a `SynchronizerStream` pinned to a given state, for exercising state-transition
1325    /// logic without a live synchronizer. The receiver is never polled by these tests.
1326    fn stream_in_state(name: &str, state: SynchronizerState) -> SynchronizerStream {
1327        let (_tx, rx) = mpsc::channel(1);
1328        let id = ExtractorIdentity { chain: Chain::Ethereum, name: name.to_string() };
1329        let mut stream = SynchronizerStream::new(&id, rx);
1330        stream.state = state;
1331        stream
1332    }
1333
1334    async fn receive_message(rx: &mut Receiver<BlockSyncResult<FeedMessage>>) -> FeedMessage {
1335        timeout(Duration::from_millis(100), rx.recv())
1336            .await
1337            .expect("Responds in time")
1338            .expect("Should receive first message")
1339            .expect("No error")
1340    }
1341
1342    async fn setup_block_sync(
1343    ) -> (MockStateSync, MockStateSync, JoinHandle<()>, Receiver<BlockSyncResult<FeedMessage>>)
1344    {
1345        setup_block_sync_with_behaviour(MockBehavior::Normal, MockBehavior::Normal).await
1346    }
1347
1348    // Starts up a synchronizer and consumes the first message on block 1.
1349    async fn setup_block_sync_with_behaviour(
1350        v2_behavior: MockBehavior,
1351        v3_behavior: MockBehavior,
1352    ) -> (MockStateSync, MockStateSync, JoinHandle<()>, Receiver<BlockSyncResult<FeedMessage>>)
1353    {
1354        let v2_sync = MockStateSync::with_behavior(v2_behavior);
1355        let v3_sync = MockStateSync::with_behavior(v3_behavior);
1356
1357        // Use reasonable timeouts to observe proper state transitions
1358        let mut block_sync = BlockSynchronizer::new(
1359            Duration::from_millis(20), // block_time
1360            Duration::from_millis(10), // max_wait
1361            3,                         // max_missed_blocks (stale threshold = 20ms * 3 = 60ms)
1362        );
1363        block_sync.max_messages(10); // Allow enough messages to see the progression
1364
1365        let block_sync = block_sync
1366            .register_synchronizer(
1367                ExtractorIdentity { chain: Chain::Ethereum, name: "uniswap-v2".to_string() },
1368                v2_sync.clone(),
1369            )
1370            .register_synchronizer(
1371                ExtractorIdentity { chain: Chain::Ethereum, name: "uniswap-v3".to_string() },
1372                v3_sync.clone(),
1373            );
1374
1375        // Send initial messages to both synchronizers
1376        let block1_msg = header_message(1);
1377        let _ = v2_sync
1378            .send_header(block1_msg.clone())
1379            .await;
1380        let _ = v3_sync
1381            .send_header(block1_msg.clone())
1382            .await;
1383
1384        // Start the block synchronizer
1385        let (nanny_handle, mut rx) = block_sync
1386            .run()
1387            .await
1388            .expect("BlockSynchronizer failed to start");
1389
1390        let first_feed_msg = receive_message(&mut rx).await;
1391        assert_eq!(first_feed_msg.state_msgs.len(), 2);
1392        assert!(matches!(
1393            first_feed_msg
1394                .sync_states
1395                .get("uniswap-v2")
1396                .unwrap(),
1397            SynchronizerState::Ready(_)
1398        ));
1399        assert!(matches!(
1400            first_feed_msg
1401                .sync_states
1402                .get("uniswap-v3")
1403                .unwrap(),
1404            SynchronizerState::Ready(_)
1405        ));
1406
1407        (v2_sync, v3_sync, nanny_handle, rx)
1408    }
1409
1410    async fn shutdown_block_synchronizer(
1411        nanny_handle: JoinHandle<()>,
1412        rx: Receiver<BlockSyncResult<FeedMessage>>,
1413    ) {
1414        // Dropping the receiver causes the main loop's next send to fail, which exits
1415        // the loop. The nanny then calls cleanup_synchronizers, which sends close signals
1416        // to the synchronizer tasks via their handle-side channels.
1417        drop(rx);
1418        timeout(Duration::from_secs(2), nanny_handle)
1419            .await
1420            .expect("Nanny failed to exit within time")
1421            .expect("Nanny panicked");
1422    }
1423
1424    /// Send message to sync and assert it transitions to Ready at expected block/partial
1425    async fn send_and_assert_ready(
1426        sync: &MockStateSync,
1427        sync_name: &str,
1428        rx: &mut Receiver<BlockSyncResult<FeedMessage>>,
1429        msg: StateSyncMessage<BlockHeader>,
1430        expected_block: u64,
1431        expected_partial: Option<u32>,
1432    ) {
1433        sync.send_header(msg)
1434            .await
1435            .expect("send failed");
1436        let feed_msg = receive_message(rx).await;
1437        let state = feed_msg
1438            .sync_states
1439            .get(sync_name)
1440            .unwrap();
1441        match state {
1442            SynchronizerState::Ready(h) => {
1443                assert_eq!(h.number, expected_block, "wrong block number");
1444                assert_eq!(h.partial_block_index, expected_partial, "wrong partial index");
1445            }
1446            other => panic!("expected Ready, got {:?}", other),
1447        }
1448    }
1449
1450    #[test(tokio::test)]
1451    async fn test_two_ready_synchronizers() {
1452        let (v2_sync, v3_sync, nanny_handle, mut rx) = setup_block_sync().await;
1453
1454        let second_msg = header_message(2);
1455        v2_sync
1456            .send_header(second_msg.clone())
1457            .await
1458            .expect("send_header failed");
1459        v3_sync
1460            .send_header(second_msg.clone())
1461            .await
1462            .expect("send_header failed");
1463        let second_feed_msg = receive_message(&mut rx).await;
1464
1465        let exp2 = FeedMessage {
1466            state_msgs: [
1467                ("uniswap-v2".to_string(), second_msg.clone()),
1468                ("uniswap-v3".to_string(), second_msg.clone()),
1469            ]
1470            .into_iter()
1471            .collect(),
1472            sync_states: [
1473                ("uniswap-v3".to_string(), SynchronizerState::Ready(second_msg.header.clone())),
1474                ("uniswap-v2".to_string(), SynchronizerState::Ready(second_msg.header.clone())),
1475            ]
1476            .into_iter()
1477            .collect(),
1478        };
1479        assert_eq!(second_feed_msg, exp2);
1480
1481        shutdown_block_synchronizer(nanny_handle, rx).await;
1482    }
1483
1484    /// Regression test for the "Reverting block's insert position not found! History exceeded"
1485    /// crash seen on Base with flashblocks enabled.
1486    ///
1487    /// When one synchronizer races ahead (e.g. one partial block), it is classified `Advanced`
1488    /// and the block history is reinitialized. If the reinit discards the previously retained
1489    /// history, a subsequent revert to a block *below* the advanced tip can no longer find its
1490    /// fork point and the whole stream terminates. Reinit must preserve the prior history so the
1491    /// revert still resolves.
1492    #[test]
1493    fn test_reinit_preserves_history_for_revert_below_advanced_tip() {
1494        // Old history: connected chain 323 -> 324 -> 325 (hash == number).
1495        let old_blocks = vec![
1496            full_header(323, 323, 322),
1497            full_header(324, 324, 323),
1498            full_header(325, 325, 324),
1499        ];
1500        let mut old_history = BlockHistory::new(old_blocks, 15).unwrap();
1501
1502        // One stream raced ahead to 326 (connected to 325) -> Advanced, triggering reinit.
1503        // Another stream is still at 325 (Delayed).
1504        let mut streams = vec![
1505            stream_in_state("aerodrome", SynchronizerState::Advanced(full_header(326, 326, 325))),
1506            stream_in_state("uniswap_v3", SynchronizerState::Delayed(full_header(325, 325, 324))),
1507        ];
1508
1509        let mut new_history = BlockSynchronizer::<MockStateSync>::reinit_block_history(
1510            &mut streams,
1511            &mut old_history,
1512        )
1513        .expect("reinit failed");
1514
1515        // A revert to block 325 must still find its fork point (324) in the retained history.
1516        let revert = BlockHeader {
1517            number: 325,
1518            hash: Bytes::from(325u64.to_be_bytes()),
1519            parent_hash: Bytes::from(324u64.to_be_bytes()),
1520            revert: true,
1521            timestamp: 1000,
1522            partial_block_index: None,
1523        };
1524        new_history
1525            .push(revert)
1526            .expect("revert below advanced tip must not exceed history");
1527    }
1528
1529    /// Preserving history must not resurrect blocks across a real gap. When the advanced block is
1530    /// genuinely detached (e.g. a synchronizer restarted and jumped ahead), the retained blocks do
1531    /// not connect to it and must still be discarded, leaving the history rooted at the advanced
1532    /// block rather than stitching together a discontinuous chain.
1533    #[test]
1534    fn test_reinit_discards_retained_history_on_detached_advanced_block() {
1535        let old_blocks = vec![
1536            full_header(323, 323, 322),
1537            full_header(324, 324, 323),
1538            full_header(325, 325, 324),
1539        ];
1540        let mut old_history = BlockHistory::new(old_blocks, BLOCK_HISTORY_SIZE).unwrap();
1541
1542        // Advanced block is far ahead and its parent is unknown to the retained history.
1543        let mut streams = vec![stream_in_state(
1544            "aerodrome",
1545            SynchronizerState::Advanced(full_header(400, 400, 399)),
1546        )];
1547
1548        let new_history = BlockSynchronizer::<MockStateSync>::reinit_block_history(
1549            &mut streams,
1550            &mut old_history,
1551        )
1552        .expect("reinit failed");
1553
1554        let retained: Vec<u64> = new_history
1555            .blocks()
1556            .map(|b| b.number)
1557            .collect();
1558        assert_eq!(
1559            retained,
1560            vec![400],
1561            "detached advanced block must not be stitched to old history"
1562        );
1563    }
1564
1565    /// Companion regression test for the same crash with a *partial* advanced block — the
1566    /// variant actually running on Base (the only chain with `--partial-blocks`).
1567    ///
1568    /// The retained tip is a mid-block partial whose ephemeral hash differs from the sealed
1569    /// block hash (flashblocks only carry the correct hash on the last partial). The advanced
1570    /// partial of the next block links to the sealed hash, so the hash-based stitch in
1571    /// `BlockHistory::new` cannot connect it to anything retained and the rebuilt history is
1572    /// rooted at the advanced partial alone — every fork point below it is lost.
1573    #[test]
1574    fn test_reinit_preserves_history_for_partial_advanced_block() {
1575        // Old history: sealed 323 -> sealed 324 -> mid-block partial of 325 (ephemeral hash).
1576        let old_blocks = vec![
1577            full_header(323, 323, 322),
1578            full_header(324, 324, 323),
1579            partial_header(325, 2, 324),
1580        ];
1581        let mut old_history = BlockHistory::new(old_blocks, BLOCK_HISTORY_SIZE).unwrap();
1582
1583        // A stream races ahead by one partial: first partial of 326, parented on the sealed
1584        // hash of 325, which the history never saw. This is what classifies it Advanced.
1585        let advanced = partial_header(326, 0, 325);
1586        assert_eq!(
1587            old_history
1588                .determine_block_position(&advanced)
1589                .unwrap(),
1590            BlockPosition::Advanced
1591        );
1592
1593        let mut streams = vec![
1594            stream_in_state("aerodrome", SynchronizerState::Advanced(advanced)),
1595            stream_in_state("uniswap_v3", SynchronizerState::Delayed(partial_header(325, 2, 324))),
1596        ];
1597
1598        let new_history = BlockSynchronizer::<MockStateSync>::reinit_block_history(
1599            &mut streams,
1600            &mut old_history,
1601        )
1602        .expect("reinit failed");
1603
1604        let retained: Vec<u64> = new_history
1605            .blocks()
1606            .map(|b| b.number)
1607            .collect();
1608        assert_eq!(retained, vec![323, 324, 325, 326]);
1609    }
1610
1611    /// Full prod sequence behind the Base outages: partial-advanced reinit, the stream proceeds
1612    /// for one more block, then a shallow (1-block) revert arrives. The revert must resolve;
1613    /// in prod it drained the whole history and killed the feed with "Reverting block's insert
1614    /// position not found! History exceeded".
1615    #[test]
1616    fn test_revert_below_tip_resolves_after_partial_advanced_reinit() {
1617        let old_blocks = vec![
1618            full_header(323, 323, 322),
1619            full_header(324, 324, 323),
1620            partial_header(325, 2, 324),
1621        ];
1622        let mut old_history = BlockHistory::new(old_blocks, BLOCK_HISTORY_SIZE).unwrap();
1623
1624        let mut streams = vec![
1625            stream_in_state("aerodrome", SynchronizerState::Advanced(partial_header(326, 0, 325))),
1626            stream_in_state("uniswap_v3", SynchronizerState::Delayed(partial_header(325, 2, 324))),
1627        ];
1628
1629        let mut new_history = BlockSynchronizer::<MockStateSync>::reinit_block_history(
1630            &mut streams,
1631            &mut old_history,
1632        )
1633        .expect("reinit failed");
1634
1635        // The stream proceeds normally: the last partial seals 326 with the correct hash, then
1636        // the first partial of 327 arrives on top of it.
1637        new_history
1638            .push(sealed_partial_header(326, 5, 325))
1639            .expect("sealed partial push failed");
1640        new_history
1641            .push(partial_header(327, 0, 326))
1642            .expect("next partial push failed");
1643
1644        // The chain reorgs one block: revert to sealed 326. Its fork point is block 325, which
1645        // the client saw (as a partial) and which the reinit must have retained.
1646        let revert = BlockHeader {
1647            number: 326,
1648            hash: Bytes::from(326u64.to_be_bytes()),
1649            parent_hash: Bytes::from(325u64.to_be_bytes()),
1650            revert: true,
1651            timestamp: 1000,
1652            partial_block_index: None,
1653        };
1654        new_history
1655            .push(revert)
1656            .expect("1-block revert after partial-advanced reinit must resolve");
1657
1658        // The drain must stop exactly at the retained partial fork point (325), not overshoot
1659        // it or stop early at some other same-height block.
1660        let retained: Vec<u64> = new_history
1661            .blocks()
1662            .map(|b| b.number)
1663            .collect();
1664        assert_eq!(retained, vec![323, 324, 325, 326]);
1665        let latest = new_history.latest().unwrap();
1666        assert_eq!(latest.number, 326);
1667        assert!(latest.revert);
1668        assert!(!latest.is_partial());
1669    }
1670
1671    #[test(tokio::test)]
1672    async fn test_delayed_synchronizer_catches_up() {
1673        let (v2_sync, v3_sync, nanny_handle, mut rx) = setup_block_sync().await;
1674
1675        // Send block 2 to v2 synchronizer only
1676        let block2_msg = header_message(2);
1677        v2_sync
1678            .send_header(block2_msg.clone())
1679            .await
1680            .expect("send_header failed");
1681
1682        // Consume second message - v3 should be delayed
1683        let second_feed_msg = receive_message(&mut rx).await;
1684        debug!("Consumed second message for v2");
1685
1686        assert!(second_feed_msg
1687            .state_msgs
1688            .contains_key("uniswap-v2"));
1689        assert!(matches!(
1690            second_feed_msg.sync_states.get("uniswap-v2").unwrap(),
1691            SynchronizerState::Ready(header) if header.number == 2
1692        ));
1693        assert!(!second_feed_msg
1694            .state_msgs
1695            .contains_key("uniswap-v3"));
1696        assert!(matches!(
1697            second_feed_msg.sync_states.get("uniswap-v3").unwrap(),
1698            SynchronizerState::Delayed(header) if header.number == 1
1699        ));
1700
1701        // Now v3 catches up to block 2
1702        v3_sync
1703            .send_header(block2_msg.clone())
1704            .await
1705            .expect("send_header failed");
1706
1707        // Both advance to block 3
1708        let block3_msg = header_message(3);
1709        v2_sync
1710            .send_header(block3_msg.clone())
1711            .await
1712            .expect("send_header failed");
1713        v3_sync
1714            .send_header(block3_msg)
1715            .await
1716            .expect("send_header failed");
1717
1718        // Consume messages until we get both synchronizers on block 3
1719        // We may get an intermediate message for v3's catch-up or a combined message
1720        let mut third_feed_msg = receive_message(&mut rx).await;
1721
1722        // If this message doesn't have both univ2, it's an intermediate message, so we get the next
1723        // one
1724        if !third_feed_msg
1725            .state_msgs
1726            .contains_key("uniswap-v2")
1727        {
1728            third_feed_msg = rx
1729                .recv()
1730                .await
1731                .expect("header channel was closed")
1732                .expect("no error");
1733        }
1734        assert!(third_feed_msg
1735            .state_msgs
1736            .contains_key("uniswap-v2"));
1737        assert!(third_feed_msg
1738            .state_msgs
1739            .contains_key("uniswap-v3"));
1740        assert!(matches!(
1741            third_feed_msg.sync_states.get("uniswap-v2").unwrap(),
1742            SynchronizerState::Ready(header) if header.number == 3
1743        ));
1744        assert!(matches!(
1745            third_feed_msg.sync_states.get("uniswap-v3").unwrap(),
1746            SynchronizerState::Ready(header) if header.number == 3
1747        ));
1748
1749        shutdown_block_synchronizer(nanny_handle, rx).await;
1750    }
1751
1752    #[test(tokio::test)]
1753    async fn test_different_start_blocks() {
1754        let v2_sync = MockStateSync::new();
1755        let v3_sync = MockStateSync::new();
1756        let block_sync = BlockSynchronizer::with_short_timeouts()
1757            .register_synchronizer(
1758                ExtractorIdentity { chain: Chain::Ethereum, name: "uniswap-v2".to_string() },
1759                v2_sync.clone(),
1760            )
1761            .register_synchronizer(
1762                ExtractorIdentity { chain: Chain::Ethereum, name: "uniswap-v3".to_string() },
1763                v3_sync.clone(),
1764            );
1765
1766        // Initial messages - synchronizers at different blocks
1767        let block1_msg = header_message(1);
1768        let block2_msg = header_message(2);
1769
1770        let _ = v2_sync
1771            .send_header(block1_msg.clone())
1772            .await;
1773        v3_sync
1774            .send_header(block2_msg.clone())
1775            .await
1776            .expect("send_header failed");
1777
1778        // Start the block synchronizer - it should use block 2 as the starting block
1779        let (jh, mut rx) = block_sync
1780            .run()
1781            .await
1782            .expect("BlockSynchronizer failed to start.");
1783
1784        // Consume first message
1785        let first_feed_msg = receive_message(&mut rx).await;
1786        assert!(matches!(
1787            first_feed_msg.sync_states.get("uniswap-v2").unwrap(),
1788            SynchronizerState::Delayed(header) if header.number == 1
1789        ));
1790        assert!(matches!(
1791            first_feed_msg.sync_states.get("uniswap-v3").unwrap(),
1792            SynchronizerState::Ready(header) if header.number == 2
1793        ));
1794
1795        // Now v2 catches up to block 2
1796        v2_sync
1797            .send_header(block2_msg.clone())
1798            .await
1799            .expect("send_header failed");
1800
1801        // Both advance to block 3
1802        let block3_msg = header_message(3);
1803        let _ = v2_sync
1804            .send_header(block3_msg.clone())
1805            .await;
1806        v3_sync
1807            .send_header(block3_msg.clone())
1808            .await
1809            .expect("send_header failed");
1810
1811        // Consume third message - both should be on block 3
1812        let second_feed_msg = receive_message(&mut rx).await;
1813        assert_eq!(second_feed_msg.state_msgs.len(), 2);
1814        assert!(matches!(
1815            second_feed_msg.sync_states.get("uniswap-v2").unwrap(),
1816            SynchronizerState::Ready(header) if header.number == 3
1817        ));
1818        assert!(matches!(
1819            second_feed_msg.sync_states.get("uniswap-v3").unwrap(),
1820            SynchronizerState::Ready(header) if header.number == 3
1821        ));
1822
1823        shutdown_block_synchronizer(jh, rx).await;
1824    }
1825
1826    #[test(tokio::test)]
1827    async fn test_synchronizer_fails_other_goes_stale() {
1828        let (_v2_sync, v3_sync, nanny_handle, mut sync_rx) =
1829            setup_block_sync_with_behaviour(MockBehavior::ExitImmediately, MockBehavior::Normal)
1830                .await;
1831
1832        let mut error_reported = false;
1833        for _ in 0..3 {
1834            if let Some(msg) = sync_rx.recv().await {
1835                match msg {
1836                    Err(_) => error_reported = true,
1837                    Ok(msg) => {
1838                        assert!(matches!(
1839                            msg.sync_states
1840                                .get("uniswap-v3")
1841                                .unwrap(),
1842                            SynchronizerState::Delayed(_)
1843                        ));
1844                        assert!(matches!(
1845                            msg.sync_states
1846                                .get("uniswap-v2")
1847                                .unwrap(),
1848                            SynchronizerState::Ended(_)
1849                        ));
1850                    }
1851                }
1852            }
1853        }
1854        assert!(error_reported, "BlockSynchronizer did not report final error");
1855
1856        // Wait for nanny to detect task failure and execute cleanup
1857        let result = timeout(Duration::from_secs(2), nanny_handle).await;
1858        assert!(result.is_ok(), "Nanny should complete when synchronizer task exits");
1859
1860        // Verify that the remaining synchronizer received close signal during cleanup
1861        assert!(
1862            v3_sync.was_close_received().await,
1863            "v3_sync should have received close signal during cleanup"
1864        );
1865    }
1866
1867    #[test(tokio::test)]
1868    async fn test_cleanup_timeout_warning() {
1869        // Verify that cleanup_synchronizers emits a warning when synchronizers timeout during
1870        // cleanup
1871        let (_v2_sync, _v3_sync, nanny_handle, _rx) = setup_block_sync_with_behaviour(
1872            MockBehavior::ExitImmediately,
1873            MockBehavior::IgnoreClose,
1874        )
1875        .await;
1876
1877        // Wait for nanny to complete - cleanup should timeout on v3_sync but still complete
1878        let result = timeout(Duration::from_secs(10), nanny_handle).await;
1879        assert!(
1880            result.is_ok(),
1881            "Nanny should complete even when some synchronizers timeout during cleanup"
1882        );
1883
1884        // Note: In a real test environment, we would capture log output to verify the warning was
1885        // emitted. Since this is a unit test without log capture setup, we just verify that
1886        // cleanup completes even when some synchronizers timeout.
1887    }
1888
1889    #[test(tokio::test)]
1890    async fn test_one_synchronizer_goes_stale_while_other_works() {
1891        // Test Case 1: One protocol goes stale and is removed while another protocol works normally
1892        let (_v2_sync, v3_sync, nanny_handle, mut rx) = setup_block_sync().await;
1893
1894        // Send block 2 only to v3, v2 will timeout and become delayed
1895        let block2_msg = header_message(2);
1896        let _ = v3_sync
1897            .send_header(block2_msg.clone())
1898            .await;
1899        // Don't send to v2_sync - it will timeout
1900
1901        // Consume second message - v2 should be delayed, v3 ready
1902        let second_feed_msg = receive_message(&mut rx).await;
1903        assert!(second_feed_msg
1904            .state_msgs
1905            .contains_key("uniswap-v3"));
1906        assert!(!second_feed_msg
1907            .state_msgs
1908            .contains_key("uniswap-v2"));
1909        assert!(matches!(
1910            second_feed_msg
1911                .sync_states
1912                .get("uniswap-v3")
1913                .unwrap(),
1914            SynchronizerState::Ready(_)
1915        ));
1916        // v2 should be delayed (if still present) - check nanny is still running
1917        if let Some(v2_state) = second_feed_msg
1918            .sync_states
1919            .get("uniswap-v2")
1920        {
1921            if matches!(v2_state, SynchronizerState::Delayed(_)) {
1922                // Verify nanny is still running when synchronizer is just delayed
1923                assert!(
1924                    !nanny_handle.is_finished(),
1925                    "Nanny should still be running when synchronizer is delayed (not stale yet)"
1926                );
1927            }
1928        }
1929
1930        // Wait a bit, then continue sending blocks to v3 but not v2
1931        tokio::time::sleep(Duration::from_millis(15)).await;
1932
1933        // Continue sending blocks only to v3 to keep it healthy while v2 goes stale
1934        let block3_msg = header_message(3);
1935        let _ = v3_sync
1936            .send_header(block3_msg.clone())
1937            .await;
1938
1939        tokio::time::sleep(Duration::from_millis(40)).await;
1940
1941        let mut stale_found = false;
1942        for _ in 0..2 {
1943            if let Some(Ok(msg)) = rx.recv().await {
1944                if let Some(SynchronizerState::Stale(_)) = msg.sync_states.get("uniswap-v2") {
1945                    stale_found = true;
1946                }
1947            }
1948        }
1949        assert!(stale_found, "v2 synchronizer should be stale");
1950
1951        shutdown_block_synchronizer(nanny_handle, rx).await;
1952    }
1953
1954    #[test(tokio::test)]
1955    async fn test_all_synchronizers_stale_loop_continues() {
1956        // When all synchronizers go stale simultaneously (simulating a WS reconnect), the
1957        // main loop must NOT exit — it should keep running and wait for recovery.
1958        let (v2_sync, v3_sync, nanny_handle, mut rx) = setup_block_sync().await;
1959
1960        // Stop sending messages — both should timeout, go Delayed, then Stale.
1961        let mut seen_delayed = false;
1962        let mut seen_stale = false;
1963        let start_time = tokio::time::Instant::now();
1964
1965        while let Ok(Some(Ok(msg))) =
1966            tokio::time::timeout(Duration::from_millis(50), rx.recv()).await
1967        {
1968            let v2_state = msg.sync_states.get("uniswap-v2");
1969            let v3_state = msg.sync_states.get("uniswap-v3");
1970
1971            if !seen_delayed &&
1972                (matches!(v2_state, Some(SynchronizerState::Delayed(_))) ||
1973                    matches!(v3_state, Some(SynchronizerState::Delayed(_))))
1974            {
1975                seen_delayed = true;
1976                assert!(
1977                    !nanny_handle.is_finished(),
1978                    "Nanny must still run when synchronizers are Delayed"
1979                );
1980            }
1981
1982            if matches!(v2_state, Some(SynchronizerState::Stale(_))) &&
1983                matches!(v3_state, Some(SynchronizerState::Stale(_)))
1984            {
1985                seen_stale = true;
1986                assert!(
1987                    !nanny_handle.is_finished(),
1988                    "Main loop must not exit when all synchronizers are Stale (awaiting recovery)"
1989                );
1990                break;
1991            }
1992
1993            if start_time.elapsed() > Duration::from_millis(500) {
1994                break;
1995            }
1996        }
1997
1998        assert!(seen_delayed, "Synchronizers should transition through Delayed first");
1999        assert!(seen_stale, "Both synchronizers should reach Stale state");
2000
2001        // Simulate the underlying synchronizer tasks permanently dying:
2002        // trigger_close makes the mock tasks exit (dropping their tx clones), and
2003        // dropping the MockStateSyncs closes the original sender side.
2004        // This causes SynchronizerStream.rx to return None → Ended → check_streams error.
2005        v2_sync.trigger_close().await;
2006        v3_sync.trigger_close().await;
2007        drop(v2_sync);
2008        drop(v3_sync);
2009
2010        // Now the main loop should detect all-ended and report an error.
2011        let mut error_reported = false;
2012        while let Some(msg) = rx.recv().await {
2013            if msg.is_err() {
2014                error_reported = true;
2015            }
2016        }
2017        assert!(error_reported, "Expected an error after all synchronizers ended");
2018
2019        let nanny_result = timeout(Duration::from_secs(2), nanny_handle).await;
2020        assert!(nanny_result.is_ok(), "Nanny should complete after all synchronizers ended");
2021    }
2022
2023    #[test(tokio::test)]
2024    async fn test_all_synchronizers_recover_after_going_stale() {
2025        // Simulates a full WS reconnect: both synchronizers go stale, then reconnect
2026        // and send an advanced block. The main loop should rebase block history and resume.
2027        let v2_sync = MockStateSync::new();
2028        let v3_sync = MockStateSync::new();
2029        let block_sync =
2030            BlockSynchronizer::new(Duration::from_millis(20), Duration::from_millis(10), 3)
2031                .register_synchronizer(
2032                    ExtractorIdentity { chain: Chain::Ethereum, name: "uniswap-v2".to_string() },
2033                    v2_sync.clone(),
2034                )
2035                .register_synchronizer(
2036                    ExtractorIdentity { chain: Chain::Ethereum, name: "uniswap-v3".to_string() },
2037                    v3_sync.clone(),
2038                );
2039
2040        v2_sync
2041            .send_header(header_message(1))
2042            .await
2043            .unwrap();
2044        v3_sync
2045            .send_header(header_message(1))
2046            .await
2047            .unwrap();
2048
2049        let (nanny_handle, mut rx) = block_sync
2050            .run()
2051            .await
2052            .expect("BlockSynchronizer start failed");
2053
2054        let first_msg = receive_message(&mut rx).await;
2055        assert!(matches!(
2056            first_msg.sync_states.get("uniswap-v2").unwrap(),
2057            SynchronizerState::Ready(h) if h.number == 1
2058        ));
2059        assert!(matches!(
2060            first_msg.sync_states.get("uniswap-v3").unwrap(),
2061            SynchronizerState::Ready(h) if h.number == 1
2062        ));
2063
2064        // Stop sending messages — both should go Stale.
2065        let mut seen_stale = false;
2066        let start_time = tokio::time::Instant::now();
2067        while let Ok(Some(Ok(msg))) =
2068            tokio::time::timeout(Duration::from_millis(50), rx.recv()).await
2069        {
2070            let v2 = msg
2071                .sync_states
2072                .get("uniswap-v2")
2073                .unwrap();
2074            let v3 = msg
2075                .sync_states
2076                .get("uniswap-v3")
2077                .unwrap();
2078            if matches!(v2, SynchronizerState::Stale(_)) &&
2079                matches!(v3, SynchronizerState::Stale(_))
2080            {
2081                seen_stale = true;
2082                assert!(
2083                    !nanny_handle.is_finished(),
2084                    "Main loop must not exit while synchronizers are Stale"
2085                );
2086                break;
2087            }
2088            if start_time.elapsed() > Duration::from_millis(500) {
2089                break;
2090            }
2091        }
2092        assert!(seen_stale, "Both synchronizers should go Stale");
2093
2094        // Simulate reconnect: send block 5 (advanced — not connected to block 1).
2095        v2_sync
2096            .send_header(header_message(5))
2097            .await
2098            .unwrap();
2099        v3_sync
2100            .send_header(header_message(5))
2101            .await
2102            .unwrap();
2103
2104        // The main loop should detect the advanced blocks, rebase block history, and
2105        // reclassify both synchronizers as Ready at block 5.
2106        let mut recovered = false;
2107        for _ in 0..20 {
2108            let msg = receive_message(&mut rx).await;
2109            let v2 = msg
2110                .sync_states
2111                .get("uniswap-v2")
2112                .unwrap();
2113            let v3 = msg
2114                .sync_states
2115                .get("uniswap-v3")
2116                .unwrap();
2117            if matches!(v2, SynchronizerState::Ready(h) if h.number == 5) &&
2118                matches!(v3, SynchronizerState::Ready(h) if h.number == 5)
2119            {
2120                recovered = true;
2121                break;
2122            }
2123        }
2124        assert!(recovered, "Both synchronizers should recover to Ready at block 5");
2125
2126        // Drop rx to signal the main loop to stop, then await nanny cleanup.
2127        drop(rx);
2128        timeout(Duration::from_secs(2), nanny_handle)
2129            .await
2130            .expect("Nanny timed out")
2131            .expect("Nanny panicked");
2132    }
2133
2134    #[test(tokio::test)]
2135    async fn test_stale_synchronizer_recovers() {
2136        // Test Case 2: All protocols go stale and main loop exits gracefully
2137        let (v2_sync, v3_sync, nanny_handle, mut rx) = setup_block_sync().await;
2138
2139        // Send second messages to v2 only, shortly before both would go stale
2140        tokio::time::sleep(Duration::from_millis(50)).await;
2141        let block2_msg = header_message(2);
2142        let _ = v2_sync
2143            .send_header(block2_msg.clone())
2144            .await;
2145
2146        // we should get two messages here
2147        for _ in 0..2 {
2148            if let Some(msg) = rx.recv().await {
2149                if let Ok(msg) = msg {
2150                    if matches!(
2151                        msg.sync_states
2152                            .get("uniswap-v2")
2153                            .unwrap(),
2154                        SynchronizerState::Ready(_)
2155                    ) {
2156                        assert!(matches!(
2157                            msg.sync_states
2158                                .get("uniswap-v3")
2159                                .unwrap(),
2160                            SynchronizerState::Delayed(_)
2161                        ));
2162                        break;
2163                    };
2164                }
2165            } else {
2166                panic!("Channel closed unexpectedly")
2167            }
2168        }
2169
2170        // Now v3 should be stale
2171        tokio::time::sleep(Duration::from_millis(15)).await;
2172        let block3_msg = header_message(3);
2173        let _ = v2_sync
2174            .send_header(block3_msg.clone())
2175            .await;
2176        let third_msg = receive_message(&mut rx).await;
2177        dbg!(&third_msg);
2178        assert!(matches!(
2179            third_msg
2180                .sync_states
2181                .get("uniswap-v2")
2182                .unwrap(),
2183            SynchronizerState::Ready(_)
2184        ));
2185        assert!(matches!(
2186            third_msg
2187                .sync_states
2188                .get("uniswap-v3")
2189                .unwrap(),
2190            SynchronizerState::Stale(_)
2191        ));
2192
2193        let block4_msg = header_message(4);
2194        let _ = v3_sync
2195            .send_header(block2_msg.clone())
2196            .await;
2197        let _ = v3_sync
2198            .send_header(block3_msg.clone())
2199            .await;
2200        let _ = v3_sync
2201            .send_header(block4_msg.clone())
2202            .await;
2203        let _ = v2_sync
2204            .send_header(block4_msg.clone())
2205            .await;
2206        let fourth_msg = receive_message(&mut rx).await;
2207        assert!(matches!(
2208            fourth_msg
2209                .sync_states
2210                .get("uniswap-v2")
2211                .unwrap(),
2212            SynchronizerState::Ready(_)
2213        ));
2214        assert!(matches!(
2215            fourth_msg
2216                .sync_states
2217                .get("uniswap-v3")
2218                .unwrap(),
2219            SynchronizerState::Ready(_)
2220        ));
2221
2222        shutdown_block_synchronizer(nanny_handle, rx).await;
2223
2224        // Verify cleanup was triggered for both synchronizers
2225        assert!(
2226            v2_sync.was_close_received().await,
2227            "v2_sync should have received close signal during cleanup"
2228        );
2229        assert!(
2230            v3_sync.was_close_received().await,
2231            "v3_sync should have received close signal during cleanup"
2232        );
2233    }
2234
2235    #[test(tokio::test)]
2236    async fn test_all_synchronizer_advanced() {
2237        // Test the case were all synchronizers successfully recover but stream
2238        // from a disconnected future block.
2239
2240        let (v2_sync, v3_sync, nanny_handle, mut rx) = setup_block_sync().await;
2241
2242        let block3 = header_message(3);
2243        v2_sync
2244            .send_header(block3.clone())
2245            .await
2246            .unwrap();
2247        v3_sync
2248            .send_header(block3)
2249            .await
2250            .unwrap();
2251
2252        let msg = receive_message(&mut rx).await;
2253        matches!(
2254            msg.sync_states
2255                .get("uniswap-v2")
2256                .unwrap(),
2257            SynchronizerState::Ready(_)
2258        );
2259        matches!(
2260            msg.sync_states
2261                .get("uniswap-v3")
2262                .unwrap(),
2263            SynchronizerState::Ready(_)
2264        );
2265
2266        shutdown_block_synchronizer(nanny_handle, rx).await;
2267    }
2268
2269    #[test(tokio::test)]
2270    async fn test_one_synchronizer_advanced() {
2271        let (v2_sync, v3_sync, nanny_handle, mut rx) = setup_block_sync().await;
2272
2273        let block2 = header_message(2);
2274        let block4 = header_message(4);
2275        v2_sync
2276            .send_header(block4.clone())
2277            .await
2278            .unwrap();
2279        v3_sync
2280            .send_header(block2.clone())
2281            .await
2282            .unwrap();
2283
2284        let msg = receive_message(&mut rx).await;
2285        matches!(
2286            msg.sync_states
2287                .get("uniswap-v2")
2288                .unwrap(),
2289            SynchronizerState::Ready(_)
2290        );
2291        matches!(
2292            msg.sync_states
2293                .get("uniswap-v3")
2294                .unwrap(),
2295            SynchronizerState::Delayed(_)
2296        );
2297
2298        shutdown_block_synchronizer(nanny_handle, rx).await;
2299    }
2300
2301    #[test(tokio::test)]
2302    async fn test_partial_blocks_normal_operation() {
2303        // Normal operation: partials with arbitrary index increments, then advance to next block
2304        // Scenario: block 1 → partials 0,3,7 for block 2 → partials 0,2 for block 3
2305        let (v2_sync, _v3_sync, nanny_handle, mut rx) = setup_block_sync().await;
2306
2307        // Partials for block 2 with arbitrary increments (only v2, v3 ignored)
2308        send_and_assert_ready(
2309            &v2_sync,
2310            "uniswap-v2",
2311            &mut rx,
2312            partial_header_message(2, 0),
2313            2,
2314            Some(0),
2315        )
2316        .await;
2317        send_and_assert_ready(
2318            &v2_sync,
2319            "uniswap-v2",
2320            &mut rx,
2321            partial_header_message(2, 3),
2322            2,
2323            Some(3),
2324        )
2325        .await;
2326        send_and_assert_ready(
2327            &v2_sync,
2328            "uniswap-v2",
2329            &mut rx,
2330            partial_header_message(2, 7),
2331            2,
2332            Some(7),
2333        )
2334        .await;
2335
2336        // Advance to block 3 with partials
2337        send_and_assert_ready(
2338            &v2_sync,
2339            "uniswap-v2",
2340            &mut rx,
2341            partial_header_message(3, 0),
2342            3,
2343            Some(0),
2344        )
2345        .await;
2346        send_and_assert_ready(
2347            &v2_sync,
2348            "uniswap-v2",
2349            &mut rx,
2350            partial_header_message(3, 2),
2351            3,
2352            Some(2),
2353        )
2354        .await;
2355
2356        shutdown_block_synchronizer(nanny_handle, rx).await;
2357    }
2358
2359    #[test(tokio::test)]
2360    async fn test_partial_blocks_handles_reverts() {
2361        // Revert resets state and allows continuation on new fork with full blocks
2362        // Scenario: block 1 → block 2 → partials for block 3 → revert to 2 → full block 3
2363        let (v2_sync, _v3_sync, nanny_handle, mut rx) = setup_block_sync().await;
2364
2365        // Advance to full block 2 (only v2, v3 ignored)
2366        send_and_assert_ready(&v2_sync, "uniswap-v2", &mut rx, header_message(2), 2, None).await;
2367
2368        // Partials for block 3
2369        send_and_assert_ready(
2370            &v2_sync,
2371            "uniswap-v2",
2372            &mut rx,
2373            partial_header_message(3, 0),
2374            3,
2375            Some(0),
2376        )
2377        .await;
2378        send_and_assert_ready(
2379            &v2_sync,
2380            "uniswap-v2",
2381            &mut rx,
2382            partial_header_message(3, 2),
2383            3,
2384            Some(2),
2385        )
2386        .await;
2387
2388        // Revert to block 2
2389        send_and_assert_ready(&v2_sync, "uniswap-v2", &mut rx, revert_header_message(2), 2, None)
2390            .await;
2391
2392        // New fork: full block 3
2393        send_and_assert_ready(&v2_sync, "uniswap-v2", &mut rx, header_message(3), 3, None).await;
2394
2395        shutdown_block_synchronizer(nanny_handle, rx).await;
2396    }
2397
2398    #[test(tokio::test)]
2399    async fn test_partial_blocks_delayed_synchronizer_catches_up() {
2400        // Delayed synchronizer catches up when receiving partial blocks
2401        // Scenario: v2 receives partials while v3 is delayed, then v3 catches up
2402        let (v2_sync, v3_sync, nanny_handle, mut rx) = setup_block_sync().await;
2403
2404        // v2 receives partial 0 for block 2; v3 times out and becomes Delayed
2405        let partial_0 = partial_header_message(2, 0);
2406        v2_sync
2407            .send_header(partial_0.clone())
2408            .await
2409            .expect("send partial 0 failed");
2410
2411        let msg = receive_message(&mut rx).await;
2412        // v2 is Ready with partial 0, v3 is Delayed
2413        assert!(msg
2414            .state_msgs
2415            .contains_key("uniswap-v2"));
2416        assert!(!msg
2417            .state_msgs
2418            .contains_key("uniswap-v3"));
2419        assert!(matches!(
2420            msg.sync_states.get("uniswap-v2").unwrap(),
2421            SynchronizerState::Ready(h) if h.partial_block_index == Some(0)
2422        ));
2423        assert!(matches!(
2424            msg.sync_states
2425                .get("uniswap-v3")
2426                .unwrap(),
2427            SynchronizerState::Delayed(_)
2428        ));
2429
2430        // v2 advances to partial 2; v3 catches up by sending partial 0 then partial 2
2431        let partial_2 = partial_header_message(2, 2);
2432        v2_sync
2433            .send_header(partial_2.clone())
2434            .await
2435            .expect("send partial 2 failed");
2436        v3_sync
2437            .send_header(partial_0.clone())
2438            .await
2439            .expect("v3 catch up partial 0 failed");
2440        v3_sync
2441            .send_header(partial_2.clone())
2442            .await
2443            .expect("v3 catch up partial 2 failed");
2444
2445        // v3 catches up within a few message cycles
2446        let mut v3_ready = false;
2447        for _ in 0..3 {
2448            let msg = receive_message(&mut rx).await;
2449            if matches!(
2450                msg.sync_states.get("uniswap-v3").unwrap(),
2451                SynchronizerState::Ready(h) if h.partial_block_index == Some(2)
2452            ) {
2453                v3_ready = true;
2454                break;
2455            }
2456        }
2457        assert!(v3_ready, "v3 caught up to partial 2");
2458
2459        shutdown_block_synchronizer(nanny_handle, rx).await;
2460    }
2461}