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 `SynchronizerStream` pinned to a given state, for exercising state-transition
1296    /// logic without a live synchronizer. The receiver is never polled by these tests.
1297    fn stream_in_state(name: &str, state: SynchronizerState) -> SynchronizerStream {
1298        let (_tx, rx) = mpsc::channel(1);
1299        let id = ExtractorIdentity { chain: Chain::Ethereum, name: name.to_string() };
1300        let mut stream = SynchronizerStream::new(&id, rx);
1301        stream.state = state;
1302        stream
1303    }
1304
1305    async fn receive_message(rx: &mut Receiver<BlockSyncResult<FeedMessage>>) -> FeedMessage {
1306        timeout(Duration::from_millis(100), rx.recv())
1307            .await
1308            .expect("Responds in time")
1309            .expect("Should receive first message")
1310            .expect("No error")
1311    }
1312
1313    async fn setup_block_sync(
1314    ) -> (MockStateSync, MockStateSync, JoinHandle<()>, Receiver<BlockSyncResult<FeedMessage>>)
1315    {
1316        setup_block_sync_with_behaviour(MockBehavior::Normal, MockBehavior::Normal).await
1317    }
1318
1319    // Starts up a synchronizer and consumes the first message on block 1.
1320    async fn setup_block_sync_with_behaviour(
1321        v2_behavior: MockBehavior,
1322        v3_behavior: MockBehavior,
1323    ) -> (MockStateSync, MockStateSync, JoinHandle<()>, Receiver<BlockSyncResult<FeedMessage>>)
1324    {
1325        let v2_sync = MockStateSync::with_behavior(v2_behavior);
1326        let v3_sync = MockStateSync::with_behavior(v3_behavior);
1327
1328        // Use reasonable timeouts to observe proper state transitions
1329        let mut block_sync = BlockSynchronizer::new(
1330            Duration::from_millis(20), // block_time
1331            Duration::from_millis(10), // max_wait
1332            3,                         // max_missed_blocks (stale threshold = 20ms * 3 = 60ms)
1333        );
1334        block_sync.max_messages(10); // Allow enough messages to see the progression
1335
1336        let block_sync = block_sync
1337            .register_synchronizer(
1338                ExtractorIdentity { chain: Chain::Ethereum, name: "uniswap-v2".to_string() },
1339                v2_sync.clone(),
1340            )
1341            .register_synchronizer(
1342                ExtractorIdentity { chain: Chain::Ethereum, name: "uniswap-v3".to_string() },
1343                v3_sync.clone(),
1344            );
1345
1346        // Send initial messages to both synchronizers
1347        let block1_msg = header_message(1);
1348        let _ = v2_sync
1349            .send_header(block1_msg.clone())
1350            .await;
1351        let _ = v3_sync
1352            .send_header(block1_msg.clone())
1353            .await;
1354
1355        // Start the block synchronizer
1356        let (nanny_handle, mut rx) = block_sync
1357            .run()
1358            .await
1359            .expect("BlockSynchronizer failed to start");
1360
1361        let first_feed_msg = receive_message(&mut rx).await;
1362        assert_eq!(first_feed_msg.state_msgs.len(), 2);
1363        assert!(matches!(
1364            first_feed_msg
1365                .sync_states
1366                .get("uniswap-v2")
1367                .unwrap(),
1368            SynchronizerState::Ready(_)
1369        ));
1370        assert!(matches!(
1371            first_feed_msg
1372                .sync_states
1373                .get("uniswap-v3")
1374                .unwrap(),
1375            SynchronizerState::Ready(_)
1376        ));
1377
1378        (v2_sync, v3_sync, nanny_handle, rx)
1379    }
1380
1381    async fn shutdown_block_synchronizer(
1382        nanny_handle: JoinHandle<()>,
1383        rx: Receiver<BlockSyncResult<FeedMessage>>,
1384    ) {
1385        // Dropping the receiver causes the main loop's next send to fail, which exits
1386        // the loop. The nanny then calls cleanup_synchronizers, which sends close signals
1387        // to the synchronizer tasks via their handle-side channels.
1388        drop(rx);
1389        timeout(Duration::from_secs(2), nanny_handle)
1390            .await
1391            .expect("Nanny failed to exit within time")
1392            .expect("Nanny panicked");
1393    }
1394
1395    /// Send message to sync and assert it transitions to Ready at expected block/partial
1396    async fn send_and_assert_ready(
1397        sync: &MockStateSync,
1398        sync_name: &str,
1399        rx: &mut Receiver<BlockSyncResult<FeedMessage>>,
1400        msg: StateSyncMessage<BlockHeader>,
1401        expected_block: u64,
1402        expected_partial: Option<u32>,
1403    ) {
1404        sync.send_header(msg)
1405            .await
1406            .expect("send failed");
1407        let feed_msg = receive_message(rx).await;
1408        let state = feed_msg
1409            .sync_states
1410            .get(sync_name)
1411            .unwrap();
1412        match state {
1413            SynchronizerState::Ready(h) => {
1414                assert_eq!(h.number, expected_block, "wrong block number");
1415                assert_eq!(h.partial_block_index, expected_partial, "wrong partial index");
1416            }
1417            other => panic!("expected Ready, got {:?}", other),
1418        }
1419    }
1420
1421    #[test(tokio::test)]
1422    async fn test_two_ready_synchronizers() {
1423        let (v2_sync, v3_sync, nanny_handle, mut rx) = setup_block_sync().await;
1424
1425        let second_msg = header_message(2);
1426        v2_sync
1427            .send_header(second_msg.clone())
1428            .await
1429            .expect("send_header failed");
1430        v3_sync
1431            .send_header(second_msg.clone())
1432            .await
1433            .expect("send_header failed");
1434        let second_feed_msg = receive_message(&mut rx).await;
1435
1436        let exp2 = FeedMessage {
1437            state_msgs: [
1438                ("uniswap-v2".to_string(), second_msg.clone()),
1439                ("uniswap-v3".to_string(), second_msg.clone()),
1440            ]
1441            .into_iter()
1442            .collect(),
1443            sync_states: [
1444                ("uniswap-v3".to_string(), SynchronizerState::Ready(second_msg.header.clone())),
1445                ("uniswap-v2".to_string(), SynchronizerState::Ready(second_msg.header.clone())),
1446            ]
1447            .into_iter()
1448            .collect(),
1449        };
1450        assert_eq!(second_feed_msg, exp2);
1451
1452        shutdown_block_synchronizer(nanny_handle, rx).await;
1453    }
1454
1455    /// Regression test for the "Reverting block's insert position not found! History exceeded"
1456    /// crash seen on Base with flashblocks enabled.
1457    ///
1458    /// When one synchronizer races ahead (e.g. one partial block), it is classified `Advanced`
1459    /// and the block history is reinitialized. If the reinit discards the previously retained
1460    /// history, a subsequent revert to a block *below* the advanced tip can no longer find its
1461    /// fork point and the whole stream terminates. Reinit must preserve the prior history so the
1462    /// revert still resolves.
1463    #[test]
1464    fn test_reinit_preserves_history_for_revert_below_advanced_tip() {
1465        // Old history: connected chain 323 -> 324 -> 325 (hash == number).
1466        let old_blocks = vec![
1467            full_header(323, 323, 322),
1468            full_header(324, 324, 323),
1469            full_header(325, 325, 324),
1470        ];
1471        let mut old_history = BlockHistory::new(old_blocks, 15).unwrap();
1472
1473        // One stream raced ahead to 326 (connected to 325) -> Advanced, triggering reinit.
1474        // Another stream is still at 325 (Delayed).
1475        let mut streams = vec![
1476            stream_in_state("aerodrome", SynchronizerState::Advanced(full_header(326, 326, 325))),
1477            stream_in_state("uniswap_v3", SynchronizerState::Delayed(full_header(325, 325, 324))),
1478        ];
1479
1480        let mut new_history = BlockSynchronizer::<MockStateSync>::reinit_block_history(
1481            &mut streams,
1482            &mut old_history,
1483        )
1484        .expect("reinit failed");
1485
1486        // A revert to block 325 must still find its fork point (324) in the retained history.
1487        let revert = BlockHeader {
1488            number: 325,
1489            hash: Bytes::from(325u64.to_be_bytes()),
1490            parent_hash: Bytes::from(324u64.to_be_bytes()),
1491            revert: true,
1492            timestamp: 1000,
1493            partial_block_index: None,
1494        };
1495        new_history
1496            .push(revert)
1497            .expect("revert below advanced tip must not exceed history");
1498    }
1499
1500    /// Preserving history must not resurrect blocks across a real gap. When the advanced block is
1501    /// genuinely detached (e.g. a synchronizer restarted and jumped ahead), the retained blocks do
1502    /// not connect to it and must still be discarded, leaving the history rooted at the advanced
1503    /// block rather than stitching together a discontinuous chain.
1504    #[test]
1505    fn test_reinit_discards_retained_history_on_detached_advanced_block() {
1506        let old_blocks = vec![
1507            full_header(323, 323, 322),
1508            full_header(324, 324, 323),
1509            full_header(325, 325, 324),
1510        ];
1511        let mut old_history = BlockHistory::new(old_blocks, BLOCK_HISTORY_SIZE).unwrap();
1512
1513        // Advanced block is far ahead and its parent is unknown to the retained history.
1514        let mut streams = vec![stream_in_state(
1515            "aerodrome",
1516            SynchronizerState::Advanced(full_header(400, 400, 399)),
1517        )];
1518
1519        let new_history = BlockSynchronizer::<MockStateSync>::reinit_block_history(
1520            &mut streams,
1521            &mut old_history,
1522        )
1523        .expect("reinit failed");
1524
1525        let retained: Vec<u64> = new_history
1526            .blocks()
1527            .map(|b| b.number)
1528            .collect();
1529        assert_eq!(
1530            retained,
1531            vec![400],
1532            "detached advanced block must not be stitched to old history"
1533        );
1534    }
1535
1536    #[test(tokio::test)]
1537    async fn test_delayed_synchronizer_catches_up() {
1538        let (v2_sync, v3_sync, nanny_handle, mut rx) = setup_block_sync().await;
1539
1540        // Send block 2 to v2 synchronizer only
1541        let block2_msg = header_message(2);
1542        v2_sync
1543            .send_header(block2_msg.clone())
1544            .await
1545            .expect("send_header failed");
1546
1547        // Consume second message - v3 should be delayed
1548        let second_feed_msg = receive_message(&mut rx).await;
1549        debug!("Consumed second message for v2");
1550
1551        assert!(second_feed_msg
1552            .state_msgs
1553            .contains_key("uniswap-v2"));
1554        assert!(matches!(
1555            second_feed_msg.sync_states.get("uniswap-v2").unwrap(),
1556            SynchronizerState::Ready(header) if header.number == 2
1557        ));
1558        assert!(!second_feed_msg
1559            .state_msgs
1560            .contains_key("uniswap-v3"));
1561        assert!(matches!(
1562            second_feed_msg.sync_states.get("uniswap-v3").unwrap(),
1563            SynchronizerState::Delayed(header) if header.number == 1
1564        ));
1565
1566        // Now v3 catches up to block 2
1567        v3_sync
1568            .send_header(block2_msg.clone())
1569            .await
1570            .expect("send_header failed");
1571
1572        // Both advance to block 3
1573        let block3_msg = header_message(3);
1574        v2_sync
1575            .send_header(block3_msg.clone())
1576            .await
1577            .expect("send_header failed");
1578        v3_sync
1579            .send_header(block3_msg)
1580            .await
1581            .expect("send_header failed");
1582
1583        // Consume messages until we get both synchronizers on block 3
1584        // We may get an intermediate message for v3's catch-up or a combined message
1585        let mut third_feed_msg = receive_message(&mut rx).await;
1586
1587        // If this message doesn't have both univ2, it's an intermediate message, so we get the next
1588        // one
1589        if !third_feed_msg
1590            .state_msgs
1591            .contains_key("uniswap-v2")
1592        {
1593            third_feed_msg = rx
1594                .recv()
1595                .await
1596                .expect("header channel was closed")
1597                .expect("no error");
1598        }
1599        assert!(third_feed_msg
1600            .state_msgs
1601            .contains_key("uniswap-v2"));
1602        assert!(third_feed_msg
1603            .state_msgs
1604            .contains_key("uniswap-v3"));
1605        assert!(matches!(
1606            third_feed_msg.sync_states.get("uniswap-v2").unwrap(),
1607            SynchronizerState::Ready(header) if header.number == 3
1608        ));
1609        assert!(matches!(
1610            third_feed_msg.sync_states.get("uniswap-v3").unwrap(),
1611            SynchronizerState::Ready(header) if header.number == 3
1612        ));
1613
1614        shutdown_block_synchronizer(nanny_handle, rx).await;
1615    }
1616
1617    #[test(tokio::test)]
1618    async fn test_different_start_blocks() {
1619        let v2_sync = MockStateSync::new();
1620        let v3_sync = MockStateSync::new();
1621        let block_sync = BlockSynchronizer::with_short_timeouts()
1622            .register_synchronizer(
1623                ExtractorIdentity { chain: Chain::Ethereum, name: "uniswap-v2".to_string() },
1624                v2_sync.clone(),
1625            )
1626            .register_synchronizer(
1627                ExtractorIdentity { chain: Chain::Ethereum, name: "uniswap-v3".to_string() },
1628                v3_sync.clone(),
1629            );
1630
1631        // Initial messages - synchronizers at different blocks
1632        let block1_msg = header_message(1);
1633        let block2_msg = header_message(2);
1634
1635        let _ = v2_sync
1636            .send_header(block1_msg.clone())
1637            .await;
1638        v3_sync
1639            .send_header(block2_msg.clone())
1640            .await
1641            .expect("send_header failed");
1642
1643        // Start the block synchronizer - it should use block 2 as the starting block
1644        let (jh, mut rx) = block_sync
1645            .run()
1646            .await
1647            .expect("BlockSynchronizer failed to start.");
1648
1649        // Consume first message
1650        let first_feed_msg = receive_message(&mut rx).await;
1651        assert!(matches!(
1652            first_feed_msg.sync_states.get("uniswap-v2").unwrap(),
1653            SynchronizerState::Delayed(header) if header.number == 1
1654        ));
1655        assert!(matches!(
1656            first_feed_msg.sync_states.get("uniswap-v3").unwrap(),
1657            SynchronizerState::Ready(header) if header.number == 2
1658        ));
1659
1660        // Now v2 catches up to block 2
1661        v2_sync
1662            .send_header(block2_msg.clone())
1663            .await
1664            .expect("send_header failed");
1665
1666        // Both advance to block 3
1667        let block3_msg = header_message(3);
1668        let _ = v2_sync
1669            .send_header(block3_msg.clone())
1670            .await;
1671        v3_sync
1672            .send_header(block3_msg.clone())
1673            .await
1674            .expect("send_header failed");
1675
1676        // Consume third message - both should be on block 3
1677        let second_feed_msg = receive_message(&mut rx).await;
1678        assert_eq!(second_feed_msg.state_msgs.len(), 2);
1679        assert!(matches!(
1680            second_feed_msg.sync_states.get("uniswap-v2").unwrap(),
1681            SynchronizerState::Ready(header) if header.number == 3
1682        ));
1683        assert!(matches!(
1684            second_feed_msg.sync_states.get("uniswap-v3").unwrap(),
1685            SynchronizerState::Ready(header) if header.number == 3
1686        ));
1687
1688        shutdown_block_synchronizer(jh, rx).await;
1689    }
1690
1691    #[test(tokio::test)]
1692    async fn test_synchronizer_fails_other_goes_stale() {
1693        let (_v2_sync, v3_sync, nanny_handle, mut sync_rx) =
1694            setup_block_sync_with_behaviour(MockBehavior::ExitImmediately, MockBehavior::Normal)
1695                .await;
1696
1697        let mut error_reported = false;
1698        for _ in 0..3 {
1699            if let Some(msg) = sync_rx.recv().await {
1700                match msg {
1701                    Err(_) => error_reported = true,
1702                    Ok(msg) => {
1703                        assert!(matches!(
1704                            msg.sync_states
1705                                .get("uniswap-v3")
1706                                .unwrap(),
1707                            SynchronizerState::Delayed(_)
1708                        ));
1709                        assert!(matches!(
1710                            msg.sync_states
1711                                .get("uniswap-v2")
1712                                .unwrap(),
1713                            SynchronizerState::Ended(_)
1714                        ));
1715                    }
1716                }
1717            }
1718        }
1719        assert!(error_reported, "BlockSynchronizer did not report final error");
1720
1721        // Wait for nanny to detect task failure and execute cleanup
1722        let result = timeout(Duration::from_secs(2), nanny_handle).await;
1723        assert!(result.is_ok(), "Nanny should complete when synchronizer task exits");
1724
1725        // Verify that the remaining synchronizer received close signal during cleanup
1726        assert!(
1727            v3_sync.was_close_received().await,
1728            "v3_sync should have received close signal during cleanup"
1729        );
1730    }
1731
1732    #[test(tokio::test)]
1733    async fn test_cleanup_timeout_warning() {
1734        // Verify that cleanup_synchronizers emits a warning when synchronizers timeout during
1735        // cleanup
1736        let (_v2_sync, _v3_sync, nanny_handle, _rx) = setup_block_sync_with_behaviour(
1737            MockBehavior::ExitImmediately,
1738            MockBehavior::IgnoreClose,
1739        )
1740        .await;
1741
1742        // Wait for nanny to complete - cleanup should timeout on v3_sync but still complete
1743        let result = timeout(Duration::from_secs(10), nanny_handle).await;
1744        assert!(
1745            result.is_ok(),
1746            "Nanny should complete even when some synchronizers timeout during cleanup"
1747        );
1748
1749        // Note: In a real test environment, we would capture log output to verify the warning was
1750        // emitted. Since this is a unit test without log capture setup, we just verify that
1751        // cleanup completes even when some synchronizers timeout.
1752    }
1753
1754    #[test(tokio::test)]
1755    async fn test_one_synchronizer_goes_stale_while_other_works() {
1756        // Test Case 1: One protocol goes stale and is removed while another protocol works normally
1757        let (_v2_sync, v3_sync, nanny_handle, mut rx) = setup_block_sync().await;
1758
1759        // Send block 2 only to v3, v2 will timeout and become delayed
1760        let block2_msg = header_message(2);
1761        let _ = v3_sync
1762            .send_header(block2_msg.clone())
1763            .await;
1764        // Don't send to v2_sync - it will timeout
1765
1766        // Consume second message - v2 should be delayed, v3 ready
1767        let second_feed_msg = receive_message(&mut rx).await;
1768        assert!(second_feed_msg
1769            .state_msgs
1770            .contains_key("uniswap-v3"));
1771        assert!(!second_feed_msg
1772            .state_msgs
1773            .contains_key("uniswap-v2"));
1774        assert!(matches!(
1775            second_feed_msg
1776                .sync_states
1777                .get("uniswap-v3")
1778                .unwrap(),
1779            SynchronizerState::Ready(_)
1780        ));
1781        // v2 should be delayed (if still present) - check nanny is still running
1782        if let Some(v2_state) = second_feed_msg
1783            .sync_states
1784            .get("uniswap-v2")
1785        {
1786            if matches!(v2_state, SynchronizerState::Delayed(_)) {
1787                // Verify nanny is still running when synchronizer is just delayed
1788                assert!(
1789                    !nanny_handle.is_finished(),
1790                    "Nanny should still be running when synchronizer is delayed (not stale yet)"
1791                );
1792            }
1793        }
1794
1795        // Wait a bit, then continue sending blocks to v3 but not v2
1796        tokio::time::sleep(Duration::from_millis(15)).await;
1797
1798        // Continue sending blocks only to v3 to keep it healthy while v2 goes stale
1799        let block3_msg = header_message(3);
1800        let _ = v3_sync
1801            .send_header(block3_msg.clone())
1802            .await;
1803
1804        tokio::time::sleep(Duration::from_millis(40)).await;
1805
1806        let mut stale_found = false;
1807        for _ in 0..2 {
1808            if let Some(Ok(msg)) = rx.recv().await {
1809                if let Some(SynchronizerState::Stale(_)) = msg.sync_states.get("uniswap-v2") {
1810                    stale_found = true;
1811                }
1812            }
1813        }
1814        assert!(stale_found, "v2 synchronizer should be stale");
1815
1816        shutdown_block_synchronizer(nanny_handle, rx).await;
1817    }
1818
1819    #[test(tokio::test)]
1820    async fn test_all_synchronizers_stale_loop_continues() {
1821        // When all synchronizers go stale simultaneously (simulating a WS reconnect), the
1822        // main loop must NOT exit — it should keep running and wait for recovery.
1823        let (v2_sync, v3_sync, nanny_handle, mut rx) = setup_block_sync().await;
1824
1825        // Stop sending messages — both should timeout, go Delayed, then Stale.
1826        let mut seen_delayed = false;
1827        let mut seen_stale = false;
1828        let start_time = tokio::time::Instant::now();
1829
1830        while let Ok(Some(Ok(msg))) =
1831            tokio::time::timeout(Duration::from_millis(50), rx.recv()).await
1832        {
1833            let v2_state = msg.sync_states.get("uniswap-v2");
1834            let v3_state = msg.sync_states.get("uniswap-v3");
1835
1836            if !seen_delayed &&
1837                (matches!(v2_state, Some(SynchronizerState::Delayed(_))) ||
1838                    matches!(v3_state, Some(SynchronizerState::Delayed(_))))
1839            {
1840                seen_delayed = true;
1841                assert!(
1842                    !nanny_handle.is_finished(),
1843                    "Nanny must still run when synchronizers are Delayed"
1844                );
1845            }
1846
1847            if matches!(v2_state, Some(SynchronizerState::Stale(_))) &&
1848                matches!(v3_state, Some(SynchronizerState::Stale(_)))
1849            {
1850                seen_stale = true;
1851                assert!(
1852                    !nanny_handle.is_finished(),
1853                    "Main loop must not exit when all synchronizers are Stale (awaiting recovery)"
1854                );
1855                break;
1856            }
1857
1858            if start_time.elapsed() > Duration::from_millis(500) {
1859                break;
1860            }
1861        }
1862
1863        assert!(seen_delayed, "Synchronizers should transition through Delayed first");
1864        assert!(seen_stale, "Both synchronizers should reach Stale state");
1865
1866        // Simulate the underlying synchronizer tasks permanently dying:
1867        // trigger_close makes the mock tasks exit (dropping their tx clones), and
1868        // dropping the MockStateSyncs closes the original sender side.
1869        // This causes SynchronizerStream.rx to return None → Ended → check_streams error.
1870        v2_sync.trigger_close().await;
1871        v3_sync.trigger_close().await;
1872        drop(v2_sync);
1873        drop(v3_sync);
1874
1875        // Now the main loop should detect all-ended and report an error.
1876        let mut error_reported = false;
1877        while let Some(msg) = rx.recv().await {
1878            if msg.is_err() {
1879                error_reported = true;
1880            }
1881        }
1882        assert!(error_reported, "Expected an error after all synchronizers ended");
1883
1884        let nanny_result = timeout(Duration::from_secs(2), nanny_handle).await;
1885        assert!(nanny_result.is_ok(), "Nanny should complete after all synchronizers ended");
1886    }
1887
1888    #[test(tokio::test)]
1889    async fn test_all_synchronizers_recover_after_going_stale() {
1890        // Simulates a full WS reconnect: both synchronizers go stale, then reconnect
1891        // and send an advanced block. The main loop should rebase block history and resume.
1892        let v2_sync = MockStateSync::new();
1893        let v3_sync = MockStateSync::new();
1894        let block_sync =
1895            BlockSynchronizer::new(Duration::from_millis(20), Duration::from_millis(10), 3)
1896                .register_synchronizer(
1897                    ExtractorIdentity { chain: Chain::Ethereum, name: "uniswap-v2".to_string() },
1898                    v2_sync.clone(),
1899                )
1900                .register_synchronizer(
1901                    ExtractorIdentity { chain: Chain::Ethereum, name: "uniswap-v3".to_string() },
1902                    v3_sync.clone(),
1903                );
1904
1905        v2_sync
1906            .send_header(header_message(1))
1907            .await
1908            .unwrap();
1909        v3_sync
1910            .send_header(header_message(1))
1911            .await
1912            .unwrap();
1913
1914        let (nanny_handle, mut rx) = block_sync
1915            .run()
1916            .await
1917            .expect("BlockSynchronizer start failed");
1918
1919        let first_msg = receive_message(&mut rx).await;
1920        assert!(matches!(
1921            first_msg.sync_states.get("uniswap-v2").unwrap(),
1922            SynchronizerState::Ready(h) if h.number == 1
1923        ));
1924        assert!(matches!(
1925            first_msg.sync_states.get("uniswap-v3").unwrap(),
1926            SynchronizerState::Ready(h) if h.number == 1
1927        ));
1928
1929        // Stop sending messages — both should go Stale.
1930        let mut seen_stale = false;
1931        let start_time = tokio::time::Instant::now();
1932        while let Ok(Some(Ok(msg))) =
1933            tokio::time::timeout(Duration::from_millis(50), rx.recv()).await
1934        {
1935            let v2 = msg
1936                .sync_states
1937                .get("uniswap-v2")
1938                .unwrap();
1939            let v3 = msg
1940                .sync_states
1941                .get("uniswap-v3")
1942                .unwrap();
1943            if matches!(v2, SynchronizerState::Stale(_)) &&
1944                matches!(v3, SynchronizerState::Stale(_))
1945            {
1946                seen_stale = true;
1947                assert!(
1948                    !nanny_handle.is_finished(),
1949                    "Main loop must not exit while synchronizers are Stale"
1950                );
1951                break;
1952            }
1953            if start_time.elapsed() > Duration::from_millis(500) {
1954                break;
1955            }
1956        }
1957        assert!(seen_stale, "Both synchronizers should go Stale");
1958
1959        // Simulate reconnect: send block 5 (advanced — not connected to block 1).
1960        v2_sync
1961            .send_header(header_message(5))
1962            .await
1963            .unwrap();
1964        v3_sync
1965            .send_header(header_message(5))
1966            .await
1967            .unwrap();
1968
1969        // The main loop should detect the advanced blocks, rebase block history, and
1970        // reclassify both synchronizers as Ready at block 5.
1971        let mut recovered = false;
1972        for _ in 0..20 {
1973            let msg = receive_message(&mut rx).await;
1974            let v2 = msg
1975                .sync_states
1976                .get("uniswap-v2")
1977                .unwrap();
1978            let v3 = msg
1979                .sync_states
1980                .get("uniswap-v3")
1981                .unwrap();
1982            if matches!(v2, SynchronizerState::Ready(h) if h.number == 5) &&
1983                matches!(v3, SynchronizerState::Ready(h) if h.number == 5)
1984            {
1985                recovered = true;
1986                break;
1987            }
1988        }
1989        assert!(recovered, "Both synchronizers should recover to Ready at block 5");
1990
1991        // Drop rx to signal the main loop to stop, then await nanny cleanup.
1992        drop(rx);
1993        timeout(Duration::from_secs(2), nanny_handle)
1994            .await
1995            .expect("Nanny timed out")
1996            .expect("Nanny panicked");
1997    }
1998
1999    #[test(tokio::test)]
2000    async fn test_stale_synchronizer_recovers() {
2001        // Test Case 2: All protocols go stale and main loop exits gracefully
2002        let (v2_sync, v3_sync, nanny_handle, mut rx) = setup_block_sync().await;
2003
2004        // Send second messages to v2 only, shortly before both would go stale
2005        tokio::time::sleep(Duration::from_millis(50)).await;
2006        let block2_msg = header_message(2);
2007        let _ = v2_sync
2008            .send_header(block2_msg.clone())
2009            .await;
2010
2011        // we should get two messages here
2012        for _ in 0..2 {
2013            if let Some(msg) = rx.recv().await {
2014                if let Ok(msg) = msg {
2015                    if matches!(
2016                        msg.sync_states
2017                            .get("uniswap-v2")
2018                            .unwrap(),
2019                        SynchronizerState::Ready(_)
2020                    ) {
2021                        assert!(matches!(
2022                            msg.sync_states
2023                                .get("uniswap-v3")
2024                                .unwrap(),
2025                            SynchronizerState::Delayed(_)
2026                        ));
2027                        break;
2028                    };
2029                }
2030            } else {
2031                panic!("Channel closed unexpectedly")
2032            }
2033        }
2034
2035        // Now v3 should be stale
2036        tokio::time::sleep(Duration::from_millis(15)).await;
2037        let block3_msg = header_message(3);
2038        let _ = v2_sync
2039            .send_header(block3_msg.clone())
2040            .await;
2041        let third_msg = receive_message(&mut rx).await;
2042        dbg!(&third_msg);
2043        assert!(matches!(
2044            third_msg
2045                .sync_states
2046                .get("uniswap-v2")
2047                .unwrap(),
2048            SynchronizerState::Ready(_)
2049        ));
2050        assert!(matches!(
2051            third_msg
2052                .sync_states
2053                .get("uniswap-v3")
2054                .unwrap(),
2055            SynchronizerState::Stale(_)
2056        ));
2057
2058        let block4_msg = header_message(4);
2059        let _ = v3_sync
2060            .send_header(block2_msg.clone())
2061            .await;
2062        let _ = v3_sync
2063            .send_header(block3_msg.clone())
2064            .await;
2065        let _ = v3_sync
2066            .send_header(block4_msg.clone())
2067            .await;
2068        let _ = v2_sync
2069            .send_header(block4_msg.clone())
2070            .await;
2071        let fourth_msg = receive_message(&mut rx).await;
2072        assert!(matches!(
2073            fourth_msg
2074                .sync_states
2075                .get("uniswap-v2")
2076                .unwrap(),
2077            SynchronizerState::Ready(_)
2078        ));
2079        assert!(matches!(
2080            fourth_msg
2081                .sync_states
2082                .get("uniswap-v3")
2083                .unwrap(),
2084            SynchronizerState::Ready(_)
2085        ));
2086
2087        shutdown_block_synchronizer(nanny_handle, rx).await;
2088
2089        // Verify cleanup was triggered for both synchronizers
2090        assert!(
2091            v2_sync.was_close_received().await,
2092            "v2_sync should have received close signal during cleanup"
2093        );
2094        assert!(
2095            v3_sync.was_close_received().await,
2096            "v3_sync should have received close signal during cleanup"
2097        );
2098    }
2099
2100    #[test(tokio::test)]
2101    async fn test_all_synchronizer_advanced() {
2102        // Test the case were all synchronizers successfully recover but stream
2103        // from a disconnected future block.
2104
2105        let (v2_sync, v3_sync, nanny_handle, mut rx) = setup_block_sync().await;
2106
2107        let block3 = header_message(3);
2108        v2_sync
2109            .send_header(block3.clone())
2110            .await
2111            .unwrap();
2112        v3_sync
2113            .send_header(block3)
2114            .await
2115            .unwrap();
2116
2117        let msg = receive_message(&mut rx).await;
2118        matches!(
2119            msg.sync_states
2120                .get("uniswap-v2")
2121                .unwrap(),
2122            SynchronizerState::Ready(_)
2123        );
2124        matches!(
2125            msg.sync_states
2126                .get("uniswap-v3")
2127                .unwrap(),
2128            SynchronizerState::Ready(_)
2129        );
2130
2131        shutdown_block_synchronizer(nanny_handle, rx).await;
2132    }
2133
2134    #[test(tokio::test)]
2135    async fn test_one_synchronizer_advanced() {
2136        let (v2_sync, v3_sync, nanny_handle, mut rx) = setup_block_sync().await;
2137
2138        let block2 = header_message(2);
2139        let block4 = header_message(4);
2140        v2_sync
2141            .send_header(block4.clone())
2142            .await
2143            .unwrap();
2144        v3_sync
2145            .send_header(block2.clone())
2146            .await
2147            .unwrap();
2148
2149        let msg = receive_message(&mut rx).await;
2150        matches!(
2151            msg.sync_states
2152                .get("uniswap-v2")
2153                .unwrap(),
2154            SynchronizerState::Ready(_)
2155        );
2156        matches!(
2157            msg.sync_states
2158                .get("uniswap-v3")
2159                .unwrap(),
2160            SynchronizerState::Delayed(_)
2161        );
2162
2163        shutdown_block_synchronizer(nanny_handle, rx).await;
2164    }
2165
2166    #[test(tokio::test)]
2167    async fn test_partial_blocks_normal_operation() {
2168        // Normal operation: partials with arbitrary index increments, then advance to next block
2169        // Scenario: block 1 → partials 0,3,7 for block 2 → partials 0,2 for block 3
2170        let (v2_sync, _v3_sync, nanny_handle, mut rx) = setup_block_sync().await;
2171
2172        // Partials for block 2 with arbitrary increments (only v2, v3 ignored)
2173        send_and_assert_ready(
2174            &v2_sync,
2175            "uniswap-v2",
2176            &mut rx,
2177            partial_header_message(2, 0),
2178            2,
2179            Some(0),
2180        )
2181        .await;
2182        send_and_assert_ready(
2183            &v2_sync,
2184            "uniswap-v2",
2185            &mut rx,
2186            partial_header_message(2, 3),
2187            2,
2188            Some(3),
2189        )
2190        .await;
2191        send_and_assert_ready(
2192            &v2_sync,
2193            "uniswap-v2",
2194            &mut rx,
2195            partial_header_message(2, 7),
2196            2,
2197            Some(7),
2198        )
2199        .await;
2200
2201        // Advance to block 3 with partials
2202        send_and_assert_ready(
2203            &v2_sync,
2204            "uniswap-v2",
2205            &mut rx,
2206            partial_header_message(3, 0),
2207            3,
2208            Some(0),
2209        )
2210        .await;
2211        send_and_assert_ready(
2212            &v2_sync,
2213            "uniswap-v2",
2214            &mut rx,
2215            partial_header_message(3, 2),
2216            3,
2217            Some(2),
2218        )
2219        .await;
2220
2221        shutdown_block_synchronizer(nanny_handle, rx).await;
2222    }
2223
2224    #[test(tokio::test)]
2225    async fn test_partial_blocks_handles_reverts() {
2226        // Revert resets state and allows continuation on new fork with full blocks
2227        // Scenario: block 1 → block 2 → partials for block 3 → revert to 2 → full block 3
2228        let (v2_sync, _v3_sync, nanny_handle, mut rx) = setup_block_sync().await;
2229
2230        // Advance to full block 2 (only v2, v3 ignored)
2231        send_and_assert_ready(&v2_sync, "uniswap-v2", &mut rx, header_message(2), 2, None).await;
2232
2233        // Partials for block 3
2234        send_and_assert_ready(
2235            &v2_sync,
2236            "uniswap-v2",
2237            &mut rx,
2238            partial_header_message(3, 0),
2239            3,
2240            Some(0),
2241        )
2242        .await;
2243        send_and_assert_ready(
2244            &v2_sync,
2245            "uniswap-v2",
2246            &mut rx,
2247            partial_header_message(3, 2),
2248            3,
2249            Some(2),
2250        )
2251        .await;
2252
2253        // Revert to block 2
2254        send_and_assert_ready(&v2_sync, "uniswap-v2", &mut rx, revert_header_message(2), 2, None)
2255            .await;
2256
2257        // New fork: full block 3
2258        send_and_assert_ready(&v2_sync, "uniswap-v2", &mut rx, header_message(3), 3, None).await;
2259
2260        shutdown_block_synchronizer(nanny_handle, rx).await;
2261    }
2262
2263    #[test(tokio::test)]
2264    async fn test_partial_blocks_delayed_synchronizer_catches_up() {
2265        // Delayed synchronizer catches up when receiving partial blocks
2266        // Scenario: v2 receives partials while v3 is delayed, then v3 catches up
2267        let (v2_sync, v3_sync, nanny_handle, mut rx) = setup_block_sync().await;
2268
2269        // v2 receives partial 0 for block 2; v3 times out and becomes Delayed
2270        let partial_0 = partial_header_message(2, 0);
2271        v2_sync
2272            .send_header(partial_0.clone())
2273            .await
2274            .expect("send partial 0 failed");
2275
2276        let msg = receive_message(&mut rx).await;
2277        // v2 is Ready with partial 0, v3 is Delayed
2278        assert!(msg
2279            .state_msgs
2280            .contains_key("uniswap-v2"));
2281        assert!(!msg
2282            .state_msgs
2283            .contains_key("uniswap-v3"));
2284        assert!(matches!(
2285            msg.sync_states.get("uniswap-v2").unwrap(),
2286            SynchronizerState::Ready(h) if h.partial_block_index == Some(0)
2287        ));
2288        assert!(matches!(
2289            msg.sync_states
2290                .get("uniswap-v3")
2291                .unwrap(),
2292            SynchronizerState::Delayed(_)
2293        ));
2294
2295        // v2 advances to partial 2; v3 catches up by sending partial 0 then partial 2
2296        let partial_2 = partial_header_message(2, 2);
2297        v2_sync
2298            .send_header(partial_2.clone())
2299            .await
2300            .expect("send partial 2 failed");
2301        v3_sync
2302            .send_header(partial_0.clone())
2303            .await
2304            .expect("v3 catch up partial 0 failed");
2305        v3_sync
2306            .send_header(partial_2.clone())
2307            .await
2308            .expect("v3 catch up partial 2 failed");
2309
2310        // v3 catches up within a few message cycles
2311        let mut v3_ready = false;
2312        for _ in 0..3 {
2313            let msg = receive_message(&mut rx).await;
2314            if matches!(
2315                msg.sync_states.get("uniswap-v3").unwrap(),
2316                SynchronizerState::Ready(h) if h.partial_block_index == Some(2)
2317            ) {
2318                v3_ready = true;
2319                break;
2320            }
2321        }
2322        assert!(v3_ready, "v3 caught up to partial 2");
2323
2324        shutdown_block_synchronizer(nanny_handle, rx).await;
2325    }
2326}