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