Skip to main content

alloy_provider/provider/
watch_canonical_blocks_from.rs

1use crate::{transport::TransportErrorKind, WatchBlocksFrom, WatchBlocksFromStream};
2use alloy_consensus::BlockHeader;
3use alloy_eips::BlockNumberOrTag;
4use alloy_network::{BlockResponse as _, Network};
5use alloy_network_primitives::HeaderResponse;
6use alloy_transport::{TransportError, TransportResult};
7use futures::{stream::Buffered, Stream, StreamExt as _};
8use pin_project::pin_project;
9use std::{
10    collections::VecDeque,
11    future::Future,
12    pin::Pin,
13    task::{Context, Poll},
14    time::Duration,
15};
16
17const RPC_CONCURRENCY_DEFAULT: usize = 4;
18const MAX_REORG_DEPTH_DEFAULT: usize = 64;
19
20/// A builder for streaming canonical block events from a historical block.
21///
22/// This wraps [`WatchBlocksFrom`] and performs reorg detection: when the chain tip changes
23/// incompatibly, the stream yields [`CanonicalEvent::Removed`] for rolled-back blocks
24/// followed by [`CanonicalEvent::Added`] for the new canonical chain segment.
25#[derive(Debug)]
26#[must_use = "this builder does nothing unless you call `.into_stream`"]
27pub struct WatchCanonicalBlocksFrom<N: Network> {
28    watch_blocks_from: WatchBlocksFrom<N>,
29    rpc_concurrency: usize,
30    max_reorg_depth: usize,
31}
32
33/// An item emitted by the canonical block stream.
34#[derive(Debug, Clone)]
35pub enum CanonicalEvent<T> {
36    /// A new canonical block to add.
37    Added(T),
38    /// A canonical block to remove due to a reorg.
39    Removed(T),
40}
41
42impl<N: Network> WatchCanonicalBlocksFrom<N> {
43    pub(crate) const fn new(watch_blocks_from: WatchBlocksFrom<N>) -> Self {
44        Self {
45            watch_blocks_from,
46            rpc_concurrency: RPC_CONCURRENCY_DEFAULT,
47            max_reorg_depth: MAX_REORG_DEPTH_DEFAULT,
48        }
49    }
50
51    /// Streams canonical blocks with full transaction bodies.
52    pub fn full(mut self) -> Self {
53        self.watch_blocks_from = self.watch_blocks_from.full();
54        self
55    }
56
57    /// Streams canonical blocks with transaction hashes only.
58    pub fn hashes(mut self) -> Self {
59        self.watch_blocks_from = self.watch_blocks_from.hashes();
60        self
61    }
62
63    /// Sets the poll interval used when the stream is caught up.
64    pub fn poll_interval(mut self, poll_interval: Duration) -> Self {
65        self.watch_blocks_from = self.watch_blocks_from.poll_interval(poll_interval);
66        self
67    }
68
69    /// Sets the head block tag used to determine stream progress.
70    pub fn block_tag(mut self, block_tag: BlockNumberOrTag) -> Self {
71        self.watch_blocks_from = self.watch_blocks_from.block_tag(block_tag);
72        self
73    }
74
75    /// Sets the number of in-flight `eth_getBlockByNumber` requests.
76    pub const fn rpc_concurrency(mut self, rpc_concurrency: usize) -> Self {
77        self.rpc_concurrency = if rpc_concurrency == 0 { 1 } else { rpc_concurrency };
78        self
79    }
80
81    /// Sets the maximum number of canonical blocks retained for reorg detection.
82    pub const fn max_reorg_depth(mut self, max_reorg_depth: usize) -> Self {
83        self.max_reorg_depth = if max_reorg_depth == 0 { 1 } else { max_reorg_depth };
84        self
85    }
86
87    /// Converts the builder into a stream of canonical block events.
88    pub fn into_stream(self) -> WatchCanonicalBlocksFromStream<N> {
89        let Self { watch_blocks_from, rpc_concurrency, max_reorg_depth } = self;
90        let stream = watch_blocks_from.clone().into_stream().buffered(rpc_concurrency.max(1));
91
92        WatchCanonicalBlocksFromStream {
93            watch_blocks_from,
94            stream,
95            buffer: FixedBuf::new(max_reorg_depth),
96            state: WatchCanonicalBlocksFromState::PollNext,
97        }
98    }
99}
100
101#[derive(Debug)]
102enum WatchCanonicalBlocksFromState<N: Network> {
103    /// Polling the next block from `watch_blocks_from(...).buffered(...)`.
104    PollNext,
105    /// Reconciling `next` with the canonical buffer by walking parents.
106    Reconcile { next: N::BlockResponse, pending: VecDeque<N::BlockResponse> },
107    /// Polling an in-flight parent fetch.
108    FetchingParent {
109        next: N::BlockResponse,
110        pending: VecDeque<N::BlockResponse>,
111        fut: super::BlockFut<N::BlockResponse>,
112    },
113    /// Emitting `Added` events for `pending`, then `next`.
114    EmitPending { pending: VecDeque<N::BlockResponse>, next: Option<N::BlockResponse> },
115    /// Yield one terminal error item and then end the stream.
116    EmitError { err: TransportError },
117    /// Stream terminated.
118    Done,
119}
120
121/// A stream of canonical block events produced by [`WatchCanonicalBlocksFrom`].
122#[derive(Debug)]
123#[pin_project]
124pub struct WatchCanonicalBlocksFromStream<N: Network> {
125    watch_blocks_from: WatchBlocksFrom<N>,
126    #[pin]
127    stream: Buffered<WatchBlocksFromStream<N>>,
128    buffer: FixedBuf<N::BlockResponse>,
129    state: WatchCanonicalBlocksFromState<N>,
130}
131
132impl<N: Network> Stream for WatchCanonicalBlocksFromStream<N> {
133    type Item = TransportResult<CanonicalEvent<N::BlockResponse>>;
134
135    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
136        let mut this = self.project();
137
138        loop {
139            let state = std::mem::replace(this.state, WatchCanonicalBlocksFromState::Done);
140            match state {
141                WatchCanonicalBlocksFromState::PollNext => match this.stream.as_mut().poll_next(cx)
142                {
143                    Poll::Pending => {
144                        *this.state = WatchCanonicalBlocksFromState::PollNext;
145                        return Poll::Pending;
146                    }
147                    Poll::Ready(None) => {
148                        *this.state = WatchCanonicalBlocksFromState::Done;
149                    }
150                    Poll::Ready(Some(Ok(next))) => {
151                        *this.state = WatchCanonicalBlocksFromState::Reconcile {
152                            next,
153                            pending: VecDeque::new(),
154                        };
155                    }
156                    Poll::Ready(Some(Err(err))) => {
157                        *this.state = WatchCanonicalBlocksFromState::EmitError { err };
158                    }
159                },
160                WatchCanonicalBlocksFromState::Reconcile { next, pending } => {
161                    let front = pending.front().unwrap_or(&next);
162                    let Some(canonical_tip) = this.buffer.last() else {
163                        *this.state = WatchCanonicalBlocksFromState::EmitPending {
164                            pending,
165                            next: Some(next),
166                        };
167                        continue;
168                    };
169
170                    let parent_hash = front.header().parent_hash();
171                    if parent_hash == canonical_tip.header().hash() {
172                        *this.state = WatchCanonicalBlocksFromState::EmitPending {
173                            pending,
174                            next: Some(next),
175                        };
176                        continue;
177                    }
178
179                    // Reorg detected: `front` does not build on canonical tip.
180                    // Because WatchBlocksFrom emits strictly sequential heights, we can
181                    // remove the tip when heights are adjacent.
182                    let height = front.header().number();
183                    let canonical_height = canonical_tip.header().number();
184                    if canonical_height + 1 == height {
185                        let removed = this
186                            .buffer
187                            .pop()
188                            .expect("position is always < canonical buffer length");
189                        if this.buffer.len() == 0 {
190                            *this.state = WatchCanonicalBlocksFromState::EmitError {
191                                err: TransportErrorKind::custom_str(
192                                    "Deep reorg detected; no canonical history retained.",
193                                ),
194                            };
195                        } else {
196                            *this.state =
197                                WatchCanonicalBlocksFromState::Reconcile { next, pending };
198                        }
199                        return Poll::Ready(Some(Ok(CanonicalEvent::Removed(removed))));
200                    }
201
202                    let Some(parent_height) = height.checked_sub(1) else {
203                        *this.state = WatchCanonicalBlocksFromState::EmitError {
204                            err: TransportErrorKind::custom_str(
205                                "Cannot backfill parent for genesis block during canonical reconciliation.",
206                            ),
207                        };
208                        continue;
209                    };
210
211                    let watch_blocks_from = this.watch_blocks_from.clone();
212                    let fut = watch_blocks_from.get_block(parent_height);
213                    *this.state =
214                        WatchCanonicalBlocksFromState::FetchingParent { next, pending, fut };
215                }
216                WatchCanonicalBlocksFromState::FetchingParent { next, mut pending, mut fut } => {
217                    match Pin::new(&mut fut).poll(cx) {
218                        Poll::Pending => {
219                            *this.state = WatchCanonicalBlocksFromState::FetchingParent {
220                                next,
221                                pending,
222                                fut,
223                            };
224                            return Poll::Pending;
225                        }
226                        Poll::Ready(Err(err)) => {
227                            *this.state = WatchCanonicalBlocksFromState::EmitError { err };
228                        }
229                        Poll::Ready(Ok(parent)) => {
230                            let front = pending.front().unwrap_or(&next);
231                            if parent.header().hash() != front.header().parent_hash() {
232                                // Parent no longer matches: a second reorg happened while
233                                // reconciling. Abandon this item and continue with next blocks.
234                                *this.state = WatchCanonicalBlocksFromState::PollNext;
235                                continue;
236                            }
237
238                            pending.push_front(parent);
239                            *this.state =
240                                WatchCanonicalBlocksFromState::Reconcile { next, pending };
241                        }
242                    }
243                }
244                WatchCanonicalBlocksFromState::EmitPending { mut pending, mut next } => {
245                    if let Some(block) = pending.pop_front() {
246                        this.buffer.push(block.clone());
247                        *this.state = WatchCanonicalBlocksFromState::EmitPending { pending, next };
248                        return Poll::Ready(Some(Ok(CanonicalEvent::Added(block))));
249                    }
250
251                    if let Some(next) = next.take() {
252                        this.buffer.push(next.clone());
253                        *this.state = WatchCanonicalBlocksFromState::PollNext;
254                        return Poll::Ready(Some(Ok(CanonicalEvent::Added(next))));
255                    }
256
257                    *this.state = WatchCanonicalBlocksFromState::PollNext;
258                }
259                WatchCanonicalBlocksFromState::EmitError { err } => {
260                    *this.state = WatchCanonicalBlocksFromState::Done;
261                    return Poll::Ready(Some(Err(err)));
262                }
263                WatchCanonicalBlocksFromState::Done => {
264                    *this.state = WatchCanonicalBlocksFromState::Done;
265                    return Poll::Ready(None);
266                }
267            }
268        }
269    }
270}
271
272#[derive(Debug)]
273pub(super) struct FixedBuf<T> {
274    buf: VecDeque<T>,
275}
276
277impl<T> FixedBuf<T> {
278    pub(super) fn new(capacity: usize) -> Self {
279        Self { buf: VecDeque::with_capacity(capacity.max(1)) }
280    }
281
282    /// Pushes `item` and discards the oldest item if the buffer is full.
283    pub(super) fn push(&mut self, item: T) {
284        if self.buf.len() == self.buf.capacity() {
285            self.buf.pop_front();
286        }
287        self.buf.push_back(item);
288    }
289
290    /// Returns the most recent item, if any.
291    pub(super) fn pop(&mut self) -> Option<T> {
292        self.buf.pop_back()
293    }
294
295    pub(super) fn last(&self) -> Option<&T> {
296        self.buf.back()
297    }
298
299    pub(super) fn len(&self) -> usize {
300        self.buf.len()
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307    use crate::{Provider, ProviderBuilder};
308    use alloy_eips::BlockNumberOrTag;
309    use alloy_primitives::{B256, U64};
310    use alloy_rpc_client::RpcClient;
311    use alloy_rpc_types_eth::Block;
312    use alloy_transport::{TransportError, TransportFut};
313    use futures::StreamExt;
314    use std::{
315        collections::HashMap,
316        sync::{Arc, RwLock},
317        task::Poll,
318        time::Duration,
319    };
320    use tokio::time::timeout;
321
322    struct ChainState {
323        blocks: HashMap<u64, Block>,
324        head: u64,
325    }
326
327    #[derive(Clone)]
328    struct MockChain {
329        state: Arc<RwLock<ChainState>>,
330    }
331
332    impl MockChain {
333        fn new() -> Self {
334            Self { state: Arc::new(RwLock::new(ChainState { blocks: HashMap::new(), head: 0 })) }
335        }
336
337        /// Insert blocks and set head to the highest block number.
338        fn extend(&self, blocks: &[Block]) {
339            let mut state = self.state.write().unwrap();
340            for b in blocks {
341                let number = b.header.inner.number;
342                state.blocks.insert(number, b.clone());
343                if number > state.head {
344                    state.head = number;
345                }
346            }
347        }
348
349        /// Simulate a reorg: remove all blocks at height >= the first block's
350        /// height, insert the new blocks, and set head to the highest.
351        fn reorg(&self, blocks: &[Block]) {
352            let mut state = self.state.write().unwrap();
353            let min_height =
354                blocks.iter().map(|b| b.header.inner.number).min().expect("reorg needs blocks");
355            state.blocks.retain(|&h, _| h < min_height);
356            let mut max = state.head;
357            for b in blocks {
358                let number = b.header.inner.number;
359                state.blocks.insert(number, b.clone());
360                if number > max {
361                    max = number;
362                }
363            }
364            state.head = max;
365        }
366
367        fn provider(&self) -> impl Provider {
368            let transport = MockChainTransport { chain: self.clone() };
369            ProviderBuilder::new().connect_client(RpcClient::new(transport, true))
370        }
371
372        fn handle_request(
373            &self,
374            req: &alloy_json_rpc::SerializedRequest,
375        ) -> alloy_json_rpc::Response {
376            let state = self.state.read().unwrap();
377            let payload = match req.method() {
378                "eth_blockNumber" => {
379                    let raw = serde_json::to_string(&U64::from(state.head)).unwrap();
380                    alloy_json_rpc::ResponsePayload::Success(
381                        serde_json::value::RawValue::from_string(raw).unwrap(),
382                    )
383                }
384                "eth_getBlockByNumber" => {
385                    let params = req.params().expect("eth_getBlockByNumber requires params");
386                    let (tag, _full): (BlockNumberOrTag, bool) =
387                        serde_json::from_str(params.get()).unwrap();
388                    let number = match tag {
389                        BlockNumberOrTag::Number(n) => n,
390                        BlockNumberOrTag::Latest => state.head,
391                        _ => unimplemented!("unsupported block tag in MockChain: {tag:?}"),
392                    };
393                    let block = state.blocks.get(&number).cloned();
394                    let raw = serde_json::to_string(&block).unwrap();
395                    alloy_json_rpc::ResponsePayload::Success(
396                        serde_json::value::RawValue::from_string(raw).unwrap(),
397                    )
398                }
399                other => panic!("MockChain: unexpected RPC method `{other}`"),
400            };
401            alloy_json_rpc::Response { id: req.id().clone(), payload }
402        }
403    }
404
405    #[derive(Clone)]
406    struct MockChainTransport {
407        chain: MockChain,
408    }
409
410    impl tower::Service<alloy_json_rpc::RequestPacket> for MockChainTransport {
411        type Response = alloy_json_rpc::ResponsePacket;
412        type Error = TransportError;
413        type Future = TransportFut<'static>;
414
415        fn poll_ready(
416            &mut self,
417            _cx: &mut std::task::Context<'_>,
418        ) -> Poll<Result<(), Self::Error>> {
419            Poll::Ready(Ok(()))
420        }
421
422        fn call(&mut self, req: alloy_json_rpc::RequestPacket) -> Self::Future {
423            let chain = self.chain.clone();
424            Box::pin(async move {
425                Ok(match req {
426                    alloy_json_rpc::RequestPacket::Single(req) => {
427                        alloy_json_rpc::ResponsePacket::Single(chain.handle_request(&req))
428                    }
429                    alloy_json_rpc::RequestPacket::Batch(reqs) => {
430                        alloy_json_rpc::ResponsePacket::Batch(
431                            reqs.iter().map(|r| chain.handle_request(r)).collect(),
432                        )
433                    }
434                })
435            })
436        }
437    }
438
439    fn block(number: u64, hash_last_byte: u8, parent_hash_last_byte: u8) -> Block {
440        let mut block: Block = Block::default();
441        block.header.inner.number = number;
442        block.header.hash = B256::with_last_byte(hash_last_byte);
443        block.header.inner.parent_hash = B256::with_last_byte(parent_hash_last_byte);
444        block
445    }
446
447    #[tokio::test]
448    async fn emits_removed_then_added_on_reorg_within_buffer() {
449        let chain = MockChain::new();
450        // Initial chain: 1 -> 2 -> 3.
451        chain.extend(&[block(1, 1, 0), block(2, 2, 1), block(3, 3, 2)]);
452
453        let provider = chain.provider();
454        let mut stream = provider
455            .watch_blocks_from(1)
456            .block_tag(BlockNumberOrTag::Latest)
457            .poll_interval(Duration::from_millis(1))
458            .canonical()
459            .rpc_concurrency(1)
460            .max_reorg_depth(16)
461            .into_stream();
462
463        // Added 1, 2, 3.
464        for expected in [1_u64, 2, 3] {
465            let item =
466                timeout(Duration::from_secs(1), stream.next()).await.unwrap().unwrap().unwrap();
467            match item {
468                CanonicalEvent::Added(block) => assert_eq!(block.header.number, expected),
469                other => panic!("expected Added({expected}), got {other:?}"),
470            }
471        }
472
473        // Reorg: replace block 3, add block 4.
474        chain.reorg(&[block(3, 33, 2), block(4, 44, 33)]);
475
476        // Removed 3, Added 3', Added 4.
477        let removed_3 =
478            timeout(Duration::from_secs(1), stream.next()).await.unwrap().unwrap().unwrap();
479        let added_3_prime =
480            timeout(Duration::from_secs(1), stream.next()).await.unwrap().unwrap().unwrap();
481        let added_4 =
482            timeout(Duration::from_secs(1), stream.next()).await.unwrap().unwrap().unwrap();
483
484        match removed_3 {
485            CanonicalEvent::Removed(block) => {
486                assert_eq!(block.header.number, 3);
487                assert_eq!(block.header.hash, B256::with_last_byte(3));
488            }
489            other => panic!("expected Removed(3), got {other:?}"),
490        }
491        match added_3_prime {
492            CanonicalEvent::Added(block) => {
493                assert_eq!(block.header.number, 3);
494                assert_eq!(block.header.hash, B256::with_last_byte(33));
495            }
496            other => panic!("expected Added(3'), got {other:?}"),
497        }
498        match added_4 {
499            CanonicalEvent::Added(block) => {
500                assert_eq!(block.header.number, 4);
501                assert_eq!(block.header.hash, B256::with_last_byte(44));
502            }
503            other => panic!("expected Added(4), got {other:?}"),
504        }
505    }
506
507    #[tokio::test]
508    async fn emits_error_when_reorg_exceeds_retained_history() {
509        let chain = MockChain::new();
510        // Initial chain: 1 -> 2 -> 3.
511        chain.extend(&[block(1, 1, 0), block(2, 2, 1), block(3, 3, 2)]);
512
513        let provider = chain.provider();
514        let mut stream = provider
515            .watch_blocks_from(1)
516            .block_tag(BlockNumberOrTag::Latest)
517            .poll_interval(Duration::from_millis(1))
518            .canonical()
519            .rpc_concurrency(1)
520            .max_reorg_depth(2)
521            .into_stream();
522
523        // Added 1, 2, 3.
524        for expected in [1_u64, 2, 3] {
525            let item =
526                timeout(Duration::from_secs(1), stream.next()).await.unwrap().unwrap().unwrap();
527            match item {
528                CanonicalEvent::Added(block) => assert_eq!(block.header.number, expected),
529                other => panic!("expected Added({expected}), got {other:?}"),
530            }
531        }
532
533        // Deep reorg: entirely new chain from height 2 onward.
534        chain.reorg(&[block(2, 22, 11), block(3, 33, 22), block(4, 44, 33)]);
535
536        // Removed 3, Removed 2.
537        let removed_3 =
538            timeout(Duration::from_secs(1), stream.next()).await.unwrap().unwrap().unwrap();
539        let removed_2 =
540            timeout(Duration::from_secs(1), stream.next()).await.unwrap().unwrap().unwrap();
541        match removed_3 {
542            CanonicalEvent::Removed(block) => assert_eq!(block.header.number, 3),
543            other => panic!("expected Removed(3), got {other:?}"),
544        }
545        match removed_2 {
546            CanonicalEvent::Removed(block) => assert_eq!(block.header.number, 2),
547            other => panic!("expected Removed(2), got {other:?}"),
548        }
549
550        let err =
551            timeout(Duration::from_secs(1), stream.next()).await.unwrap().unwrap().unwrap_err();
552        assert!(format!("{err}").contains("Deep reorg detected"));
553
554        // Stream ends after the first error.
555        let next = timeout(Duration::from_secs(1), stream.next()).await.unwrap();
556        assert!(next.is_none());
557    }
558
559    #[tokio::test]
560    async fn backfills_parent_chain_when_reorg_ancestor_is_retained() {
561        let chain = MockChain::new();
562        // Initial chain: 1 -> 2 -> 3 -> 4.
563        chain.extend(&[block(1, 1, 0), block(2, 2, 1), block(3, 3, 2), block(4, 4, 3)]);
564
565        let provider = chain.provider();
566        let mut stream = provider
567            .watch_blocks_from(1)
568            .block_tag(BlockNumberOrTag::Latest)
569            .poll_interval(Duration::from_millis(1))
570            .canonical()
571            .rpc_concurrency(1)
572            .max_reorg_depth(8)
573            .into_stream();
574
575        // Added 1, 2, 3, 4.
576        for expected in [1_u64, 2, 3, 4] {
577            let item =
578                timeout(Duration::from_secs(1), stream.next()).await.unwrap().unwrap().unwrap();
579            match item {
580                CanonicalEvent::Added(block) => assert_eq!(block.header.number, expected),
581                other => panic!("expected Added({expected}), got {other:?}"),
582            }
583        }
584
585        // Reorg: new chain from height 3 onward, adding block 5.
586        chain.reorg(&[block(3, 33, 2), block(4, 44, 33), block(5, 5, 44)]);
587
588        // Removed 4, Removed 3, Added 3', Added 4', Added 5.
589        let removed_4 =
590            timeout(Duration::from_secs(1), stream.next()).await.unwrap().unwrap().unwrap();
591        let removed_3 =
592            timeout(Duration::from_secs(1), stream.next()).await.unwrap().unwrap().unwrap();
593        let added_3_prime =
594            timeout(Duration::from_secs(1), stream.next()).await.unwrap().unwrap().unwrap();
595        let added_4_prime =
596            timeout(Duration::from_secs(1), stream.next()).await.unwrap().unwrap().unwrap();
597        let added_5 =
598            timeout(Duration::from_secs(1), stream.next()).await.unwrap().unwrap().unwrap();
599
600        match removed_4 {
601            CanonicalEvent::Removed(block) => {
602                assert_eq!(block.header.number, 4);
603                assert_eq!(block.header.hash, B256::with_last_byte(4));
604            }
605            other => panic!("expected Removed(4), got {other:?}"),
606        }
607        match removed_3 {
608            CanonicalEvent::Removed(block) => {
609                assert_eq!(block.header.number, 3);
610                assert_eq!(block.header.hash, B256::with_last_byte(3));
611            }
612            other => panic!("expected Removed(3), got {other:?}"),
613        }
614        match added_3_prime {
615            CanonicalEvent::Added(block) => {
616                assert_eq!(block.header.number, 3);
617                assert_eq!(block.header.hash, B256::with_last_byte(33));
618            }
619            other => panic!("expected Added(3'), got {other:?}"),
620        }
621        match added_4_prime {
622            CanonicalEvent::Added(block) => {
623                assert_eq!(block.header.number, 4);
624                assert_eq!(block.header.hash, B256::with_last_byte(44));
625            }
626            other => panic!("expected Added(4'), got {other:?}"),
627        }
628        match added_5 {
629            CanonicalEvent::Added(block) => {
630                assert_eq!(block.header.number, 5);
631                assert_eq!(block.header.hash, B256::with_last_byte(5));
632            }
633            other => panic!("expected Added(5), got {other:?}"),
634        }
635    }
636
637    #[tokio::test]
638    async fn recovers_when_chain_changes_during_backfill() {
639        let chain = MockChain::new();
640        // Initial chain: 1 -> 2 -> 3.
641        chain.extend(&[block(1, 1, 0), block(2, 2, 1), block(3, 3, 2)]);
642
643        let provider = chain.provider();
644        let mut stream = provider
645            .watch_blocks_from(1)
646            .block_tag(BlockNumberOrTag::Latest)
647            .poll_interval(Duration::from_millis(1))
648            .canonical()
649            .rpc_concurrency(1)
650            .max_reorg_depth(8)
651            .into_stream();
652
653        // Added 1, 2, 3.
654        for expected in [1_u64, 2, 3] {
655            let item =
656                timeout(Duration::from_secs(1), stream.next()).await.unwrap().unwrap().unwrap();
657            match item {
658                CanonicalEvent::Added(block) => assert_eq!(block.header.number, expected),
659                other => panic!("expected Added({expected}), got {other:?}"),
660            }
661        }
662
663        // First reorg: block 4 expects parent hash 33, but block 3 has hash 34.
664        // The stream will detect the mismatch during backfill and abandon reconciliation
665        // via `continue 'stream`, then poll for new blocks.
666        chain.reorg(&[block(3, 34, 2), block(4, 4, 33)]);
667
668        // Removed(3) is yielded before the mismatch is discovered.
669        let removed_3 =
670            timeout(Duration::from_secs(1), stream.next()).await.unwrap().unwrap().unwrap();
671        match removed_3 {
672            CanonicalEvent::Removed(block) => {
673                assert_eq!(block.header.number, 3);
674                assert_eq!(block.header.hash, B256::with_last_byte(3));
675            }
676            other => panic!("expected Removed(3), got {other:?}"),
677        }
678
679        // Schedule the second reorg to happen while the stream is polling for new blocks.
680        // The generator has already resumed and hit `continue 'stream` (because it saw
681        // hash 34 instead of the expected 33). It's now waiting for the head to advance.
682        let chain_clone = chain.clone();
683        tokio::spawn(async move {
684            tokio::time::sleep(Duration::from_millis(10)).await;
685            chain_clone.reorg(&[block(3, 33, 2), block(4, 44, 33), block(5, 5, 44)]);
686        });
687
688        // Recovery: Added 3', Added 4', Added 5.
689        let added_3_prime =
690            timeout(Duration::from_secs(1), stream.next()).await.unwrap().unwrap().unwrap();
691        let added_4_prime =
692            timeout(Duration::from_secs(1), stream.next()).await.unwrap().unwrap().unwrap();
693        let added_5 =
694            timeout(Duration::from_secs(1), stream.next()).await.unwrap().unwrap().unwrap();
695
696        match added_3_prime {
697            CanonicalEvent::Added(block) => {
698                assert_eq!(block.header.number, 3);
699                assert_eq!(block.header.hash, B256::with_last_byte(33));
700            }
701            other => panic!("expected Added(3'), got {other:?}"),
702        }
703        match added_4_prime {
704            CanonicalEvent::Added(block) => {
705                assert_eq!(block.header.number, 4);
706                assert_eq!(block.header.hash, B256::with_last_byte(44));
707            }
708            other => panic!("expected Added(4'), got {other:?}"),
709        }
710        match added_5 {
711            CanonicalEvent::Added(block) => {
712                assert_eq!(block.header.number, 5);
713                assert_eq!(block.header.hash, B256::with_last_byte(5));
714            }
715            other => panic!("expected Added(5), got {other:?}"),
716        }
717    }
718
719    #[tokio::test]
720    async fn clamps_zero_values_for_rpc_concurrency_and_reorg_depth() {
721        let chain = MockChain::new();
722        chain.extend(&[block(1, 1, 0)]);
723
724        let provider = chain.provider();
725        let mut stream = provider
726            .watch_blocks_from(1)
727            .block_tag(BlockNumberOrTag::Latest)
728            .poll_interval(Duration::from_millis(1))
729            .canonical()
730            .rpc_concurrency(0)
731            .max_reorg_depth(0)
732            .into_stream();
733
734        let first = timeout(Duration::from_secs(1), stream.next()).await.unwrap().unwrap().unwrap();
735        match first {
736            CanonicalEvent::Added(block) => assert_eq!(block.header.number, 1),
737            other => panic!("expected Added(1), got {other:?}"),
738        }
739    }
740
741    #[tokio::test]
742    async fn canonical_builder_exposes_watch_blocks_from_methods() {
743        let chain = MockChain::new();
744        chain.extend(&[block(1, 1, 0)]);
745
746        let provider = chain.provider();
747        let mut stream = provider
748            .watch_canonical_blocks_from(1)
749            .block_tag(BlockNumberOrTag::Latest)
750            .poll_interval(Duration::from_millis(1))
751            .hashes()
752            .rpc_concurrency(1)
753            .max_reorg_depth(8)
754            .into_stream();
755
756        let first = timeout(Duration::from_secs(1), stream.next()).await.unwrap().unwrap().unwrap();
757        match first {
758            CanonicalEvent::Added(block) => assert_eq!(block.header.number, 1),
759            other => panic!("expected Added(1), got {other:?}"),
760        }
761    }
762
763    #[tokio::test]
764    async fn stream_ends_when_provider_is_dropped() {
765        let chain = MockChain::new();
766        let provider = chain.provider();
767        let mut stream = provider.watch_canonical_blocks_from(0).into_stream();
768        drop(provider);
769
770        let next = timeout(Duration::from_secs(1), stream.next()).await.unwrap();
771        assert!(next.is_none());
772    }
773
774    #[tokio::test]
775    async fn errors_instead_of_underflow_when_backfilling_genesis_parent() {
776        let chain = MockChain::new();
777        {
778            let mut state = chain.state.write().unwrap();
779            state.head = 2;
780            // Intentionally inconsistent mock state to force a malformed backfill path:
781            // request #1 -> block number 0 (hash=1), request #2 -> another block number 0
782            // with a non-matching parent hash. This drives reconciliation to `height == 0`.
783            state.blocks.insert(1, block(0, 1, 0));
784            state.blocks.insert(2, block(0, 2, 9));
785        }
786
787        let provider = chain.provider();
788        let mut stream = provider
789            .watch_blocks_from(1)
790            .block_tag(BlockNumberOrTag::Latest)
791            .poll_interval(Duration::from_millis(1))
792            .canonical()
793            .rpc_concurrency(1)
794            .max_reorg_depth(8)
795            .into_stream();
796
797        let first = timeout(Duration::from_secs(1), stream.next()).await.unwrap().unwrap().unwrap();
798        match first {
799            CanonicalEvent::Added(block) => assert_eq!(block.header.number, 0),
800            other => panic!("expected Added(0), got {other:?}"),
801        }
802
803        let err =
804            timeout(Duration::from_secs(1), stream.next()).await.unwrap().unwrap().unwrap_err();
805        assert!(format!("{err}").contains("genesis block"));
806    }
807}