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