1use crate::{BackfillJobFactory, ExExNotification, StreamBackfillJob, WalHandle};
2use alloy_consensus::BlockHeader;
3use alloy_eips::BlockNumHash;
4use futures::{Stream, StreamExt};
5use reth_ethereum_primitives::EthPrimitives;
6use reth_evm::ConfigureEvm;
7use reth_exex_types::ExExHead;
8use reth_node_api::NodePrimitives;
9use reth_provider::{BlockNumReader, BlockReader, Chain, HeaderProvider, StateProviderFactory};
10use reth_stages_api::ExecutionStageThresholds;
11use reth_tracing::tracing::debug;
12use std::{
13 collections::VecDeque,
14 fmt::Debug,
15 pin::Pin,
16 sync::Arc,
17 task::{ready, Context, Poll},
18};
19use tokio::sync::mpsc::Receiver;
20
21#[derive(Debug)]
25pub struct ExExNotifications<P, E>
26where
27 E: ConfigureEvm,
28{
29 inner: ExExNotificationsInner<P, E>,
30}
31
32pub trait ExExNotificationsStream<N: NodePrimitives = EthPrimitives>:
36 Stream<Item = eyre::Result<ExExNotification<N>>> + Unpin
37{
38 fn set_without_head(&mut self);
44
45 fn set_with_head(&mut self, exex_head: ExExHead);
52
53 fn without_head(self) -> Self
57 where
58 Self: Sized;
59
60 fn with_head(self, exex_head: ExExHead) -> Self
64 where
65 Self: Sized;
66
67 fn set_backfill_thresholds(&mut self, _thresholds: ExecutionStageThresholds) {}
75}
76
77#[derive(Debug)]
78enum ExExNotificationsInner<P, E>
79where
80 E: ConfigureEvm,
81{
82 WithoutHead(ExExNotificationsWithoutHead<P, E>),
84 WithHead(Box<ExExNotificationsWithHead<P, E>>),
87 Invalid,
90}
91
92impl<P, E> ExExNotificationsInner<P, E>
93where
94 E: ConfigureEvm,
95{
96 fn provider(&self) -> &P {
98 match self {
99 Self::WithoutHead(n) => &n.provider,
100 Self::WithHead(n) => &n.provider,
101 Self::Invalid => unreachable!(),
102 }
103 }
104}
105
106impl<P, E> ExExNotifications<P, E>
107where
108 E: ConfigureEvm,
109{
110 pub const fn new(
112 node_head: BlockNumHash,
113 provider: P,
114 evm_config: E,
115 notifications: Receiver<ExExNotification<E::Primitives>>,
116 wal_handle: WalHandle<E::Primitives>,
117 ) -> Self {
118 Self {
119 inner: ExExNotificationsInner::WithoutHead(ExExNotificationsWithoutHead::new(
120 node_head,
121 provider,
122 evm_config,
123 notifications,
124 wal_handle,
125 )),
126 }
127 }
128
129 pub fn catch_up_with_head(&mut self, exex_head: ExExHead) -> eyre::Result<()>
132 where
133 P: BlockNumReader,
134 {
135 let local_head: BlockNumHash = self.inner.provider().chain_info()?.into();
138
139 let current = std::mem::replace(&mut self.inner, ExExNotificationsInner::Invalid);
140 let (provider, evm_config, notifications, wal_handle, backfill_thresholds) = match current {
141 ExExNotificationsInner::WithoutHead(n) => {
142 (n.provider, n.evm_config, n.notifications, n.wal_handle, None)
143 }
144 ExExNotificationsInner::WithHead(n) => {
145 (n.provider, n.evm_config, n.notifications, n.wal_handle, n.backfill_thresholds)
146 }
147 ExExNotificationsInner::Invalid => unreachable!(),
148 };
149 let mut with_head = ExExNotificationsWithHead::new(
150 local_head,
151 provider,
152 evm_config,
153 notifications,
154 wal_handle,
155 exex_head,
156 );
157 with_head.backfill_thresholds = backfill_thresholds;
160 self.inner = ExExNotificationsInner::WithHead(Box::new(with_head));
161 Ok(())
162 }
163}
164
165impl<P, E> ExExNotificationsStream<E::Primitives> for ExExNotifications<P, E>
166where
167 P: BlockReader + HeaderProvider + StateProviderFactory + Clone + Unpin + 'static,
168 E: ConfigureEvm<Primitives: NodePrimitives<Block = P::Block>> + Clone + Unpin + 'static,
169{
170 fn set_without_head(&mut self) {
171 let current = std::mem::replace(&mut self.inner, ExExNotificationsInner::Invalid);
172 self.inner = ExExNotificationsInner::WithoutHead(match current {
173 ExExNotificationsInner::WithoutHead(notifications) => notifications,
174 ExExNotificationsInner::WithHead(notifications) => ExExNotificationsWithoutHead::new(
175 notifications.initial_local_head,
176 notifications.provider,
177 notifications.evm_config,
178 notifications.notifications,
179 notifications.wal_handle,
180 ),
181 ExExNotificationsInner::Invalid => unreachable!(),
182 });
183 }
184
185 fn set_with_head(&mut self, exex_head: ExExHead) {
186 let current = std::mem::replace(&mut self.inner, ExExNotificationsInner::Invalid);
187 self.inner = ExExNotificationsInner::WithHead(match current {
188 ExExNotificationsInner::WithoutHead(notifications) => {
189 Box::new(notifications.with_head(exex_head))
190 }
191 ExExNotificationsInner::WithHead(notifications) => {
192 Box::new(ExExNotificationsWithHead::new(
193 notifications.initial_local_head,
194 notifications.provider,
195 notifications.evm_config,
196 notifications.notifications,
197 notifications.wal_handle,
198 exex_head,
199 ))
200 }
201 ExExNotificationsInner::Invalid => unreachable!(),
202 });
203 }
204
205 fn without_head(mut self) -> Self {
206 self.set_without_head();
207 self
208 }
209
210 fn with_head(mut self, exex_head: ExExHead) -> Self {
211 self.set_with_head(exex_head);
212 self
213 }
214
215 fn set_backfill_thresholds(&mut self, thresholds: ExecutionStageThresholds) {
216 if let ExExNotificationsInner::WithHead(notifications) = &mut self.inner {
217 notifications.backfill_thresholds = Some(thresholds);
218 }
219 }
220}
221
222impl<P, E> Stream for ExExNotifications<P, E>
223where
224 P: BlockReader + HeaderProvider + StateProviderFactory + Clone + Unpin + 'static,
225 E: ConfigureEvm<Primitives: NodePrimitives<Block = P::Block>> + 'static,
226{
227 type Item = eyre::Result<ExExNotification<E::Primitives>>;
228
229 fn poll_next(
230 self: std::pin::Pin<&mut Self>,
231 cx: &mut std::task::Context<'_>,
232 ) -> std::task::Poll<Option<Self::Item>> {
233 match &mut self.get_mut().inner {
234 ExExNotificationsInner::WithoutHead(notifications) => {
235 notifications.poll_next_unpin(cx).map(|result| result.map(Ok))
236 }
237 ExExNotificationsInner::WithHead(notifications) => notifications.poll_next_unpin(cx),
238 ExExNotificationsInner::Invalid => unreachable!(),
239 }
240 }
241}
242
243pub struct ExExNotificationsWithoutHead<P, E>
245where
246 E: ConfigureEvm,
247{
248 node_head: BlockNumHash,
249 provider: P,
250 evm_config: E,
251 notifications: Receiver<ExExNotification<E::Primitives>>,
252 wal_handle: WalHandle<E::Primitives>,
253}
254
255impl<P: Debug, E> Debug for ExExNotificationsWithoutHead<P, E>
256where
257 E: ConfigureEvm + Debug,
258{
259 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
260 f.debug_struct("ExExNotifications")
261 .field("provider", &self.provider)
262 .field("evm_config", &self.evm_config)
263 .field("notifications", &self.notifications)
264 .finish()
265 }
266}
267
268impl<P, E> ExExNotificationsWithoutHead<P, E>
269where
270 E: ConfigureEvm,
271{
272 const fn new(
274 node_head: BlockNumHash,
275 provider: P,
276 evm_config: E,
277 notifications: Receiver<ExExNotification<E::Primitives>>,
278 wal_handle: WalHandle<E::Primitives>,
279 ) -> Self {
280 Self { node_head, provider, evm_config, notifications, wal_handle }
281 }
282
283 fn with_head(self, head: ExExHead) -> ExExNotificationsWithHead<P, E> {
285 ExExNotificationsWithHead::new(
286 self.node_head,
287 self.provider,
288 self.evm_config,
289 self.notifications,
290 self.wal_handle,
291 head,
292 )
293 }
294}
295
296impl<P: Unpin, E> Stream for ExExNotificationsWithoutHead<P, E>
297where
298 E: ConfigureEvm,
299{
300 type Item = ExExNotification<E::Primitives>;
301
302 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
303 self.get_mut().notifications.poll_recv(cx)
304 }
305}
306
307#[derive(Debug)]
316pub struct ExExNotificationsWithHead<P, E>
317where
318 E: ConfigureEvm,
319{
320 initial_local_head: BlockNumHash,
322 provider: P,
323 evm_config: E,
324 notifications: Receiver<ExExNotification<E::Primitives>>,
325 wal_handle: WalHandle<E::Primitives>,
326 initial_exex_head: ExExHead,
328
329 pending_check_canonical: bool,
332 pending_check_backfill: bool,
335 backfill_job: Option<StreamBackfillJob<E, P, Chain<E::Primitives>>>,
337 backfill_thresholds: Option<ExecutionStageThresholds>,
339 pending_notifications: VecDeque<ExExNotification<E::Primitives>>,
342}
343
344impl<P, E> ExExNotificationsWithHead<P, E>
345where
346 E: ConfigureEvm,
347{
348 const fn new(
350 node_head: BlockNumHash,
351 provider: P,
352 evm_config: E,
353 notifications: Receiver<ExExNotification<E::Primitives>>,
354 wal_handle: WalHandle<E::Primitives>,
355 exex_head: ExExHead,
356 ) -> Self {
357 Self {
358 initial_local_head: node_head,
359 provider,
360 evm_config,
361 notifications,
362 wal_handle,
363 initial_exex_head: exex_head,
364 pending_check_canonical: true,
365 pending_check_backfill: true,
366 backfill_job: None,
367 backfill_thresholds: None,
368 pending_notifications: VecDeque::new(),
369 }
370 }
371
372 pub const fn with_backfill_thresholds(mut self, thresholds: ExecutionStageThresholds) -> Self {
381 self.backfill_thresholds = Some(thresholds);
382 self
383 }
384}
385
386impl<P, E> ExExNotificationsWithHead<P, E>
387where
388 P: BlockReader + HeaderProvider + StateProviderFactory + Clone + Unpin + 'static,
389 E: ConfigureEvm<Primitives: NodePrimitives<Block = P::Block>> + Clone + Unpin + 'static,
390{
391 fn check_canonical(&mut self) -> eyre::Result<Option<ExExNotification<E::Primitives>>> {
397 if self.provider.is_known(self.initial_exex_head.block.hash)? &&
398 self.initial_exex_head.block.number <= self.initial_local_head.number
399 {
400 debug!(target: "exex::notifications", "ExEx head is on the canonical chain");
402 return Ok(None)
403 }
404
405 let Some(notification) = self
410 .wal_handle
411 .get_committed_notification_by_block_hash(&self.initial_exex_head.block.hash)?
412 else {
413 if self.initial_exex_head.block.number > self.initial_local_head.number {
415 debug!(target: "exex::notifications", "ExEx head is ahead of the canonical chain");
416 return Ok(None);
417 }
418
419 return Err(eyre::eyre!(
420 "Could not find notification for block hash {:?} in the WAL",
421 self.initial_exex_head.block.hash
422 ))
423 };
424
425 let committed_chain = notification.committed_chain().unwrap();
427 let new_exex_head =
428 (committed_chain.first().parent_hash(), committed_chain.first().number() - 1).into();
429 debug!(target: "exex::notifications", old_exex_head = ?self.initial_exex_head.block, new_exex_head = ?new_exex_head, "ExEx head updated");
430 self.initial_exex_head.block = new_exex_head;
431
432 Ok(Some(notification.into_inverted()))
435 }
436
437 fn check_backfill(&mut self) -> eyre::Result<()> {
448 let mut backfill_job_factory =
449 BackfillJobFactory::new(self.evm_config.clone(), self.provider.clone());
450 if let Some(thresholds) = self.backfill_thresholds.clone() {
451 backfill_job_factory = backfill_job_factory.with_thresholds(thresholds);
452 }
453 match self.initial_exex_head.block.number.cmp(&self.initial_local_head.number) {
454 std::cmp::Ordering::Less => {
455 debug!(target: "exex::notifications", "ExEx is behind the node head and on the canonical chain, starting backfill");
457 let backfill = backfill_job_factory
458 .backfill(
459 self.initial_exex_head.block.number + 1..=self.initial_local_head.number,
460 )
461 .into_stream();
462 self.backfill_job = Some(backfill);
463 }
464 std::cmp::Ordering::Equal => {
465 debug!(target: "exex::notifications", "ExEx is at the node head");
466 }
467 std::cmp::Ordering::Greater => {
468 debug!(target: "exex::notifications", "ExEx is ahead of the node head");
469 }
470 };
471
472 Ok(())
473 }
474}
475
476impl<P, E> Stream for ExExNotificationsWithHead<P, E>
477where
478 P: BlockReader + HeaderProvider + StateProviderFactory + Clone + Unpin + 'static,
479 E: ConfigureEvm<Primitives: NodePrimitives<Block = P::Block>> + Clone + Unpin + 'static,
480{
481 type Item = eyre::Result<ExExNotification<E::Primitives>>;
482
483 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
484 let this = self.get_mut();
485
486 if this.pending_check_canonical {
488 if let Some(canonical_notification) = this.check_canonical()? {
489 return Poll::Ready(Some(Ok(canonical_notification)))
490 }
491
492 this.pending_check_canonical = false;
494 }
495
496 if this.pending_check_backfill {
498 this.check_backfill()?;
499 this.pending_check_backfill = false;
500 }
501
502 if let Some(backfill_job) = &mut this.backfill_job {
504 debug!(target: "exex::notifications", "Polling backfill job");
505
506 while let Poll::Ready(Some(notification)) = this.notifications.poll_recv(cx) {
515 if notification.reverted_chain().is_some() {
520 this.pending_notifications.push_back(notification);
521 continue;
522 }
523 if let Some(committed) = notification.committed_chain() &&
524 committed.tip().number() <= this.initial_local_head.number
525 {
526 continue;
528 }
529 this.pending_notifications.push_back(notification);
531 }
532
533 if let Some(chain) = ready!(backfill_job.poll_next_unpin(cx)).transpose()? {
534 debug!(target: "exex::notifications", range = ?chain.range(), "Backfill job returned a chain");
535 return Poll::Ready(Some(Ok(ExExNotification::ChainCommitted {
536 new: Arc::new(chain),
537 })))
538 }
539
540 this.backfill_job = None;
542 }
543
544 if let Some(notification) = this.pending_notifications.pop_front() {
546 return Poll::Ready(Some(Ok(notification)))
547 }
548
549 loop {
551 let Some(notification) = ready!(this.notifications.poll_recv(cx)) else {
552 return Poll::Ready(None)
553 };
554
555 if let Some(committed) = notification.committed_chain() {
557 if this.initial_exex_head.block.number >= committed.tip().number() {
559 continue
560 }
561 }
562
563 return Poll::Ready(Some(Ok(notification)))
564 }
565 }
566}
567
568#[cfg(test)]
569mod tests {
570 use super::*;
571 use crate::Wal;
572 use alloy_consensus::Header;
573 use alloy_eips::BlockNumHash;
574 use eyre::OptionExt;
575 use futures::StreamExt;
576 use reth_db_common::init::init_genesis;
577 use reth_ethereum_primitives::Block;
578 use reth_evm_ethereum::EthEvmConfig;
579 use reth_primitives_traits::Block as _;
580 use reth_provider::{
581 providers::BlockchainProvider, test_utils::create_test_provider_factory, BlockWriter,
582 Chain, DBProvider, DatabaseProviderFactory,
583 };
584 use reth_testing_utils::generators::{self, random_block, BlockParams};
585 use std::collections::BTreeMap;
586 use tokio::sync::mpsc;
587
588 #[tokio::test]
589 async fn exex_notifications_behind_head_canonical() -> eyre::Result<()> {
590 let mut rng = generators::rng();
591
592 let temp_dir = tempfile::tempdir().unwrap();
593 let wal = Wal::new(temp_dir.path()).unwrap();
594
595 let provider_factory = create_test_provider_factory();
596 let genesis_hash = init_genesis(&provider_factory)?;
597 let genesis_block = provider_factory
598 .block(genesis_hash.into())?
599 .ok_or_else(|| eyre::eyre!("genesis block not found"))?;
600
601 let provider = BlockchainProvider::new(provider_factory.clone())?;
602
603 let node_head_block = random_block(
604 &mut rng,
605 genesis_block.number + 1,
606 BlockParams { parent: Some(genesis_hash), tx_count: Some(0), ..Default::default() },
607 )
608 .try_recover()?;
609 let node_head = node_head_block.num_hash();
610 let provider_rw = provider_factory.provider_rw()?;
611 provider_rw.insert_block(&node_head_block)?;
612 provider_rw.commit()?;
613 let exex_head =
614 ExExHead { block: BlockNumHash { number: genesis_block.number, hash: genesis_hash } };
615
616 let notification = ExExNotification::ChainCommitted {
617 new: Arc::new(Chain::new(
618 vec![random_block(
619 &mut rng,
620 node_head.number + 1,
621 BlockParams { parent: Some(node_head.hash), ..Default::default() },
622 )
623 .try_recover()?],
624 Default::default(),
625 BTreeMap::new(),
626 )),
627 };
628
629 let (notifications_tx, notifications_rx) = mpsc::channel(1);
630
631 notifications_tx.send(notification.clone()).await?;
632
633 let mut notifications = ExExNotificationsWithoutHead::new(
634 node_head,
635 provider,
636 EthEvmConfig::mainnet(),
637 notifications_rx,
638 wal.handle(),
639 )
640 .with_head(exex_head);
641
642 assert_eq!(
644 notifications.next().await.transpose()?,
645 Some(ExExNotification::ChainCommitted {
646 new: Arc::new(
647 BackfillJobFactory::new(
648 notifications.evm_config.clone(),
649 notifications.provider.clone()
650 )
651 .backfill(1..=1)
652 .next()
653 .ok_or_eyre("failed to backfill")??
654 )
655 })
656 );
657
658 assert_eq!(notifications.next().await.transpose()?, Some(notification));
660
661 Ok(())
662 }
663
664 #[tokio::test]
665 async fn catch_up_with_head_after_pause_backfills_missed_blocks() -> eyre::Result<()> {
666 let mut rng = generators::rng();
667
668 let temp_dir = tempfile::tempdir().unwrap();
669 let wal = Wal::new(temp_dir.path()).unwrap();
670
671 let provider_factory = create_test_provider_factory();
672 let genesis_hash = init_genesis(&provider_factory)?;
673 let genesis_block = provider_factory
674 .block(genesis_hash.into())?
675 .ok_or_else(|| eyre::eyre!("genesis block not found"))?;
676 let provider = BlockchainProvider::new(provider_factory.clone())?;
677
678 let exex_head =
679 ExExHead { block: BlockNumHash { number: genesis_block.number, hash: genesis_hash } };
680 let (notifications_tx, notifications_rx) = mpsc::channel(1);
681
682 let evm_config = EthEvmConfig::mainnet();
683 let mut notifications = ExExNotifications::new(
684 BlockNumHash { number: genesis_block.number, hash: genesis_hash },
685 provider.clone(),
686 evm_config.clone(),
687 notifications_rx,
688 wal.handle(),
689 );
690 notifications.set_with_head(exex_head);
692
693 let node_head_block = random_block(
695 &mut rng,
696 genesis_block.number + 1,
697 BlockParams { parent: Some(genesis_hash), tx_count: Some(0), ..Default::default() },
698 )
699 .try_recover()?;
700 let node_head = node_head_block.num_hash();
701 let block_1_notification = ExExNotification::ChainCommitted {
702 new: Arc::new(Chain::new(
703 vec![node_head_block.clone()],
704 Default::default(),
705 BTreeMap::new(),
706 )),
707 };
708 notifications_tx.send(block_1_notification.clone()).await?;
709 assert_eq!(notifications.next().await.transpose()?, Some(block_1_notification));
710
711 let provider_rw = provider_factory.provider_rw()?;
714 provider_rw.insert_block(&node_head_block)?;
715 provider_rw.commit()?;
716 provider
717 .canonical_in_memory_state()
718 .set_canonical_head(node_head_block.clone_sealed_header());
719
720 notifications.catch_up_with_head(exex_head)?;
723
724 let block_2_notification = ExExNotification::ChainCommitted {
725 new: Arc::new(Chain::new(
726 vec![random_block(
727 &mut rng,
728 node_head.number + 1,
729 BlockParams { parent: Some(node_head.hash), ..Default::default() },
730 )
731 .try_recover()?],
732 Default::default(),
733 BTreeMap::new(),
734 )),
735 };
736 notifications_tx.send(block_2_notification.clone()).await?;
737
738 assert_eq!(
740 notifications.next().await.transpose()?,
741 Some(ExExNotification::ChainCommitted {
742 new: Arc::new(
743 BackfillJobFactory::new(evm_config, provider)
744 .backfill(1..=1)
745 .next()
746 .ok_or_eyre("failed to backfill")??
747 )
748 })
749 );
750 assert_eq!(notifications.next().await.transpose()?, Some(block_2_notification));
752
753 Ok(())
754 }
755
756 #[tokio::test]
757 async fn exex_notifications_same_head_canonical() -> eyre::Result<()> {
758 let temp_dir = tempfile::tempdir().unwrap();
759 let wal = Wal::new(temp_dir.path()).unwrap();
760
761 let provider_factory = create_test_provider_factory();
762 let genesis_hash = init_genesis(&provider_factory)?;
763 let genesis_block = provider_factory
764 .block(genesis_hash.into())?
765 .ok_or_else(|| eyre::eyre!("genesis block not found"))?;
766
767 let provider = BlockchainProvider::new(provider_factory)?;
768
769 let node_head = BlockNumHash { number: genesis_block.number, hash: genesis_hash };
770 let exex_head = ExExHead { block: node_head };
771
772 let notification = ExExNotification::ChainCommitted {
773 new: Arc::new(Chain::new(
774 vec![Block {
775 header: Header {
776 parent_hash: node_head.hash,
777 number: node_head.number + 1,
778 ..Default::default()
779 },
780 ..Default::default()
781 }
782 .seal_slow()
783 .try_recover()?],
784 Default::default(),
785 BTreeMap::new(),
786 )),
787 };
788
789 let (notifications_tx, notifications_rx) = mpsc::channel(1);
790
791 notifications_tx.send(notification.clone()).await?;
792
793 let mut notifications = ExExNotificationsWithoutHead::new(
794 node_head,
795 provider,
796 EthEvmConfig::mainnet(),
797 notifications_rx,
798 wal.handle(),
799 )
800 .with_head(exex_head);
801
802 let new_notification = notifications.next().await.transpose()?;
803 assert_eq!(new_notification, Some(notification));
804
805 Ok(())
806 }
807
808 #[tokio::test]
809 async fn exex_notifications_same_head_non_canonical() -> eyre::Result<()> {
810 let mut rng = generators::rng();
811
812 let temp_dir = tempfile::tempdir().unwrap();
813 let wal = Wal::new(temp_dir.path()).unwrap();
814
815 let provider_factory = create_test_provider_factory();
816 let genesis_hash = init_genesis(&provider_factory)?;
817 let genesis_block = provider_factory
818 .block(genesis_hash.into())?
819 .ok_or_else(|| eyre::eyre!("genesis block not found"))?;
820
821 let provider = BlockchainProvider::new(provider_factory)?;
822
823 let node_head_block = random_block(
824 &mut rng,
825 genesis_block.number + 1,
826 BlockParams { parent: Some(genesis_hash), tx_count: Some(0), ..Default::default() },
827 )
828 .try_recover()?;
829 let node_head = node_head_block.num_hash();
830 let provider_rw = provider.database_provider_rw()?;
831 provider_rw.insert_block(&node_head_block)?;
832 provider_rw.commit()?;
833 let node_head_notification = ExExNotification::ChainCommitted {
834 new: Arc::new(
835 BackfillJobFactory::new(EthEvmConfig::mainnet(), provider.clone())
836 .backfill(node_head.number..=node_head.number)
837 .next()
838 .ok_or_else(|| eyre::eyre!("failed to backfill"))??,
839 ),
840 };
841
842 let exex_head_block = random_block(
843 &mut rng,
844 genesis_block.number + 1,
845 BlockParams { parent: Some(genesis_hash), tx_count: Some(0), ..Default::default() },
846 );
847 let exex_head = ExExHead { block: exex_head_block.num_hash() };
848 let exex_head_notification = ExExNotification::ChainCommitted {
849 new: Arc::new(Chain::new(
850 vec![exex_head_block.clone().try_recover()?],
851 Default::default(),
852 BTreeMap::new(),
853 )),
854 };
855 wal.commit(&exex_head_notification)?;
856
857 let new_notification = ExExNotification::ChainCommitted {
858 new: Arc::new(Chain::new(
859 vec![random_block(
860 &mut rng,
861 node_head.number + 1,
862 BlockParams { parent: Some(node_head.hash), ..Default::default() },
863 )
864 .try_recover()?],
865 Default::default(),
866 BTreeMap::new(),
867 )),
868 };
869
870 let (notifications_tx, notifications_rx) = mpsc::channel(1);
871
872 notifications_tx.send(new_notification.clone()).await?;
873
874 let mut notifications = ExExNotificationsWithoutHead::new(
875 node_head,
876 provider,
877 EthEvmConfig::mainnet(),
878 notifications_rx,
879 wal.handle(),
880 )
881 .with_head(exex_head);
882
883 assert_eq!(
886 notifications.next().await.transpose()?,
887 Some(exex_head_notification.into_inverted())
888 );
889 assert_eq!(notifications.next().await.transpose()?, Some(node_head_notification));
892 assert_eq!(notifications.next().await.transpose()?, Some(new_notification));
894
895 Ok(())
896 }
897
898 #[tokio::test]
899 async fn test_notifications_ahead_of_head() -> eyre::Result<()> {
900 reth_tracing::init_test_tracing();
901 let mut rng = generators::rng();
902
903 let temp_dir = tempfile::tempdir().unwrap();
904 let wal = Wal::new(temp_dir.path()).unwrap();
905
906 let provider_factory = create_test_provider_factory();
907 let genesis_hash = init_genesis(&provider_factory)?;
908 let genesis_block = provider_factory
909 .block(genesis_hash.into())?
910 .ok_or_else(|| eyre::eyre!("genesis block not found"))?;
911
912 let provider = BlockchainProvider::new(provider_factory)?;
913
914 let exex_head_block = random_block(
915 &mut rng,
916 genesis_block.number + 1,
917 BlockParams { parent: Some(genesis_hash), tx_count: Some(0), ..Default::default() },
918 );
919 let exex_head_notification = ExExNotification::ChainCommitted {
920 new: Arc::new(Chain::new(
921 vec![exex_head_block.clone().try_recover()?],
922 Default::default(),
923 BTreeMap::new(),
924 )),
925 };
926 wal.commit(&exex_head_notification)?;
927
928 let node_head = BlockNumHash { number: genesis_block.number, hash: genesis_hash };
929 let exex_head = ExExHead {
930 block: BlockNumHash { number: exex_head_block.number, hash: exex_head_block.hash() },
931 };
932
933 let new_notification = ExExNotification::ChainCommitted {
934 new: Arc::new(Chain::new(
935 vec![random_block(
936 &mut rng,
937 genesis_block.number + 1,
938 BlockParams { parent: Some(genesis_hash), ..Default::default() },
939 )
940 .try_recover()?],
941 Default::default(),
942 BTreeMap::new(),
943 )),
944 };
945
946 let (notifications_tx, notifications_rx) = mpsc::channel(1);
947
948 notifications_tx.send(new_notification.clone()).await?;
949
950 let mut notifications = ExExNotificationsWithoutHead::new(
951 node_head,
952 provider,
953 EthEvmConfig::mainnet(),
954 notifications_rx,
955 wal.handle(),
956 )
957 .with_head(exex_head);
958
959 assert_eq!(
962 notifications.next().await.transpose()?,
963 Some(exex_head_notification.into_inverted())
964 );
965
966 assert_eq!(notifications.next().await.transpose()?, Some(new_notification));
968
969 Ok(())
970 }
971
972 #[tokio::test]
985 async fn exex_notifications_backfill_drains_channel() -> eyre::Result<()> {
986 let mut rng = generators::rng();
987
988 let temp_dir = tempfile::tempdir().unwrap();
989 let wal = Wal::new(temp_dir.path()).unwrap();
990
991 let provider_factory = create_test_provider_factory();
992 let genesis_hash = init_genesis(&provider_factory)?;
993 let genesis_block = provider_factory
994 .block(genesis_hash.into())?
995 .ok_or_else(|| eyre::eyre!("genesis block not found"))?;
996
997 let provider = BlockchainProvider::new(provider_factory.clone())?;
998
999 let node_head_block = random_block(
1001 &mut rng,
1002 genesis_block.number + 1,
1003 BlockParams { parent: Some(genesis_hash), tx_count: Some(0), ..Default::default() },
1004 )
1005 .try_recover()?;
1006 let node_head = node_head_block.num_hash();
1007 let provider_rw = provider_factory.provider_rw()?;
1008 provider_rw.insert_block(&node_head_block)?;
1009 provider_rw.commit()?;
1010
1011 let exex_head =
1013 ExExHead { block: BlockNumHash { number: genesis_block.number, hash: genesis_hash } };
1014
1015 let post_backfill_notification = ExExNotification::ChainCommitted {
1017 new: Arc::new(Chain::new(
1018 vec![random_block(
1019 &mut rng,
1020 node_head.number + 1,
1021 BlockParams { parent: Some(node_head.hash), ..Default::default() },
1022 )
1023 .try_recover()?],
1024 Default::default(),
1025 BTreeMap::new(),
1026 )),
1027 };
1028
1029 let probe_notification = ExExNotification::ChainCommitted {
1031 new: Arc::new(Chain::new(
1032 vec![random_block(
1033 &mut rng,
1034 node_head.number + 2,
1035 BlockParams { parent: None, ..Default::default() },
1036 )
1037 .try_recover()?],
1038 Default::default(),
1039 BTreeMap::new(),
1040 )),
1041 };
1042
1043 let (notifications_tx, notifications_rx) = mpsc::channel(1);
1044
1045 notifications_tx.send(post_backfill_notification.clone()).await?;
1047
1048 assert!(
1051 notifications_tx.try_send(probe_notification.clone()).is_err(),
1052 "channel should be full before backfill poll"
1053 );
1054
1055 let mut notifications = ExExNotificationsWithoutHead::new(
1056 node_head,
1057 provider,
1058 EthEvmConfig::mainnet(),
1059 notifications_rx,
1060 wal.handle(),
1061 )
1062 .with_head(exex_head);
1063
1064 let backfill_result = notifications.next().await.transpose()?;
1068 assert_eq!(
1069 backfill_result,
1070 Some(ExExNotification::ChainCommitted {
1071 new: Arc::new(
1072 BackfillJobFactory::new(
1073 notifications.evm_config.clone(),
1074 notifications.provider.clone()
1075 )
1076 .backfill(1..=1)
1077 .next()
1078 .ok_or_eyre("failed to backfill")??
1079 )
1080 })
1081 );
1082
1083 assert!(
1087 notifications_tx.try_send(probe_notification.clone()).is_ok(),
1088 "channel should have been drained during backfill poll"
1089 );
1090
1091 let buffered = notifications.next().await.transpose()?;
1094 assert_eq!(buffered, Some(post_backfill_notification));
1095
1096 let probe = notifications.next().await.transpose()?;
1098 assert_eq!(probe, Some(probe_notification));
1099
1100 Ok(())
1101 }
1102}