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