1use std::{cmp::Ordering, fmt, num::NonZeroU64, str::FromStr, sync::Arc};
2#[cfg(feature = "node")]
3use std::{cmp::min, num::NonZeroUsize, pin::Pin, result::Result as StdResult, time::Instant};
4
5#[cfg(feature = "node")]
6use alloy::{
7 eips::BlockId,
8 hex,
9 primitives::Address,
10 providers::{Provider, ProviderBuilder, WsConnect},
11 rpc::{
12 client::RpcClient,
13 json_rpc::{ErrorPayload, RequestPacket, ResponsePacket},
14 },
15 transports::{HttpError, RpcError, TransportErrorKind, http::Http},
16};
17use alloy::{
18 primitives::{B256, U256},
19 rpc::types::Block,
20};
21#[cfg(feature = "node")]
22use anyhow::Context;
23#[cfg(feature = "node")]
24use async_broadcast::Sender;
25#[cfg(feature = "node")]
26use async_trait::async_trait;
27use clap::Parser;
28use committable::{Commitment, Committable, RawCommitmentBuilder};
29#[cfg(feature = "node")]
30use espresso_utils::redact::redact_url;
31#[cfg(feature = "node")]
32use futures::{
33 future::{Future, TryFuture, TryFutureExt},
34 stream::{self, StreamExt},
35};
36#[cfg(feature = "node")]
37use hotshot_contract_adapter::sol_types::FeeContract;
38use hotshot_types::traits::metrics::Metrics;
39#[cfg(feature = "node")]
40use lru::LruCache;
41#[cfg(feature = "node")]
42use parking_lot::RwLock;
43#[cfg(feature = "node")]
44use tokio::{
45 spawn,
46 sync::{Mutex, MutexGuard, Notify},
47 time::{Duration, sleep},
48};
49#[cfg(feature = "node")]
50use tower_service::Service;
51#[cfg(feature = "node")]
52use tracing::Instrument;
53#[cfg(feature = "node")]
54use url::Url;
55
56use super::{
57 L1BlockInfo, L1SafetyMargin,
58 v0_1::{L1BlockInfoWithParent, ParseL1SafetyMarginError},
59};
60#[cfg(feature = "node")]
61use super::{
62 L1ClientMetrics, L1State, L1UpdateTask,
63 v0_1::{SingleTransport, SingleTransportStatus, SwitchingTransport},
64};
65use crate::L1ClientOptions;
66#[cfg(feature = "node")]
67use crate::{FeeInfo, L1Client, L1Event, L1Snapshot};
68
69impl PartialOrd for L1BlockInfo {
70 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
71 Some(self.cmp(other))
72 }
73}
74
75impl Ord for L1BlockInfo {
76 fn cmp(&self, other: &Self) -> Ordering {
77 self.number.cmp(&other.number)
78 }
79}
80
81impl From<&Block> for L1BlockInfo {
82 fn from(block: &Block) -> Self {
83 Self {
84 number: block.header.number,
85 timestamp: U256::from(block.header.timestamp),
86 hash: block.header.hash,
87 }
88 }
89}
90
91impl From<&Block> for L1BlockInfoWithParent {
92 fn from(block: &Block) -> Self {
93 Self {
94 info: block.into(),
95 parent_hash: block.header.parent_hash,
96 }
97 }
98}
99
100impl Committable for L1BlockInfo {
101 fn commit(&self) -> Commitment<Self> {
102 let timestamp: [u8; 32] = self.timestamp.to_le_bytes();
103
104 RawCommitmentBuilder::new(&Self::tag())
105 .u64_field("number", self.number)
106 .constant_str("timestamp")
108 .fixed_size_bytes(×tamp)
109 .constant_str("hash")
110 .fixed_size_bytes(&self.hash.0)
111 .finalize()
112 }
113
114 fn tag() -> String {
115 "L1BLOCK".into()
116 }
117}
118
119impl L1BlockInfo {
120 pub fn number(&self) -> u64 {
121 self.number
122 }
123
124 pub fn timestamp(&self) -> U256 {
125 self.timestamp
126 }
127
128 pub fn hash(&self) -> B256 {
129 self.hash
130 }
131}
132
133impl L1SafetyMargin {
134 pub const UNLIMITED: Self = Self(None);
136
137 pub fn blocks(self) -> Option<NonZeroU64> {
138 self.0
139 }
140}
141
142impl From<NonZeroU64> for L1SafetyMargin {
143 fn from(blocks: NonZeroU64) -> Self {
144 Self(Some(blocks))
145 }
146}
147
148impl FromStr for L1SafetyMargin {
149 type Err = ParseL1SafetyMarginError;
150
151 fn from_str(s: &str) -> Result<Self, Self::Err> {
152 let s = s.trim();
153 if s.eq_ignore_ascii_case("unlimited") {
154 return Ok(Self(None));
155 }
156 let blocks = s.parse().map_err(|_| ParseL1SafetyMarginError {
157 input: s.to_string(),
158 })?;
159 Ok(Self(Some(blocks)))
160 }
161}
162
163impl fmt::Display for L1SafetyMargin {
164 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165 match self.0 {
166 Some(blocks) => write!(f, "{blocks}"),
167 None => write!(f, "unlimited"),
168 }
169 }
170}
171
172#[cfg(feature = "node")]
173impl Drop for L1UpdateTask {
174 fn drop(&mut self) {
175 if let Some(task) = self.0.get_mut().take() {
176 task.abort();
177 }
178 }
179}
180
181impl Default for L1ClientOptions {
182 fn default() -> Self {
183 Self::parse_from(std::iter::empty::<String>())
184 }
185}
186
187impl L1ClientOptions {
188 pub fn with_metrics(mut self, metrics: &(impl Metrics + ?Sized)) -> Self {
190 self.metrics = Arc::new(metrics.subgroup("l1".into()));
191 self
192 }
193
194 #[cfg(feature = "node")]
196 pub fn connect(self, urls: Vec<Url>) -> anyhow::Result<L1Client> {
197 let t = SwitchingTransport::new(self, urls)
199 .with_context(|| "failed to create switching transport")?;
200 Ok(L1Client::with_transport(t))
202 }
203
204 #[cfg(feature = "node")]
205 fn rate_limit_delay(&self) -> Duration {
206 self.l1_rate_limit_delay.unwrap_or(self.l1_retry_delay)
207 }
208}
209
210#[cfg(feature = "node")]
211impl L1ClientMetrics {
212 fn new(metrics: &(impl Metrics + ?Sized), num_urls: usize) -> Self {
213 let failures = metrics.counter_family("failed_requests".into(), vec!["provider".into()]);
215
216 let mut failure_metrics = Vec::with_capacity(num_urls);
218 for url_index in 0..num_urls {
219 failure_metrics.push(failures.create(vec![url_index.to_string()]));
220 }
221
222 Self {
223 head: metrics.create_gauge("head".into(), None).into(),
224 finalized: metrics.create_gauge("finalized".into(), None).into(),
225 reconnects: metrics
226 .create_counter("stream_reconnects".into(), None)
227 .into(),
228 failovers: metrics.create_counter("failovers".into(), None).into(),
229 failures: Arc::new(failure_metrics),
230 }
231 }
232}
233
234#[cfg(feature = "node")]
235impl SwitchingTransport {
236 pub fn new(opt: L1ClientOptions, urls: Vec<Url>) -> anyhow::Result<Self> {
238 let Some(first_url) = urls.first().cloned() else {
240 return Err(anyhow::anyhow!("No valid URLs provided"));
241 };
242
243 let metrics = L1ClientMetrics::new(&**opt.metrics, urls.len());
245
246 let first_transport = Arc::new(RwLock::new(SingleTransport::new(&first_url, 0, None)));
248
249 Ok(Self {
250 urls: Arc::new(urls),
251 current_transport: first_transport,
252 opt: Arc::new(opt),
253 metrics,
254 switch_notify: Arc::new(Notify::new()),
255 })
256 }
257
258 async fn wait_switch(&self) {
260 self.switch_notify.notified().await;
261 }
262
263 fn options(&self) -> &L1ClientOptions {
264 &self.opt
265 }
266
267 fn metrics(&self) -> &L1ClientMetrics {
268 &self.metrics
269 }
270}
271
272#[cfg(feature = "node")]
273impl SingleTransportStatus {
274 fn log_success(&mut self) {
276 self.consecutive_failures = 0;
277 self.consecutive_rate_limits = 0;
278 }
279
280 fn log_rate_limit(&mut self, until: Instant) -> bool {
292 let outlived_backoff = self
293 .rate_limited_until
294 .is_none_or(|prev| Instant::now() >= prev);
295 self.rate_limited_until = Some(until);
296 if !outlived_backoff {
297 return false;
298 }
299
300 self.consecutive_rate_limits += 1;
301 if self.shutting_down || self.consecutive_rate_limits < MAX_CONSECUTIVE_RATE_LIMITS {
302 return false;
303 }
304 self.shutting_down = true;
305 true
306 }
307
308 fn log_failure(&mut self, opt: &L1ClientOptions) -> bool {
310 self.consecutive_failures += 1;
312
313 let should_switch = self.should_switch(opt);
315
316 self.last_failure = Some(Instant::now());
318
319 should_switch
321 }
322
323 fn should_switch(&mut self, opt: &L1ClientOptions) -> bool {
325 if self.shutting_down {
327 return false;
328 }
329
330 if self.consecutive_failures >= opt.l1_consecutive_failure_tolerance {
332 self.shutting_down = true;
333 return true;
334 }
335
336 let now = Instant::now();
338 if let Some(prev) = self.last_failure
339 && now.saturating_duration_since(prev) < opt.l1_frequent_failure_tolerance
340 {
341 self.shutting_down = true;
342 return true;
343 }
344
345 false
346 }
347
348 fn should_revert(&mut self, revert_at: Option<Instant>) -> bool {
350 if self.shutting_down {
351 return false;
353 }
354 let Some(revert_at) = revert_at else {
355 return false;
356 };
357 if Instant::now() >= revert_at {
358 self.shutting_down = true;
359 return true;
360 }
361
362 false
363 }
364}
365
366#[cfg(feature = "node")]
367impl SingleTransport {
368 fn new(url: &Url, generation: usize, revert_at: Option<Instant>) -> Self {
370 Self {
371 generation,
372 redacted_url: redact_url(url),
373 client: Http::new(url.clone()),
374 status: Default::default(),
375 revert_at,
376 }
377 }
378}
379
380#[cfg(feature = "node")]
392#[derive(Debug)]
393enum ResponseOutcome {
394 Healthy,
395 RateLimited {
398 retry_after: Option<Duration>,
399 },
400 RequestRejected,
404 Failed,
405}
406
407#[cfg(feature = "node")]
415const MAX_CONSECUTIVE_RATE_LIMITS: usize = 2;
416
417#[cfg(feature = "node")]
424const MAX_RATE_LIMIT_BACKOFF: Duration = Duration::from_secs(300);
425
426#[cfg(feature = "node")]
428fn rate_limit_backoff(retry_after: Option<Duration>, configured: Duration) -> Duration {
429 match retry_after {
430 Some(retry_after) => retry_after.min(MAX_RATE_LIMIT_BACKOFF),
431 None => configured,
432 }
433}
434
435#[cfg(feature = "node")]
452fn is_request_rejection(res: &ResponsePacket) -> bool {
453 res.as_error()
454 .is_some_and(|e| e.message.contains("query returned more than"))
455}
456
457#[cfg(feature = "node")]
462fn error_payload(
463 result: &StdResult<ResponsePacket, RpcError<TransportErrorKind>>,
464) -> Option<&ErrorPayload> {
465 result.as_ref().ok().and_then(ResponsePacket::as_error)
466}
467
468#[cfg(feature = "node")]
469impl ResponseOutcome {
470 fn classify(result: &StdResult<ResponsePacket, RpcError<TransportErrorKind>>) -> Self {
473 use ResponseOutcome::*;
474 match result {
475 Ok(res) if is_request_rejection(res) => RequestRejected,
478 Ok(res) if res.as_error().is_some_and(ErrorPayload::is_retry_err) => {
481 RateLimited { retry_after: None }
482 },
483 Ok(res) if res.is_error() => Failed,
484 Ok(_) => Healthy,
485 Err(RpcError::Transport(kind))
486 if kind
487 .as_http_error()
488 .is_some_and(HttpError::is_rate_limit_err) =>
489 {
490 RateLimited {
491 retry_after: kind.retry_after(),
492 }
493 },
494 Err(_) => Failed,
495 }
496 }
497}
498
499#[cfg(feature = "node")]
504#[async_trait]
505impl Service<RequestPacket> for SwitchingTransport {
506 type Error = RpcError<TransportErrorKind>;
507 type Response = ResponsePacket;
508 type Future =
509 Pin<Box<dyn Future<Output = Result<ResponsePacket, RpcError<TransportErrorKind>>> + Send>>;
510
511 fn poll_ready(
512 &mut self,
513 cx: &mut std::task::Context<'_>,
514 ) -> std::task::Poll<StdResult<(), Self::Error>> {
515 self.current_transport.read().clone().client.poll_ready(cx)
517 }
518
519 fn call(&mut self, req: RequestPacket) -> Self::Future {
520 let self_clone = self.clone();
522
523 Box::pin(async move {
525 let mut current_transport = self_clone.current_transport.read().clone();
527
528 let should_revert = current_transport
530 .status
531 .write()
532 .should_revert(current_transport.revert_at);
533 if should_revert {
534 let n = self_clone.urls.len();
536 let prev_primary_gen = (current_transport.generation / n) * n;
538 let next_gen = prev_primary_gen + n;
540 current_transport = self_clone.switch_to(next_gen, current_transport);
541 }
542
543 let rate_limit_until = current_transport.status.read().rate_limited_until;
545 if let Some(t) = rate_limit_until {
546 if t > Instant::now() {
547 return Err(RpcError::Transport(TransportErrorKind::Custom(
549 "Rate limit exceeded".into(),
550 )));
551 } else {
552 current_transport.status.write().rate_limited_until = None;
554 }
555 }
556
557 let result = current_transport.client.call(req).await;
558 let outcome = ResponseOutcome::classify(&result);
559
560 if matches!(outcome, ResponseOutcome::Healthy) {
561 current_transport.status.write().log_success();
562 return result;
563 }
564
565 if matches!(outcome, ResponseOutcome::RequestRejected) {
570 tracing::warn!(
571 url = %current_transport.redacted_url,
572 error = ?error_payload(&result),
573 "L1 rejected the request"
574 );
575 return result;
576 }
577
578 if let Some(f) = self_clone
580 .metrics
581 .failures
582 .get(current_transport.generation % self_clone.urls.len())
583 {
584 f.add(1);
585 }
586
587 let should_switch = if let ResponseOutcome::RateLimited { retry_after } = outcome {
590 let until = Instant::now()
593 + rate_limit_backoff(retry_after, self_clone.opt.rate_limit_delay());
594 let mut status = current_transport.status.write();
595 let should_switch = status.log_rate_limit(until);
596 if should_switch {
599 tracing::warn!(
600 url = %current_transport.redacted_url,
601 consecutive = status.consecutive_rate_limits,
602 "L1 still rate limiting after its own backoff, failing over"
603 );
604 } else {
605 tracing::debug!(
606 url = %current_transport.redacted_url,
607 consecutive = status.consecutive_rate_limits,
608 "L1 rate limited"
609 );
610 }
611 should_switch
612 } else {
613 tracing::warn!(
614 url = %current_transport.redacted_url,
615 ?outcome,
616 error = ?error_payload(&result),
617 transport_err = ?result.as_ref().err(),
618 "L1 client error"
619 );
620 current_transport
621 .status
622 .write()
623 .log_failure(&self_clone.opt)
624 };
625
626 if should_switch {
627 self_clone.metrics.failovers.add(1);
629 self_clone.switch_to(current_transport.generation + 1, current_transport);
630 }
631
632 result
633 })
634 }
635}
636
637#[cfg(feature = "node")]
638impl SwitchingTransport {
639 fn switch_to(&self, next_gen: usize, current_transport: SingleTransport) -> SingleTransport {
640 let next_index = next_gen % self.urls.len();
641 let url = self.urls[next_index].clone();
642 tracing::info!(url = %redact_url(&url), next_gen, "switch L1 transport");
643
644 let revert_at = if next_gen.is_multiple_of(self.urls.len()) {
645 None
647 } else if current_transport.generation.is_multiple_of(self.urls.len()) {
648 Some(Instant::now() + self.opt.l1_failover_revert)
651 } else {
652 current_transport.revert_at
654 };
655
656 let new_transport = SingleTransport::new(&url, next_gen, revert_at);
658
659 *self.current_transport.write() = new_transport.clone();
661
662 self.switch_notify.notify_waiters();
664
665 new_transport
666 }
667}
668
669#[cfg(feature = "node")]
671enum RefreshKind {
672 Head,
673 Finalized,
674}
675
676#[cfg(feature = "node")]
677impl L1Client {
678 fn with_transport(transport: SwitchingTransport) -> Self {
679 let rpc_client = RpcClient::new(transport.clone(), false);
681 let provider = ProviderBuilder::new().connect_client(rpc_client);
682
683 let opt = transport.options().clone();
684
685 let (sender, mut receiver) = async_broadcast::broadcast(opt.l1_events_channel_capacity);
686 receiver.set_await_active(false);
687 receiver.set_overflow(true);
688
689 Self {
690 provider,
691 transport,
692 state: Arc::new(Mutex::new(L1State::new(opt.l1_blocks_cache_size))),
693 sender,
694 receiver: receiver.deactivate(),
695 update_task: Default::default(),
696 }
697 }
698
699 pub fn new(url: Vec<Url>) -> anyhow::Result<Self> {
701 L1ClientOptions::default().connect(url)
702 }
703
704 pub fn anvil(anvil: &alloy::node_bindings::AnvilInstance) -> anyhow::Result<Self> {
706 L1ClientOptions {
707 l1_ws_provider: Some(vec![anvil.ws_endpoint().parse()?]),
708 ..Default::default()
709 }
710 .connect(vec![anvil.endpoint().parse()?])
711 }
712
713 pub async fn spawn_tasks(&self) {
715 let mut update_task = self.update_task.0.lock().await;
716 if update_task.is_none() {
717 *update_task = Some(spawn(self.update_loop()));
718 }
719 }
720
721 pub async fn shut_down_tasks(&self) {
726 let update_task = self.update_task.0.lock().await.take();
727 if let Some(update_task) = update_task {
728 update_task.abort();
729 }
730 }
731
732 fn update_loop(&self) -> impl Future<Output = ()> + use<> {
733 let opt = self.options().clone();
734 let rpc = self.provider.clone();
735 let ws_urls = opt.l1_ws_provider.clone();
736 let retry_delay = opt.l1_retry_delay;
737 let request_timeout = opt.l1_request_timeout;
738 let subscription_timeout = opt.subscription_timeout;
739 let state = self.state.clone();
740 let sender = self.sender.clone();
741 let metrics = self.metrics().clone();
742 let polling_interval = opt.l1_polling_interval;
743 let transport = self.transport.clone();
744
745 let span = tracing::warn_span!("L1 client update");
746
747 async move {
748
749 for i in 0..usize::MAX {
750 let ws;
751
752 let l1_head = loop {
755 match tokio::time::timeout(request_timeout, rpc.get_block(BlockId::latest()))
756 .await
757 {
758 Ok(Ok(Some(block))) => break block.header,
759 Ok(Ok(None)) => {
760 tracing::warn!("Failed to fetch L1 head block, will retry");
761 },
762 Ok(Err(err)) => {
763 tracing::warn!("Failed to fetch L1 head block, will retry: err {err}");
764 },
765 Err(_) => {
766 tracing::warn!(
767 "Failed to fetch L1 head block, will retry: timed out"
768 );
769 }
770 }
771 sleep(retry_delay).await;
772 };
773
774 let mut block_stream = {
776 let res = match &ws_urls {
777 Some(urls) => {
778 let provider = i % urls.len();
781 let url = &urls[provider];
782 ws = match ProviderBuilder::new()
783 .connect_ws(WsConnect::new(url.clone()))
784 .await
785 {
786 Ok(ws) => ws,
787 Err(err) => {
788 tracing::warn!(
789 provider,
790 "Failed to connect WebSockets provider: {err:#}"
791 );
792 sleep(retry_delay).await;
793 continue;
794 }
795 };
796 ws.subscribe_blocks().await.map(|stream| {stream::once(async { l1_head.clone() }).chain(stream.into_stream()).boxed()})
797 },
798 None => {
799 rpc
800 .watch_blocks()
801 .await
802 .map(|poller_builder| {
803 let stream = poller_builder.with_poll_interval(polling_interval).into_stream();
805
806 let rpc = rpc.clone();
807
808 stream::once(async { l1_head.clone() })
812 .chain(
813 stream.map(stream::iter).flatten().filter_map(move |hash| {
814 let rpc = rpc.clone();
815 async move {
816 match rpc.get_block(BlockId::hash(hash)).await {
817 Ok(Some(block)) => Some(block.header),
818 Ok(None) => {
821 tracing::warn!(%hash, "HTTP stream yielded a block hash that was not available");
822 None
823 }
824 Err(err) => {
825 tracing::warn!(
826 %hash,
827 "Error fetching block from HTTP stream: {err:#}"
828 );
829 None
830 }
831 }
832 }
833 }))
834 }.take_until(transport.wait_switch())
836 .boxed())
837 }
838 };
839 match res {
840 Ok(stream) => stream,
841 Err(err) => {
842 tracing::error!("Error subscribing to L1 blocks: {err:#}");
843 sleep(retry_delay).await;
844 continue;
845 }
846 }
847 };
848
849 tracing::info!("Established L1 block stream");
850 loop {
851 let block_timeout = tokio::time::timeout(subscription_timeout, block_stream.next()).await;
853 match block_timeout {
854 Ok(Some(head)) => {
856 let head = head.number;
857 tracing::debug!(head, "Received L1 block");
858
859 {
862 let mut state = state.lock().await;
863 apply_head(&mut state, head, &metrics, &sender).await;
864 }
865
866 match fetch_finalized_block_from_rpc(&rpc, request_timeout).await {
870 Ok(Some(finalized)) => {
871 let mut state = state.lock().await;
872 apply_finalized(&mut state, finalized, &metrics, &sender).await;
873 },
874 Ok(None) => {
875 tracing::warn!("no finalized block yet");
876 },
877 Err(err) => {
878 tracing::warn!("Error getting finalized block: {err:#}");
879 },
880 }
881 }
882 Ok(None) => {
884 tracing::error!("L1 block stream ended unexpectedly, trying to re-establish block stream");
885 break;
886 }
887 Err(_) => {
889 tracing::error!("No block received for {} seconds, trying to re-establish block stream", subscription_timeout.as_secs());
890 break;
891 }
892 }
893 }
894
895 metrics.reconnects.add(1);
896 }
897 }.instrument(span)
898 }
899
900 pub async fn snapshot(&self) -> L1Snapshot {
902 self.state.lock().await.snapshot
903 }
904
905 pub async fn wait_for_block(&self, number: u64) {
914 'outer: loop {
915 let mut events = self.receiver.activate_cloned();
918
919 {
921 let state = self.state.lock().await;
922 if state.snapshot.head >= number {
923 return;
924 }
925 tracing::info!(number, head = state.snapshot.head, "Waiting for l1 block");
926 }
927
928 let mut refresh = tokio::time::interval(self.options().l1_wait_refresh_interval);
932 refresh.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
933 refresh.tick().await; loop {
936 tokio::select! {
937 event = events.next() => {
938 match event {
939 Some(L1Event::NewHead { head }) => {
940 if head >= number {
941 tracing::info!(number, head, "Got L1 block");
942 return;
943 }
944 tracing::debug!(number, head, "Waiting for L1 block");
945 },
946 Some(_) => {},
947 None => {
948 tracing::warn!(number, "L1 event stream ended unexpectedly; retry");
951 self.retry_delay().await;
952 continue 'outer;
953 },
954 }
955 },
956 _ = refresh.tick() => {
957 self.refresh_head().await;
958 let head = self.state.lock().await.snapshot.head;
959 if head >= number {
960 tracing::info!(number, head, "Got L1 block");
961 return;
962 }
963 },
964 }
965 }
966 }
967 }
968
969 pub async fn wait_for_finalized_block(&self, number: u64) -> L1BlockInfo {
974 let (state, _) = self.lock_once_finalized_reaches(number).await;
975 self.fetch_finalized_block_by_number(state, number).await.1
976 }
977
978 pub async fn wait_for_latest_finalized_at_least(&self, number: u64) -> L1BlockInfo {
981 self.lock_once_finalized_reaches(number).await.1
982 }
983
984 async fn lock_once_finalized_reaches(
989 &self,
990 number: u64,
991 ) -> (MutexGuard<'_, L1State>, L1BlockInfo) {
992 'outer: loop {
993 let mut events = self.receiver.activate_cloned();
996
997 {
999 let state = self.state.lock().await;
1000 if let Some(finalized) = state.snapshot.finalized {
1001 if finalized.number >= number {
1002 return (state, finalized);
1003 }
1004 tracing::info!(
1005 number,
1006 finalized = ?state.snapshot.finalized,
1007 "waiting for l1 finalized block",
1008 );
1009 };
1010 }
1011
1012 let mut refresh = tokio::time::interval(self.options().l1_wait_refresh_interval);
1016 refresh.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
1017 refresh.tick().await; loop {
1020 tokio::select! {
1021 event = events.next() => {
1022 match event {
1023 Some(L1Event::NewFinalized { finalized }) => {
1024 let mut state = self.state.lock().await;
1025 state.put_finalized(finalized);
1026 if finalized.info.number >= number {
1027 tracing::info!(number, ?finalized, "got finalized L1 block");
1028 return (state, finalized.info);
1029 }
1030 tracing::debug!(number, ?finalized, "waiting for finalized L1 block");
1031 },
1032 Some(_) => {},
1033 None => {
1034 tracing::warn!(number, "L1 event stream ended unexpectedly; retry");
1037 self.retry_delay().await;
1038 continue 'outer;
1039 },
1040 }
1041 },
1042 _ = refresh.tick() => {
1043 self.refresh_finalized().await;
1044 let state = self.state.lock().await;
1045 if let Some(finalized) = state.snapshot.finalized
1046 && finalized.number >= number
1047 {
1048 tracing::info!(number, ?finalized, "got finalized L1 block");
1049 return (state, finalized);
1050 }
1051 },
1052 }
1053 }
1054 }
1055 }
1056
1057 pub async fn wait_for_finalized_block_with_timestamp(&self, timestamp: U256) -> L1BlockInfo {
1060 let (mut state, mut block) = 'outer: loop {
1062 let mut events = self.receiver.activate_cloned();
1065
1066 {
1068 let state = self.state.lock().await;
1069 if let Some(finalized) = state.snapshot.finalized
1070 && finalized.timestamp >= timestamp
1071 {
1072 break 'outer (state, finalized);
1073 }
1074 tracing::info!(
1075 %timestamp,
1076 finalized = ?state.snapshot.finalized,
1077 "waiting for L1 finalized block",
1078 );
1079 }
1080
1081 while let Some(event) = events.next().await {
1083 let L1Event::NewFinalized { finalized } = event else {
1084 continue;
1085 };
1086 if finalized.info.timestamp >= timestamp {
1087 tracing::info!(%timestamp, ?finalized, "got finalized block");
1088 break 'outer (self.state.lock().await, finalized.info);
1089 }
1090 tracing::debug!(%timestamp, ?finalized, "waiting for L1 finalized block");
1091 }
1092
1093 tracing::warn!(%timestamp, "L1 event stream ended unexpectedly; retry",);
1095 self.retry_delay().await;
1096 };
1097
1098 let mut upper_bound = block.number;
1107 let mut lower_bound = 0;
1108 while lower_bound < upper_bound {
1109 let midpoint = (upper_bound + lower_bound) / 2;
1110 tracing::debug!(
1111 lower_bound,
1112 midpoint,
1113 upper_bound,
1114 %timestamp,
1115 ?block,
1116 "searching for earliest block with sufficient timestamp"
1117 );
1118
1119 let (state_lock, midpoint_block) =
1120 self.fetch_finalized_block_by_number(state, midpoint).await;
1121 state = state_lock;
1122
1123 tracing::debug!(?midpoint_block, %timestamp, "pivot on midpoint block");
1124 if midpoint_block.timestamp < timestamp {
1125 lower_bound = midpoint + 1;
1126 } else {
1127 upper_bound = midpoint;
1128 block = midpoint_block;
1129 }
1130 }
1131
1132 block
1133 }
1134
1135 async fn refresh_head(&self) {
1139 if !self.throttle_refresh(RefreshKind::Head).await {
1140 return;
1141 }
1142 let timeout = self.options().l1_wait_refresh_timeout;
1143 match tokio::time::timeout(timeout, self.provider.get_block_number()).await {
1144 Ok(Ok(head)) => {
1145 let mut state = self.state.lock().await;
1146 apply_head(&mut state, head, self.metrics(), &self.sender).await;
1147 },
1148 Ok(Err(err)) => {
1149 tracing::debug!("Error refreshing L1 head from RPC: {err:#}");
1150 },
1151 Err(_) => {
1152 tracing::debug!("Timed out refreshing L1 head from RPC");
1153 },
1154 }
1155 }
1156
1157 async fn refresh_finalized(&self) {
1161 if !self.throttle_refresh(RefreshKind::Finalized).await {
1162 return;
1163 }
1164 let timeout = self.options().l1_wait_refresh_timeout;
1165 match fetch_finalized_block_from_rpc(&self.provider, timeout).await {
1166 Ok(Some(finalized)) => {
1167 let mut state = self.state.lock().await;
1168 apply_finalized(&mut state, finalized, self.metrics(), &self.sender).await;
1169 },
1170 Ok(None) => {
1171 tracing::debug!("no finalized block yet");
1172 },
1173 Err(err) => {
1174 tracing::debug!("Error refreshing L1 finalized block from RPC: {err:#}");
1175 },
1176 }
1177 }
1178
1179 async fn throttle_refresh(&self, kind: RefreshKind) -> bool {
1184 let interval = self.options().l1_wait_refresh_interval;
1185 let mut state = self.state.lock().await;
1186 let now = Instant::now();
1187 let last_refresh = match kind {
1188 RefreshKind::Head => &mut state.last_head_refresh,
1189 RefreshKind::Finalized => &mut state.last_finalized_refresh,
1190 };
1191 if last_refresh.is_some_and(|last| now.saturating_duration_since(last) < interval) {
1192 return false;
1193 }
1194 *last_refresh = Some(now);
1195 true
1196 }
1197
1198 async fn fetch_finalized_block_by_number<'a>(
1199 &'a self,
1200 mut state: MutexGuard<'a, L1State>,
1201 number: u64,
1202 ) -> (MutexGuard<'a, L1State>, L1BlockInfo) {
1203 let latest_finalized = state
1204 .snapshot
1205 .finalized
1206 .expect("get_finalized_block called before any blocks are finalized");
1207 assert!(
1208 number <= latest_finalized.number,
1209 "requesting a finalized block {number} that isn't finalized; snapshot: {:?}",
1210 state.snapshot,
1211 );
1212
1213 if let Some(safety_margin) = self.options().l1_finalized_safety_margin.blocks()
1214 && number < latest_finalized.number.saturating_sub(safety_margin.get())
1215 {
1216 tracing::debug!(
1222 number,
1223 ?latest_finalized,
1224 "skipping hash check for old finalized block"
1225 );
1226 let (state, block) = self
1227 .load_and_cache_finalized_block(state, number.into())
1228 .await;
1229 return (state, block.info);
1230 }
1231
1232 let mut successor_number = number;
1238 let mut successor = loop {
1239 if let Some(block) = state.finalized.get(&successor_number) {
1240 break *block;
1241 }
1242 successor_number += 1;
1243 if successor_number > latest_finalized.number {
1244 drop(state);
1248 let block = loop {
1249 match fetch_finalized_block_from_rpc(
1250 &self.provider,
1251 self.options().l1_request_timeout,
1252 )
1253 .await
1254 {
1255 Ok(Some(block)) => {
1256 break block;
1257 },
1258 Ok(None) => {
1259 tracing::warn!(
1260 "no finalized block even though finalized snapshot is Some; this \
1261 can be caused by an L1 client failover"
1262 );
1263 self.retry_delay().await;
1264 },
1265 Err(err) => {
1266 tracing::warn!("Error getting finalized block: {err:#}");
1267 self.retry_delay().await;
1268 },
1269 }
1270 };
1271 state = self.state.lock().await;
1272 state.put_finalized(block);
1273 break block;
1274 }
1275 };
1276
1277 while successor.info.number > number {
1280 tracing::debug!(
1281 number,
1282 ?successor,
1283 "checking hash chaining for finalized block"
1284 );
1285 (state, successor) = self
1286 .load_and_cache_finalized_block(state, successor.parent_hash.into())
1287 .await;
1288 }
1289
1290 (state, successor.info)
1291 }
1292
1293 async fn load_and_cache_finalized_block<'a>(
1294 &'a self,
1295 mut state: MutexGuard<'a, L1State>,
1296 id: BlockId,
1297 ) -> (MutexGuard<'a, L1State>, L1BlockInfoWithParent) {
1298 drop(state);
1300 let request_timeout = self.options().l1_request_timeout;
1301 let block = loop {
1302 let block =
1303 match tokio::time::timeout(request_timeout, self.provider.get_block(id)).await {
1304 Ok(Ok(Some(block))) => block,
1305 Ok(Ok(None)) => {
1306 tracing::warn!(
1307 %id,
1308 "provider error: finalized L1 block should always be available"
1309 );
1310 self.retry_delay().await;
1311 continue;
1312 },
1313 Ok(Err(err)) => {
1314 tracing::warn!(%id, "failed to get finalized L1 block: {err:#}");
1315 self.retry_delay().await;
1316 continue;
1317 },
1318 Err(_) => {
1319 tracing::warn!(%id, "timed out getting finalized L1 block");
1320 self.retry_delay().await;
1321 continue;
1322 },
1323 };
1324 break (&block).into();
1325 };
1326 state = self.state.lock().await;
1327 state.put_finalized(block);
1328 (state, block)
1329 }
1330
1331 pub async fn get_finalized_deposits(
1334 &self,
1335 fee_contract_address: Address,
1336 prev_finalized: Option<u64>,
1337 new_finalized: u64,
1338 ) -> Vec<FeeInfo> {
1339 if prev_finalized >= Some(new_finalized) {
1342 return vec![];
1343 }
1344
1345 let opt = self.options();
1346
1347 let prev = prev_finalized.map(|prev| prev + 1).unwrap_or(0);
1350
1351 let mut start = prev;
1354 let end = new_finalized;
1355 let chunk_size = opt.l1_events_max_block_range;
1356 let chunks = std::iter::from_fn(move || {
1357 let chunk_end = min(start + chunk_size - 1, end);
1358 if chunk_end < start {
1359 return None;
1360 }
1361
1362 let chunk = (start, chunk_end);
1363 start = chunk_end + 1;
1364 Some(chunk)
1365 });
1366
1367 let events = stream::iter(chunks).then(|(from, to)| {
1369 let retry_delay = opt.l1_retry_delay;
1370 let fee_contract = FeeContract::new(fee_contract_address, self.provider.clone());
1371 async move {
1372 tracing::debug!(from, to, "fetch events in range");
1373
1374 loop {
1376 match fee_contract
1377 .Deposit_filter()
1378 .address(*fee_contract.address())
1379 .from_block(from)
1380 .to_block(to)
1381 .query()
1382 .await
1383 {
1384 Ok(events) => break stream::iter(events),
1385 Err(err) => {
1386 tracing::warn!(from, to, %err, "Fee L1Event Error");
1387 sleep(retry_delay).await;
1388 },
1389 }
1390 }
1391 }
1392 });
1393 events
1394 .flatten()
1395 .map(|(deposit, _)| FeeInfo::from(deposit))
1396 .collect()
1397 .await
1398 }
1399
1400 pub async fn is_proxy_contract(&self, proxy_address: Address) -> anyhow::Result<bool> {
1402 let hex_bytes =
1405 hex::decode("360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc")
1406 .expect("Failed to decode hex string");
1407 let implementation_slot = B256::from_slice(&hex_bytes);
1408 let storage = self
1409 .provider
1410 .get_storage_at(proxy_address, implementation_slot.into())
1411 .await?;
1412
1413 let implementation_address = Address::from_slice(&storage.to_be_bytes::<32>()[12..]);
1414
1415 Ok(implementation_address != Address::ZERO)
1417 }
1418
1419 pub async fn retry_on_all_providers<Fut>(
1420 &self,
1421 op: impl Fn() -> Fut,
1422 ) -> Result<Fut::Ok, Fut::Error>
1423 where
1424 Fut: TryFuture,
1425 {
1426 let transport = &self.transport;
1427 let start = transport.current_transport.read().generation % transport.urls.len();
1428 let end = start + transport.urls.len();
1429 loop {
1430 match TryFutureExt::into_future(op()).await {
1431 Ok(res) => return Ok(res),
1432 Err(err) => {
1433 if transport.current_transport.read().generation >= end {
1434 return Err(err);
1435 } else {
1436 self.retry_delay().await;
1437 }
1438 },
1439 }
1440 }
1441 }
1442
1443 pub(crate) fn options(&self) -> &L1ClientOptions {
1444 self.transport.options()
1445 }
1446
1447 fn metrics(&self) -> &L1ClientMetrics {
1448 self.transport.metrics()
1449 }
1450
1451 async fn retry_delay(&self) {
1452 sleep(self.options().l1_retry_delay).await;
1453 }
1454}
1455
1456#[cfg(feature = "node")]
1457impl L1State {
1458 fn new(cache_size: NonZeroUsize) -> Self {
1459 Self {
1460 snapshot: Default::default(),
1461 finalized: LruCache::new(cache_size),
1462 last_finalized: None,
1463 last_head_refresh: None,
1464 last_finalized_refresh: None,
1465 }
1466 }
1467
1468 fn put_finalized(&mut self, block: L1BlockInfoWithParent) {
1469 assert!(
1470 self.snapshot.finalized.is_some()
1471 && block.info.number <= self.snapshot.finalized.unwrap().number,
1472 "inserting a finalized block {block:?} that isn't finalized; snapshot: {:?}",
1473 self.snapshot,
1474 );
1475
1476 if Some(block.info.number()) > self.last_finalized {
1477 self.last_finalized = Some(block.info.number());
1478 }
1479
1480 if let Some((old_number, old_block)) = self.finalized.push(block.info.number, block)
1481 && old_number == block.info.number
1482 && block != old_block
1483 {
1484 tracing::error!(
1485 ?old_block,
1486 ?block,
1487 "got different info for the same finalized height; something has gone very wrong \
1488 with the L1",
1489 );
1490 }
1491 }
1492}
1493
1494#[cfg(feature = "node")]
1497async fn apply_head(
1498 state: &mut L1State,
1499 head: u64,
1500 metrics: &L1ClientMetrics,
1501 sender: &Sender<L1Event>,
1502) {
1503 if head <= state.snapshot.head {
1504 return;
1505 }
1506 tracing::debug!(head, old_head = state.snapshot.head, "L1 head updated");
1507 metrics.head.set(head as usize);
1508 state.snapshot.head = head;
1509 sender
1512 .broadcast_direct(L1Event::NewHead { head })
1513 .await
1514 .ok();
1515}
1516
1517#[cfg(feature = "node")]
1523async fn apply_finalized(
1524 state: &mut L1State,
1525 finalized: L1BlockInfoWithParent,
1526 metrics: &L1ClientMetrics,
1527 sender: &Sender<L1Event>,
1528) {
1529 if Some(finalized.info) <= state.snapshot.finalized {
1530 return;
1531 }
1532 tracing::info!(
1533 ?finalized,
1534 old_finalized = ?state.snapshot.finalized,
1535 "L1 finalized updated",
1536 );
1537 if state.snapshot.head < finalized.info.number {
1538 apply_head(state, finalized.info.number, metrics, sender).await;
1539 }
1540 metrics.finalized.set(finalized.info.number as usize);
1541 state.snapshot.finalized = Some(finalized.info);
1542 state.put_finalized(finalized);
1543 sender
1544 .broadcast_direct(L1Event::NewFinalized { finalized })
1545 .await
1546 .ok();
1547}
1548
1549#[cfg(feature = "node")]
1551async fn fetch_finalized_block_from_rpc(
1552 rpc: &impl Provider,
1553 timeout: Duration,
1554) -> anyhow::Result<Option<L1BlockInfoWithParent>> {
1555 let Some(block) = tokio::time::timeout(timeout, rpc.get_block(BlockId::finalized()))
1556 .await
1557 .context("L1 request timed out")??
1558 else {
1559 return Ok(None);
1564 };
1565
1566 Ok(Some((&block).into()))
1567}
1568
1569#[cfg(test)]
1570mod test {
1571 use std::{
1572 borrow::Cow,
1573 ops::Add,
1574 sync::atomic::{AtomicUsize, Ordering},
1575 time::Duration,
1576 };
1577
1578 use alloy::{
1579 eips::BlockNumberOrTag,
1580 node_bindings::{Anvil, AnvilInstance},
1581 primitives::utils::parse_ether,
1582 providers::{ext::AnvilApi, layers::AnvilProvider},
1583 };
1584 use espresso_contract_deployer::{Contracts, deploy_fee_contract_proxy};
1585 use hotshot_types::traits::metrics::NoMetrics;
1586 use serde_json::{Value, json};
1587 use time::OffsetDateTime;
1588 use warp::Filter;
1589
1590 use super::*;
1591
1592 mod fixtures {
1597 pub const ALCHEMY_APP_INACTIVE: &str = r#"{"jsonrpc":"2.0","id":399193,"error":{"code":-32600,"message":"App is inactive. Please create a new app or contact support at https://dashboard.alchemy.com/***"}}"#;
1598 pub const ALCHEMY_10_BLOCK_RANGE: &str = r#"{"jsonrpc":"2.0","id":1005,"error":{"code":-32600,"message":"Under the Free tier plan, you can make eth_getLogs requests with up to a 10 block range. Based on your parameters, this block range should work: [0x1735fc9, 0x1735fd2]. Upgrade to PAYG for expanded block range."}}"#;
1599 pub const BLOCK_RANGE_TOO_LARGE: &str = r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32062,"message":"Block range is too large"}}"#;
1600 pub const ALCHEMY_RATE_LIMIT: &str = r#"{"jsonrpc":"2.0","id":1,"error":{"code":429,"message":"Your app has exceeded its concurrent requests capacity. If you have retries enabled, you can safely ignore this message. If not, check out https://docs.alchemy.com/reference/throughput. Reach out to us if you'd like to increase your limits: https://dashboard.alchemy.com/support"}}"#;
1604 pub const INFURA_RATE_LIMIT: &str = r#"{"code":-32005,"message":"Too Many Requests","data":{"see":"https://infura.io/dashboard"}}"#;
1607 pub const INFURA_RATE_EXCEEDED: &str = r#"{"jsonrpc":"2.0","id":3419,"error":{"code":-32005,"message":"project ID request rate exceeded","data":{"rate":{"allowed_rps":50,"backoff_seconds":0,"current_rps":52.3},"see":"https://infura.io/docs/ethereum/json-rpc/ratelimits"}}}"#;
1611 pub const INFURA_DAILY_QUOTA: &str = r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32005,"message":"daily request count exceeded, request rate limited","data":{"see":"https://infura.io/dashboard"}}}"#;
1615 pub const INFURA_HEADER_NOT_FOUND: &str =
1619 r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"header not found"}}"#;
1620 pub const INFURA_TOO_MANY_RESULTS: &str = r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32005,"message":"query returned more than 10000 results. Try with this block range [0x1500000, 0x15000FA].","data":{"from":"0x1500000","limit":10000,"to":"0x15000FA"}}}"#;
1621 pub const INFURA_UNAVAILABLE: &str = r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32603,"message":"service temporarily unavailable"}}"#;
1622 pub const SUCCESS: &str = r#"{"jsonrpc":"2.0","id":1,"result":"0x1"}"#;
1623 }
1624
1625 fn ok_packet(body: &str) -> StdResult<ResponsePacket, RpcError<TransportErrorKind>> {
1626 Ok(serde_json::from_str(body).expect("valid JSON-RPC response fixture"))
1627 }
1628
1629 #[test]
1630 fn test_response_outcome_healthy_on_success() {
1631 assert!(matches!(
1632 ResponseOutcome::classify(&ok_packet(fixtures::SUCCESS)),
1633 ResponseOutcome::Healthy
1634 ));
1635 }
1636
1637 #[test]
1638 fn test_response_outcome_alchemy_app_inactive_is_failed() {
1639 assert!(matches!(
1640 ResponseOutcome::classify(&ok_packet(fixtures::ALCHEMY_APP_INACTIVE)),
1641 ResponseOutcome::Failed
1642 ));
1643 }
1644
1645 #[test]
1648 fn test_response_outcome_alchemy_10_block_range_is_failed() {
1649 assert!(matches!(
1650 ResponseOutcome::classify(&ok_packet(fixtures::ALCHEMY_10_BLOCK_RANGE)),
1651 ResponseOutcome::Failed
1652 ));
1653 }
1654
1655 #[test]
1656 fn test_response_outcome_block_range_too_large_is_failed() {
1657 assert!(matches!(
1658 ResponseOutcome::classify(&ok_packet(fixtures::BLOCK_RANGE_TOO_LARGE)),
1659 ResponseOutcome::Failed
1660 ));
1661 }
1662
1663 #[test]
1664 fn test_response_outcome_alchemy_rate_limit_body_is_rate_limited() {
1665 assert!(matches!(
1666 ResponseOutcome::classify(&ok_packet(fixtures::ALCHEMY_RATE_LIMIT)),
1667 ResponseOutcome::RateLimited { retry_after: None }
1668 ));
1669 }
1670
1671 #[test]
1675 fn test_response_outcome_infura_daily_quota_is_rate_limited() {
1676 assert!(matches!(
1677 ResponseOutcome::classify(&ok_packet(fixtures::INFURA_DAILY_QUOTA)),
1678 ResponseOutcome::RateLimited { retry_after: None }
1679 ));
1680 }
1681
1682 #[test]
1686 fn test_response_outcome_infura_header_not_found_is_rate_limited() {
1687 assert!(matches!(
1688 ResponseOutcome::classify(&ok_packet(fixtures::INFURA_HEADER_NOT_FOUND)),
1689 ResponseOutcome::RateLimited { retry_after: None }
1690 ));
1691 }
1692
1693 #[test]
1696 fn test_response_outcome_infura_rate_exceeded_is_rate_limited() {
1697 assert!(matches!(
1698 ResponseOutcome::classify(&ok_packet(fixtures::INFURA_RATE_EXCEEDED)),
1699 ResponseOutcome::RateLimited { retry_after: None }
1700 ));
1701 }
1702
1703 #[test]
1704 fn test_infura_rate_limit_body_is_not_a_json_rpc_response() {
1705 serde_json::from_str::<ResponsePacket>(fixtures::INFURA_RATE_LIMIT).unwrap_err();
1706 }
1707
1708 #[test]
1711 fn test_response_outcome_infura_too_many_results_is_request_rejected() {
1712 assert!(matches!(
1713 ResponseOutcome::classify(&ok_packet(fixtures::INFURA_TOO_MANY_RESULTS)),
1714 ResponseOutcome::RequestRejected
1715 ));
1716 }
1717
1718 #[test]
1719 fn test_response_outcome_infura_unavailable_is_failed() {
1720 assert!(matches!(
1721 ResponseOutcome::classify(&ok_packet(fixtures::INFURA_UNAVAILABLE)),
1722 ResponseOutcome::Failed
1723 ));
1724 }
1725
1726 #[test]
1729 fn test_response_outcome_error_resp_is_failed() {
1730 let result: StdResult<ResponsePacket, RpcError<TransportErrorKind>> =
1731 Err(RpcError::ErrorResp(alloy::rpc::json_rpc::ErrorPayload {
1732 code: 429,
1733 message: "Too Many Requests".into(),
1734 data: None,
1735 }));
1736 assert!(matches!(
1737 ResponseOutcome::classify(&result),
1738 ResponseOutcome::Failed
1739 ));
1740 }
1741
1742 #[test]
1743 fn test_response_outcome_http_error_429_is_rate_limited() {
1744 let result: StdResult<ResponsePacket, RpcError<TransportErrorKind>> =
1745 Err(RpcError::Transport(TransportErrorKind::HttpError(
1746 alloy::transports::HttpError {
1747 status: 429,
1748 body: fixtures::INFURA_RATE_LIMIT.to_owned(),
1749 },
1750 )));
1751 assert!(matches!(
1752 ResponseOutcome::classify(&result),
1753 ResponseOutcome::RateLimited { retry_after: None }
1754 ));
1755 }
1756
1757 #[test]
1758 fn test_response_outcome_http_error_with_retry_after_is_rate_limited() {
1759 let result: StdResult<ResponsePacket, RpcError<TransportErrorKind>> = Err(
1760 RpcError::Transport(TransportErrorKind::HttpErrorWithRetryAfter {
1761 error: alloy::transports::HttpError {
1762 status: 429,
1763 body: fixtures::INFURA_RATE_LIMIT.to_owned(),
1764 },
1765 retry_after: Duration::from_secs(52),
1766 }),
1767 );
1768 assert!(matches!(
1769 ResponseOutcome::classify(&result),
1770 ResponseOutcome::RateLimited { retry_after: Some(d) } if d == Duration::from_secs(52)
1771 ));
1772 }
1773
1774 #[test]
1775 fn test_rate_limit_backoff_honors_server_value_under_cap() {
1776 assert_eq!(
1777 rate_limit_backoff(Some(Duration::from_secs(52)), Duration::from_secs(1)),
1778 Duration::from_secs(52)
1779 );
1780 }
1781
1782 #[test]
1783 fn test_rate_limit_backoff_clamps_server_value_over_cap() {
1784 assert_eq!(
1785 rate_limit_backoff(Some(Duration::from_secs(86400)), Duration::from_secs(1)),
1786 MAX_RATE_LIMIT_BACKOFF
1787 );
1788 }
1789
1790 #[test]
1792 fn test_rate_limit_backoff_without_server_value_uses_configured_delay() {
1793 let configured = MAX_RATE_LIMIT_BACKOFF + Duration::from_secs(1);
1794 assert_eq!(rate_limit_backoff(None, configured), configured);
1795 }
1796
1797 #[test]
1798 fn test_response_outcome_http_error_403_is_failed() {
1799 let result: StdResult<ResponsePacket, RpcError<TransportErrorKind>> =
1800 Err(RpcError::Transport(TransportErrorKind::HttpError(
1801 alloy::transports::HttpError {
1802 status: 403,
1803 body: String::new(),
1804 },
1805 )));
1806 assert!(matches!(
1807 ResponseOutcome::classify(&result),
1808 ResponseOutcome::Failed
1809 ));
1810 }
1811
1812 #[test]
1813 fn test_l1_client_options_default_safety_margin() {
1814 assert_eq!(
1815 L1ClientOptions::default()
1816 .l1_finalized_safety_margin
1817 .blocks(),
1818 NonZeroU64::new(100)
1819 );
1820 }
1821
1822 #[test]
1823 fn test_l1_safety_margin_from_str() {
1824 assert_eq!(
1825 "100".parse::<L1SafetyMargin>().unwrap().blocks(),
1826 NonZeroU64::new(100)
1827 );
1828 assert_eq!(
1829 "unlimited".parse::<L1SafetyMargin>().unwrap().blocks(),
1830 None
1831 );
1832 assert_eq!(
1833 " Unlimited ".parse::<L1SafetyMargin>().unwrap().blocks(),
1834 None
1835 );
1836 "garbage".parse::<L1SafetyMargin>().unwrap_err();
1837 "0".parse::<L1SafetyMargin>().unwrap_err();
1838 }
1839
1840 #[test]
1841 fn test_l1_safety_margin_display_round_trip() {
1842 for margin in [
1843 L1SafetyMargin::from(NonZeroU64::new(100).unwrap()),
1844 L1SafetyMargin::UNLIMITED,
1845 ] {
1846 assert_eq!(
1847 margin.to_string().parse::<L1SafetyMargin>().unwrap(),
1848 margin
1849 );
1850 }
1851 }
1852
1853 #[test]
1854 fn test_switching_transport_debug_hides_credentials() {
1855 let opt = L1ClientOptions {
1856 l1_ws_provider: Some(vec!["wss://u:p@ws.invalid/v2/WS_SECRET".parse().unwrap()]),
1857 ..Default::default()
1858 };
1859 let transport = SwitchingTransport::new(
1860 opt,
1861 vec!["https://u:p@rpc.invalid/v2/HTTP_SECRET".parse().unwrap()],
1862 )
1863 .expect("switching transport constructs");
1864
1865 let debug = format!("{transport:?}");
1866 assert!(!debug.contains("WS_SECRET"), "{debug}");
1867 assert!(!debug.contains("HTTP_SECRET"), "{debug}");
1868 assert!(!debug.contains("u:p"), "{debug}");
1869 assert!(debug.contains("rpc.invalid"), "{debug}");
1870 }
1871
1872 #[test]
1873 fn test_node_state_debug_hides_credentials() {
1874 let l1 = L1Client::new(vec![
1875 "https://u:p@rpc.invalid/v2/HTTP_SECRET".parse().unwrap(),
1876 ])
1877 .expect("L1 client constructs");
1878 let node_state = crate::NodeState::mock().with_l1(l1);
1879
1880 let debug = format!("{node_state:?}");
1881 assert!(!debug.contains("HTTP_SECRET"), "{debug}");
1882 assert!(!debug.contains("u:p"), "{debug}");
1883 }
1884
1885 async fn new_l1_client_opt(
1886 anvil: &Arc<AnvilInstance>,
1887 f: impl FnOnce(&mut L1ClientOptions),
1888 ) -> L1Client {
1889 let mut opt = L1ClientOptions {
1890 l1_events_max_block_range: 1,
1891 l1_polling_interval: Duration::from_secs(1),
1892 subscription_timeout: Duration::from_secs(5),
1893 ..Default::default()
1894 };
1895 f(&mut opt);
1896
1897 let l1_client = opt
1898 .connect(vec![anvil.endpoint_url()])
1899 .expect("Failed to create L1 client");
1900
1901 l1_client.spawn_tasks().await;
1902 l1_client
1903 }
1904
1905 async fn new_l1_client(anvil: &Arc<AnvilInstance>, include_ws: bool) -> L1Client {
1906 new_l1_client_opt(anvil, |opt| {
1907 if include_ws {
1908 opt.l1_ws_provider = Some(vec![anvil.ws_endpoint_url()]);
1909 }
1910 })
1911 .await
1912 }
1913
1914 #[test_log::test(tokio::test(flavor = "multi_thread"))]
1915 async fn test_get_finalized_deposits() -> anyhow::Result<()> {
1916 let num_deposits = 5;
1917
1918 let anvil = Anvil::new().spawn();
1919 let wallet = anvil.wallet().unwrap();
1920 let deployer = wallet.default_signer().address();
1921 let inner_provider = ProviderBuilder::new()
1922 .wallet(wallet)
1923 .connect_http(anvil.endpoint_url());
1924 let provider = AnvilProvider::new(inner_provider, Arc::new(anvil));
1926 let mut contracts = Contracts::new();
1928
1929 let l1_client = new_l1_client(provider.anvil(), false).await;
1931
1932 let fee_proxy_addr = deploy_fee_contract_proxy(&provider, &mut contracts, deployer).await?;
1934 let fee_proxy = FeeContract::new(fee_proxy_addr, &provider);
1935 let num_tx_for_deploy = provider.get_block_number().await?;
1936
1937 for n in 1..=num_deposits {
1939 let amount = n as f32 / 10.0;
1941 let receipt = fee_proxy
1942 .deposit(deployer)
1943 .value(parse_ether(&amount.to_string())?)
1944 .send()
1945 .await?
1946 .get_receipt()
1947 .await?;
1948 assert!(receipt.inner.is_success());
1949 }
1950
1951 let cur_height = provider.get_block_number().await?;
1952 assert_eq!(num_deposits + num_tx_for_deploy, cur_height);
1953
1954 let pending = l1_client
1958 .get_finalized_deposits(fee_proxy_addr, None, cur_height)
1959 .await;
1960
1961 assert_eq!(num_deposits as usize, pending.len(), "{pending:?}");
1962 assert_eq!(deployer, pending[0].account().0);
1963 assert_eq!(
1964 U256::from(1500000000000000000u64),
1965 pending
1966 .iter()
1967 .fold(U256::from(0), |total, info| total.add(info.amount().0))
1968 );
1969
1970 let pending = l1_client
1972 .get_finalized_deposits(fee_proxy_addr, Some(0), cur_height)
1973 .await;
1974 assert_eq!(num_deposits as usize, pending.len());
1975
1976 let pending = l1_client
1977 .get_finalized_deposits(fee_proxy_addr, Some(0), 0)
1978 .await;
1979 assert_eq!(0, pending.len());
1980
1981 let pending = l1_client
1982 .get_finalized_deposits(fee_proxy_addr, Some(0), 1)
1983 .await;
1984 assert_eq!(0, pending.len());
1985
1986 let pending = l1_client
1987 .get_finalized_deposits(fee_proxy_addr, Some(num_tx_for_deploy), num_tx_for_deploy)
1988 .await;
1989 assert_eq!(0, pending.len());
1990
1991 let pending = l1_client
1992 .get_finalized_deposits(
1993 fee_proxy_addr,
1994 Some(num_tx_for_deploy),
1995 num_tx_for_deploy + 1,
1996 )
1997 .await;
1998 assert_eq!(1, pending.len());
1999
2000 let pending = l1_client
2002 .get_finalized_deposits(fee_proxy_addr, Some(num_tx_for_deploy), 0)
2003 .await;
2004 assert_eq!(0, pending.len());
2005
2006 Ok(())
2007 }
2008
2009 async fn test_wait_for_finalized_block_helper(ws: bool) {
2010 let anvil = Arc::new(Anvil::new().block_time_f64(0.1).spawn());
2011 let l1_client = new_l1_client(&anvil, ws).await;
2012 let provider = &l1_client.provider;
2013
2014 let block_height = provider.get_block_number().await.unwrap();
2016 let block = l1_client.wait_for_finalized_block(block_height + 10).await;
2017 assert_eq!(block.number, block_height + 10);
2018
2019 let true_block = provider
2021 .get_block(BlockId::Number(BlockNumberOrTag::Number(block_height + 10)))
2022 .full()
2023 .await
2024 .unwrap()
2025 .unwrap();
2026
2027 assert_eq!(
2028 block.timestamp.to::<u64>(),
2029 true_block.header.inner.timestamp
2030 );
2031 assert_eq!(block.hash, true_block.header.hash);
2032 }
2033
2034 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2035 async fn test_wait_for_finalized_block_ws() {
2036 test_wait_for_finalized_block_helper(true).await
2037 }
2038
2039 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2040 async fn test_wait_for_finalized_block_http() {
2041 test_wait_for_finalized_block_helper(false).await
2042 }
2043
2044 async fn test_wait_for_old_finalized_block_helper(ws: bool) {
2045 let anvil = Arc::new(Anvil::new().block_time_f64(0.2).spawn());
2046 let l1_client = new_l1_client_opt(&anvil, |opt| {
2047 if ws {
2048 opt.l1_ws_provider = Some(vec![anvil.ws_endpoint_url()]);
2049 }
2050 opt.l1_finalized_safety_margin = NonZeroU64::new(1).unwrap().into();
2051 })
2052 .await;
2053 let provider = &l1_client.provider;
2054
2055 l1_client.wait_for_finalized_block(2).await;
2057
2058 let block = l1_client.wait_for_finalized_block(0).await;
2060
2061 let true_block = provider.get_block(0.into()).await.unwrap().unwrap();
2063 assert_eq!(block.hash, true_block.header.hash);
2064 }
2065
2066 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2067 async fn test_wait_for_old_finalized_block_ws() {
2068 test_wait_for_old_finalized_block_helper(true).await
2069 }
2070
2071 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2072 async fn test_wait_for_old_finalized_block_http() {
2073 test_wait_for_old_finalized_block_helper(false).await
2074 }
2075
2076 async fn test_wait_for_finalized_block_by_timestamp_helper(ws: bool) {
2077 let anvil = Arc::new(Anvil::new().block_time_f64(0.2).spawn());
2078 let l1_client = new_l1_client(&anvil, ws).await;
2079 let provider = &l1_client.provider;
2080
2081 let timestamp = U256::from(OffsetDateTime::now_utc().unix_timestamp() as u64 + 5);
2083 let block = l1_client
2084 .wait_for_finalized_block_with_timestamp(timestamp)
2085 .await;
2086 assert!(
2087 block.timestamp >= timestamp,
2088 "wait_for_finalized_block_with_timestamp({timestamp}) returned too early a block: \
2089 {block:?}",
2090 );
2091 let parent = provider
2092 .get_block(BlockId::Number(BlockNumberOrTag::Number(block.number - 1)))
2093 .full()
2094 .await
2095 .unwrap()
2096 .unwrap();
2097 assert!(
2098 parent.header.inner.timestamp < timestamp.to::<u64>(),
2099 "wait_for_finalized_block_with_timestamp({timestamp}) did not return the earliest \
2100 possible block: returned {block:?}, but earlier block {parent:?} has an acceptable \
2101 timestamp too",
2102 );
2103
2104 let true_block = provider
2106 .get_block(BlockId::Number(BlockNumberOrTag::Number(block.number)))
2107 .await
2108 .unwrap()
2109 .unwrap();
2110 assert_eq!(
2111 block.timestamp.to::<u64>(),
2112 true_block.header.inner.timestamp
2113 );
2114 assert_eq!(block.hash, true_block.header.hash);
2115 }
2116
2117 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2118 async fn test_wait_for_finalized_block_by_timestamp_ws() {
2119 test_wait_for_finalized_block_by_timestamp_helper(true).await
2120 }
2121
2122 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2123 async fn test_wait_for_finalized_block_by_timestamp_http() {
2124 test_wait_for_finalized_block_by_timestamp_helper(false).await
2125 }
2126
2127 async fn test_wait_for_old_finalized_block_by_timestamp_helper(ws: bool) {
2128 let anvil = Arc::new(Anvil::new().block_time_f64(0.2).spawn());
2129 let l1_client = new_l1_client(&anvil, ws).await;
2130
2131 let true_block = l1_client.wait_for_finalized_block(0).await;
2133 let timestamp = true_block.timestamp;
2134
2135 l1_client.wait_for_finalized_block(10).await;
2137
2138 let block = l1_client
2140 .wait_for_finalized_block_with_timestamp(U256::from(timestamp))
2141 .await;
2142 assert_eq!(block, true_block);
2143 }
2144
2145 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2146 async fn test_wait_for_old_finalized_block_by_timestamp_ws() {
2147 test_wait_for_old_finalized_block_by_timestamp_helper(true).await
2148 }
2149
2150 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2151 async fn test_wait_for_old_finalized_block_by_timestamp_http() {
2152 test_wait_for_old_finalized_block_by_timestamp_helper(false).await
2153 }
2154
2155 async fn test_wait_for_block_helper(ws: bool) {
2156 let anvil = Arc::new(Anvil::new().block_time_f64(0.1).spawn());
2157 let l1_client = new_l1_client(&anvil, ws).await;
2158 let provider = &l1_client.provider;
2159
2160 let block_height = provider.get_block_number().await.unwrap();
2162 l1_client.wait_for_block(block_height + 10).await;
2163
2164 let new_block_height = provider.get_block_number().await.unwrap();
2165 assert!(
2166 new_block_height >= block_height + 10,
2167 "wait_for_block returned too early; initial height = {block_height}, new height = \
2168 {new_block_height}",
2169 );
2170 }
2171
2172 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2173 async fn test_wait_for_block_ws() {
2174 test_wait_for_block_helper(true).await
2175 }
2176
2177 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2178 async fn test_wait_for_block_http() {
2179 test_wait_for_block_helper(false).await
2180 }
2181
2182 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2183 async fn test_wait_for_block_refreshes_without_poller() {
2184 let anvil = Anvil::new().arg("--no-mining").spawn();
2185 let l1_client = L1ClientOptions::default()
2186 .connect(vec![anvil.endpoint_url()])
2187 .expect("Failed to create L1 client");
2188
2189 let head = l1_client.get_block_number().await.unwrap();
2190 l1_client.anvil_mine(Some(1), None).await.unwrap();
2191
2192 tokio::time::timeout(Duration::from_secs(3), l1_client.wait_for_block(head + 1))
2193 .await
2194 .expect("wait_for_block did not refresh from the RPC without the poller");
2195 }
2196
2197 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2198 async fn test_wait_for_finalized_block_refreshes_without_poller() {
2199 let anvil = Anvil::new()
2200 .args(["--no-mining", "--slots-in-an-epoch", "1"])
2201 .spawn();
2202 let l1_client = L1ClientOptions::default()
2203 .connect(vec![anvil.endpoint_url()])
2204 .expect("Failed to create L1 client");
2205
2206 l1_client.anvil_mine(Some(5), None).await.unwrap();
2208 let new_finalized = fetch_finalized_block_from_rpc(
2209 &l1_client.provider,
2210 l1_client.options().l1_request_timeout,
2211 )
2212 .await
2213 .unwrap()
2214 .expect("anvil finalized a block after mining");
2215
2216 let block = tokio::time::timeout(
2217 Duration::from_secs(3),
2218 l1_client.wait_for_finalized_block(new_finalized.info.number),
2219 )
2220 .await
2221 .expect("wait_for_finalized_block did not refresh from the RPC without the poller");
2222 assert_eq!(block.hash, new_finalized.info.hash);
2223 }
2224
2225 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2228 async fn test_wait_for_latest_finalized_at_least_skips_pruned_block() {
2229 let anvil = Anvil::new()
2230 .args(["--no-mining", "--slots-in-an-epoch", "1"])
2231 .spawn();
2232 let by_number_requests = Arc::new(AtomicUsize::new(0));
2233 let proxy_url =
2234 serve_pruned_history_proxy(anvil.endpoint_url(), by_number_requests.clone()).await;
2235 let l1_client = L1ClientOptions {
2238 l1_finalized_safety_margin: NonZeroU64::new(1).unwrap().into(),
2239 l1_wait_refresh_interval: Duration::from_millis(50),
2240 ..Default::default()
2241 }
2242 .connect(vec![proxy_url])
2243 .expect("Failed to create L1 client");
2244
2245 l1_client.anvil_mine(Some(5), None).await.unwrap();
2246 let new_finalized = fetch_finalized_block_from_rpc(
2247 &l1_client.provider,
2248 l1_client.options().l1_request_timeout,
2249 )
2250 .await
2251 .unwrap()
2252 .expect("anvil finalized a block after mining");
2253 let pruned_number = new_finalized.info.number - 2;
2254
2255 let block = tokio::time::timeout(
2256 Duration::from_secs(3),
2257 l1_client.wait_for_latest_finalized_at_least(pruned_number),
2258 )
2259 .await
2260 .expect("wait_for_latest_finalized_at_least did not return");
2261 assert_eq!(block, new_finalized.info);
2262 assert_eq!(by_number_requests.load(Ordering::SeqCst), 0);
2263
2264 let old_path = tokio::time::timeout(
2265 Duration::from_millis(500),
2266 l1_client.wait_for_finalized_block(pruned_number),
2267 )
2268 .await;
2269 old_path.expect_err("wait_for_finalized_block returned a block the RPC pruned");
2270 assert!(by_number_requests.load(Ordering::SeqCst) > 0);
2271 }
2272
2273 async fn serve_pruned_history_proxy(target: Url, by_number_requests: Arc<AtomicUsize>) -> Url {
2276 let provider = ProviderBuilder::new().connect_http(target);
2277 let route = warp::post()
2278 .and(warp::body::json::<Value>())
2279 .then(move |req: Value| {
2280 let provider = provider.clone();
2281 let by_number_requests = by_number_requests.clone();
2282 async move {
2283 let id = req["id"].clone();
2284 let is_by_number_query = req["method"].as_str() == Some("eth_getBlockByNumber")
2285 && req["params"]
2286 .get(0)
2287 .and_then(Value::as_str)
2288 .is_some_and(|block| block.starts_with("0x"));
2289 if is_by_number_query {
2290 by_number_requests.fetch_add(1, Ordering::SeqCst);
2291 return jsonrpc_result(id, Value::Null);
2292 }
2293
2294 let method = req["method"]
2295 .as_str()
2296 .expect("JSON-RPC request has method")
2297 .to_string();
2298 let result: Value = provider
2299 .raw_request(Cow::Owned(method), req["params"].clone())
2300 .await
2301 .expect("proxy request to anvil");
2302 jsonrpc_result(id, result)
2303 }
2304 });
2305 test_server::serve_on_random_port(route).await
2306 }
2307
2308 async fn serve_hanging() -> Url {
2310 let route = warp::post()
2311 .and(warp::body::json::<Value>())
2312 .then(|req: Value| async move {
2313 tokio::time::sleep(Duration::from_secs(60)).await;
2314 jsonrpc_result(req["id"].clone(), json!("0x0"))
2315 });
2316 test_server::serve_on_random_port(route).await
2317 }
2318
2319 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2320 async fn test_fetch_finalized_block_from_rpc_bounded_by_request_timeout() {
2321 let url = serve_hanging().await;
2322 let l1_client = L1ClientOptions {
2323 l1_request_timeout: Duration::from_millis(200),
2324 ..Default::default()
2325 }
2326 .connect(vec![url])
2327 .expect("Failed to create L1 client");
2328
2329 let result = tokio::time::timeout(
2330 Duration::from_secs(2),
2331 fetch_finalized_block_from_rpc(
2332 &l1_client.provider,
2333 l1_client.options().l1_request_timeout,
2334 ),
2335 )
2336 .await
2337 .expect("l1_request_timeout did not bound a hung finalized-block RPC call");
2338
2339 assert!(result.is_err(), "a hung RPC call must time out as an error");
2340 }
2341
2342 #[test_log::test(tokio::test)]
2348 async fn test_throttle_refresh_bounds_each_kind_independently() {
2349 let interval = Duration::from_millis(20);
2350 let l1_client = L1ClientOptions {
2351 l1_wait_refresh_interval: interval,
2352 ..Default::default()
2353 }
2354 .connect(vec!["http://localhost:0".parse().unwrap()])
2355 .expect("Failed to create L1 client");
2356
2357 assert!(
2358 l1_client.throttle_refresh(RefreshKind::Head).await,
2359 "first head refresh must not be throttled"
2360 );
2361 assert!(
2362 l1_client.throttle_refresh(RefreshKind::Finalized).await,
2363 "first finalized refresh must not be throttled"
2364 );
2365 assert!(
2366 !l1_client.throttle_refresh(RefreshKind::Head).await,
2367 "a second immediate head refresh must be throttled"
2368 );
2369 assert!(
2370 !l1_client.throttle_refresh(RefreshKind::Finalized).await,
2371 "a second immediate finalized refresh must be throttled"
2372 );
2373
2374 sleep(interval).await;
2375
2376 assert!(
2377 l1_client.throttle_refresh(RefreshKind::Head).await,
2378 "head refresh must be allowed again once the interval elapses"
2379 );
2380 }
2381
2382 fn jsonrpc_result(id: Value, result: Value) -> warp::reply::Json {
2384 warp::reply::json(&json!({ "jsonrpc": "2.0", "id": id, "result": result }))
2385 }
2386
2387 async fn serve_counting_head(counter: Arc<AtomicUsize>) -> Url {
2389 let route = warp::post()
2390 .and(warp::body::json::<Value>())
2391 .then(move |req: Value| {
2392 let counter = counter.clone();
2393 async move {
2394 counter.fetch_add(1, Ordering::SeqCst);
2395 jsonrpc_result(req["id"].clone(), json!("0x0"))
2396 }
2397 });
2398 test_server::serve_on_random_port(route).await
2399 }
2400
2401 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2402 async fn test_wait_for_block_refresh_is_throttled() {
2403 let counter = Arc::new(AtomicUsize::new(0));
2404 let url = serve_counting_head(counter.clone()).await;
2405 let l1_client = L1ClientOptions::default()
2406 .connect(vec![url])
2407 .expect("Failed to create L1 client");
2408
2409 let wait = Duration::from_secs(2);
2410 let waiters = (0..10).map(|_| {
2411 let l1_client = &l1_client;
2412 async move {
2413 tokio::time::timeout(wait, l1_client.wait_for_block(u64::MAX))
2414 .await
2415 .ok();
2416 }
2417 });
2418 futures::future::join_all(waiters).await;
2419
2420 let interval = l1_client.options().l1_wait_refresh_interval;
2421 let max_requests = (wait.as_millis() / interval.as_millis()) as usize + 1;
2422 let requests = counter.load(Ordering::SeqCst);
2423 assert!(
2424 requests >= 1,
2425 "10 concurrent waiters made no refresh request at all"
2426 );
2427 assert!(
2428 requests <= max_requests,
2429 "throttle allowed {requests} refresh requests from 10 concurrent waiters, expected at \
2430 most {max_requests}",
2431 );
2432 }
2433
2434 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2435 async fn test_wait_for_block_refresh_broadcasts_new_head() {
2436 let anvil = Anvil::new().arg("--no-mining").spawn();
2437 let l1_client = L1ClientOptions::default()
2438 .connect(vec![anvil.endpoint_url()])
2439 .expect("Failed to create L1 client");
2440 let mut events = l1_client.receiver.activate_cloned();
2441
2442 let head = l1_client.get_block_number().await.unwrap();
2443 l1_client.anvil_mine(Some(1), None).await.unwrap();
2444 tokio::time::timeout(Duration::from_secs(3), l1_client.wait_for_block(head + 1))
2445 .await
2446 .expect("wait_for_block did not refresh from the RPC without the poller");
2447
2448 assert_eq!(l1_client.snapshot().await.head, head + 1);
2449
2450 tokio::time::timeout(Duration::from_secs(1), async {
2451 loop {
2452 if let L1Event::NewHead { head: h } =
2453 events.next().await.expect("event stream open")
2454 && h == head + 1
2455 {
2456 return;
2457 }
2458 }
2459 })
2460 .await
2461 .expect("refresh did not broadcast NewHead for the refreshed head");
2462 }
2463
2464 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2466 async fn test_wait_for_block_refresh_recovers_from_rate_limit() {
2467 let counter = Arc::new(AtomicUsize::new(0));
2468 let rate_limited = test_server::serve_on_random_port(warp::any().map({
2469 let counter = counter.clone();
2470 move || {
2471 counter.fetch_add(1, Ordering::SeqCst);
2472 warp::reply::with_header(
2473 warp::reply::with_status(
2474 fixtures::ALCHEMY_RATE_LIMIT,
2475 test_server::StatusCode::TOO_MANY_REQUESTS,
2476 ),
2477 "content-type",
2478 "application/json",
2479 )
2480 }
2481 }))
2482 .await;
2483 let anvil = Anvil::new().block_time(1).spawn();
2484
2485 let l1_client = L1ClientOptions {
2486 l1_frequent_failure_tolerance: Duration::from_millis(0),
2487 l1_consecutive_failure_tolerance: 1,
2488 l1_rate_limit_delay: Some(Duration::ZERO),
2489 ..Default::default()
2490 }
2491 .connect(vec![rate_limited, anvil.endpoint_url()])
2492 .expect("Failed to create L1 client");
2493
2494 tokio::time::timeout(Duration::from_secs(10), l1_client.wait_for_block(3))
2495 .await
2496 .expect(
2497 "wait_for_block did not recover once the refresh failed over off the rate-limited \
2498 provider",
2499 );
2500
2501 let requests = counter.load(Ordering::SeqCst);
2505 assert_eq!(
2506 requests, MAX_CONSECUTIVE_RATE_LIMITS,
2507 "rate-limited provider kept receiving requests after failover"
2508 );
2509 }
2510
2511 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2513 async fn test_refresh_finalized_handles_missing_finalized_block() {
2514 let route = warp::post()
2515 .and(warp::body::json::<Value>())
2516 .then(|req: Value| async move { jsonrpc_result(req["id"].clone(), Value::Null) });
2517 let url = test_server::serve_on_random_port(route).await;
2518 let l1_client = L1ClientOptions::default()
2519 .connect(vec![url])
2520 .expect("Failed to create L1 client");
2521
2522 l1_client.refresh_finalized().await;
2523
2524 assert!(l1_client.snapshot().await.finalized.is_none());
2525 }
2526
2527 #[test_log::test(tokio::test)]
2528 async fn test_apply_head_never_lowers_snapshot() {
2529 let mut state = L1State::new(NonZeroUsize::new(10).unwrap());
2530 state.snapshot.head = 10;
2531 let metrics = L1ClientMetrics::new(&NoMetrics, 1);
2532 let (sender, mut receiver) = async_broadcast::broadcast(1);
2533
2534 apply_head(&mut state, 5, &metrics, &sender).await;
2535
2536 assert_eq!(state.snapshot.head, 10);
2537 assert!(
2538 receiver.try_recv().is_err(),
2539 "a non-advancing head must not broadcast NewHead"
2540 );
2541 }
2542
2543 #[test_log::test(tokio::test)]
2544 async fn test_apply_finalized_never_lowers_snapshot() {
2545 let mut state = L1State::new(NonZeroUsize::new(10).unwrap());
2546 let current = L1BlockInfoWithParent {
2547 info: L1BlockInfo {
2548 number: 10,
2549 timestamp: U256::from(1),
2550 hash: B256::repeat_byte(1),
2551 },
2552 parent_hash: B256::ZERO,
2553 };
2554 state.snapshot.finalized = Some(current.info);
2555 let metrics = L1ClientMetrics::new(&NoMetrics, 1);
2556 let (sender, mut receiver) = async_broadcast::broadcast(1);
2557
2558 let lower = L1BlockInfoWithParent {
2559 info: L1BlockInfo {
2560 number: 5,
2561 timestamp: U256::from(2),
2562 hash: B256::repeat_byte(2),
2563 },
2564 parent_hash: B256::ZERO,
2565 };
2566 apply_finalized(&mut state, lower, &metrics, &sender).await;
2567
2568 assert_eq!(state.snapshot.finalized, Some(current.info));
2569 assert!(
2570 receiver.try_recv().is_err(),
2571 "a non-advancing finalized block must not broadcast NewFinalized"
2572 );
2573 }
2574
2575 #[test_log::test(tokio::test)]
2576 async fn test_apply_finalized_rejects_equal_height_different_hash() {
2577 let mut state = L1State::new(NonZeroUsize::new(10).unwrap());
2578 let current = L1BlockInfoWithParent {
2579 info: L1BlockInfo {
2580 number: 10,
2581 timestamp: U256::from(1),
2582 hash: B256::repeat_byte(1),
2583 },
2584 parent_hash: B256::ZERO,
2585 };
2586 state.snapshot.finalized = Some(current.info);
2587 let metrics = L1ClientMetrics::new(&NoMetrics, 1);
2588 let (sender, mut receiver) = async_broadcast::broadcast(1);
2589
2590 let same_height_different_hash = L1BlockInfoWithParent {
2591 info: L1BlockInfo {
2592 number: 10,
2593 timestamp: U256::from(2),
2594 hash: B256::repeat_byte(2),
2595 },
2596 parent_hash: B256::ZERO,
2597 };
2598 apply_finalized(&mut state, same_height_different_hash, &metrics, &sender).await;
2599
2600 assert_eq!(state.snapshot.finalized, Some(current.info));
2601 assert!(
2602 receiver.try_recv().is_err(),
2603 "an equal-height block with a different hash must not overwrite the snapshot or \
2604 broadcast"
2605 );
2606 }
2607
2608 #[test_log::test(tokio::test)]
2609 async fn test_apply_finalized_applies_higher() {
2610 let mut state = L1State::new(NonZeroUsize::new(10).unwrap());
2611 state.snapshot.head = 10;
2614 let current = L1BlockInfoWithParent {
2615 info: L1BlockInfo {
2616 number: 5,
2617 timestamp: U256::from(1),
2618 hash: B256::repeat_byte(1),
2619 },
2620 parent_hash: B256::ZERO,
2621 };
2622 state.snapshot.finalized = Some(current.info);
2623 let metrics = L1ClientMetrics::new(&NoMetrics, 1);
2624 let (sender, mut receiver) = async_broadcast::broadcast(1);
2625
2626 let higher = L1BlockInfoWithParent {
2627 info: L1BlockInfo {
2628 number: 10,
2629 timestamp: U256::from(2),
2630 hash: B256::repeat_byte(2),
2631 },
2632 parent_hash: B256::ZERO,
2633 };
2634 apply_finalized(&mut state, higher, &metrics, &sender).await;
2635
2636 assert_eq!(state.snapshot.finalized, Some(higher.info));
2637 let event = receiver
2638 .try_recv()
2639 .expect("a higher finalized block broadcasts NewFinalized");
2640 assert!(matches!(event, L1Event::NewFinalized { finalized } if finalized == higher));
2641 }
2642
2643 #[test_log::test(tokio::test)]
2644 async fn test_apply_finalized_raises_head_to_match() {
2645 let mut state = L1State::new(NonZeroUsize::new(10).unwrap());
2646 state.snapshot.head = 10;
2647 let metrics = L1ClientMetrics::new(&NoMetrics, 1);
2648 let (sender, mut receiver) = async_broadcast::broadcast(2);
2649
2650 let finalized = L1BlockInfoWithParent {
2651 info: L1BlockInfo {
2652 number: 20,
2653 timestamp: U256::from(1),
2654 hash: B256::repeat_byte(1),
2655 },
2656 parent_hash: B256::ZERO,
2657 };
2658 apply_finalized(&mut state, finalized, &metrics, &sender).await;
2659
2660 assert_eq!(state.snapshot.head, 20);
2661 assert_eq!(state.snapshot.finalized, Some(finalized.info));
2662
2663 let head_event = receiver
2664 .try_recv()
2665 .expect("a finalized block above the head broadcasts NewHead");
2666 assert!(matches!(head_event, L1Event::NewHead { head } if head == 20));
2667
2668 let finalized_event = receiver
2669 .try_recv()
2670 .expect("the finalized block also broadcasts NewFinalized");
2671 assert!(
2672 matches!(finalized_event, L1Event::NewFinalized { finalized: f } if f == finalized)
2673 );
2674 }
2675
2676 async fn test_reconnect_update_task_helper(ws: bool) {
2677 let anvil = Arc::new(Anvil::new().block_time(1).port(0u16).spawn());
2679 let port = anvil.port();
2680 let client = new_l1_client(&anvil, ws).await;
2681
2682 let initial_state = client.snapshot().await;
2683 tracing::info!(?initial_state, "initial state");
2684
2685 let mut retry = 0;
2687 let updated_state = loop {
2688 assert!(retry < 10, "state did not update in time");
2689
2690 let updated_state = client.snapshot().await;
2691 if updated_state.head > initial_state.head {
2692 break updated_state;
2693 }
2694 tracing::info!(retry, "waiting for state update");
2695 sleep(Duration::from_secs(1)).await;
2696 retry += 1;
2697 };
2698 tracing::info!(?updated_state, "state updated");
2699
2700 drop(anvil);
2704
2705 tracing::info!("sleep 5");
2710 sleep(Duration::from_secs(5)).await;
2711
2712 tracing::info!("restarting L1");
2714 let _anvil = Anvil::new().block_time(1).port(port).spawn();
2715
2716 let mut retry = 0;
2717 let final_state = loop {
2718 assert!(retry < 5, "state did not update in time");
2719
2720 let final_state = client.snapshot().await;
2721 if final_state.head > updated_state.head {
2722 break final_state;
2723 }
2724 tracing::info!(retry, "waiting for state update");
2725 sleep(Duration::from_secs(1)).await;
2726 retry += 1;
2727 };
2728 tracing::info!(?final_state, "state updated");
2729 }
2730
2731 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2732 async fn test_reconnect_update_task_ws() {
2733 test_reconnect_update_task_helper(true).await
2734 }
2735
2736 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2737 async fn test_reconnect_update_task_http() {
2738 test_reconnect_update_task_helper(false).await
2739 }
2740
2741 #[test]
2782 fn test_rate_limits_inside_the_backoff_window_count_once() {
2783 let mut status = SingleTransportStatus::default();
2784 let until = Instant::now() + Duration::from_secs(3600);
2785
2786 assert!(!status.log_rate_limit(until));
2787 for _ in 0..MAX_CONSECUTIVE_RATE_LIMITS + 1 {
2788 assert!(!status.log_rate_limit(until), "still inside the window");
2789 }
2790 assert_eq!(status.consecutive_rate_limits, 1);
2791 }
2792
2793 #[test]
2795 fn test_rate_limits_outliving_their_backoff_switch() {
2796 let mut status = SingleTransportStatus::default();
2797
2798 for _ in 0..MAX_CONSECUTIVE_RATE_LIMITS - 1 {
2799 assert!(!status.log_rate_limit(Instant::now()));
2800 }
2801 assert!(status.log_rate_limit(Instant::now()));
2802 }
2803
2804 #[test]
2807 fn test_success_resets_the_rate_limit_count() {
2808 let mut status = SingleTransportStatus::default();
2809
2810 for _ in 0..MAX_CONSECUTIVE_RATE_LIMITS * 2 {
2811 assert!(!status.log_rate_limit(Instant::now()));
2812 status.log_success();
2813 }
2814 assert_eq!(status.consecutive_rate_limits, 0);
2815 }
2816
2817 fn get_failover_index(provider: &L1Client) -> usize {
2819 let transport = &provider.transport;
2820 provider.transport.current_transport.read().generation % transport.urls.len()
2821 }
2822
2823 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2826 async fn test_failover_on_dead_provider_with_json_rpc_error_body() {
2827 let dead_provider = test_server::serve_fixed(
2828 test_server::StatusCode::FORBIDDEN,
2829 "application/json",
2830 fixtures::ALCHEMY_APP_INACTIVE,
2831 )
2832 .await;
2833 let anvil = Anvil::new().block_time(1).spawn();
2834
2835 let provider = L1ClientOptions {
2836 l1_frequent_failure_tolerance: Duration::from_millis(0),
2837 l1_consecutive_failure_tolerance: 3,
2838 ..Default::default()
2839 }
2840 .connect(vec![dead_provider, anvil.endpoint_url()])
2841 .expect("Failed to create L1 client");
2842
2843 for _ in 0..2 {
2844 provider.get_block_number().await.unwrap_err();
2845 assert_eq!(get_failover_index(&provider), 0);
2846 }
2847
2848 provider.get_block_number().await.unwrap_err();
2849 assert_eq!(
2850 get_failover_index(&provider),
2851 1,
2852 "client should have failed over from the dead provider"
2853 );
2854 provider
2855 .get_block_number()
2856 .await
2857 .expect("requests succeed from the healthy provider");
2858 }
2859
2860 async fn rate_limited_client(rate_limit_delay: Duration) -> (L1Client, AnvilInstance) {
2863 let rate_limited = test_server::serve_fixed(
2864 test_server::StatusCode::TOO_MANY_REQUESTS,
2865 "application/json",
2866 fixtures::ALCHEMY_RATE_LIMIT,
2867 )
2868 .await;
2869 let anvil = Anvil::new().block_time(1).spawn();
2870
2871 let client = L1ClientOptions {
2872 l1_frequent_failure_tolerance: Duration::from_millis(0),
2873 l1_consecutive_failure_tolerance: 1,
2874 l1_rate_limit_delay: Some(rate_limit_delay),
2875 ..Default::default()
2876 }
2877 .connect(vec![rate_limited, anvil.endpoint_url()])
2878 .expect("Failed to create L1 client");
2879
2880 (client, anvil)
2881 }
2882
2883 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2885 async fn test_no_failover_on_rate_limit_json_rpc_error_body() {
2886 let (provider, _anvil) = rate_limited_client(Duration::from_secs(3600)).await;
2887
2888 let err = provider.get_block_number().await.unwrap_err();
2889 assert_eq!(get_failover_index(&provider), 0);
2890 assert!(
2891 !err.to_string().contains("Rate limit exceeded"),
2892 "first call reaches the provider: {err}"
2893 );
2894
2895 let err = provider.get_block_number().await.unwrap_err();
2898 assert_eq!(get_failover_index(&provider), 0);
2899 assert!(
2900 err.to_string().contains("Rate limit exceeded"),
2901 "second call short-circuits on the backoff: {err}"
2902 );
2903 }
2904
2905 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2909 async fn test_failover_on_persistent_rate_limit() {
2910 let (provider, _anvil) = rate_limited_client(Duration::ZERO).await;
2911
2912 for _ in 0..MAX_CONSECUTIVE_RATE_LIMITS {
2913 provider.get_block_number().await.unwrap_err();
2914 }
2915
2916 assert_eq!(get_failover_index(&provider), 1);
2917 provider
2918 .get_block_number()
2919 .await
2920 .expect("requests succeed from the healthy provider");
2921 }
2922
2923 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2927 async fn test_no_failover_on_request_rejection() {
2928 let rejecting = test_server::serve_fixed(
2929 test_server::StatusCode::OK,
2930 "application/json",
2931 fixtures::INFURA_TOO_MANY_RESULTS,
2932 )
2933 .await;
2934 let anvil = Anvil::new().block_time(1).spawn();
2935
2936 let provider = L1ClientOptions {
2937 l1_consecutive_failure_tolerance: 1,
2938 ..Default::default()
2939 }
2940 .connect(vec![rejecting, anvil.endpoint_url()])
2941 .expect("Failed to create L1 client");
2942
2943 for _ in 0..3 {
2944 provider.get_block_number().await.unwrap_err();
2945 assert_eq!(get_failover_index(&provider), 0);
2946 }
2947 }
2948
2949 async fn test_failover_update_task_helper(ws: bool) {
2950 let anvil = Anvil::new().block_time(1).spawn();
2951
2952 let client = L1ClientOptions {
2955 l1_polling_interval: Duration::from_secs(1),
2956 subscription_timeout: Duration::from_secs(1000),
2959 l1_ws_provider: if ws {
2960 Some(vec![
2961 "ws://notarealurl:1234".parse().unwrap(),
2962 anvil.ws_endpoint_url(),
2963 ])
2964 } else {
2965 None
2966 },
2967 ..Default::default()
2968 }
2969 .connect(vec![
2970 "http://notarealurl:1234".parse().unwrap(),
2971 anvil.endpoint_url(),
2972 ])
2973 .expect("Failed to create L1 client");
2974
2975 client.spawn_tasks().await;
2976
2977 let initial_state = client.snapshot().await;
2978 tracing::info!(?initial_state, "initial state");
2979
2980 let mut retry = 0;
2982 let updated_state = loop {
2983 assert!(retry < 10, "state did not update in time");
2984
2985 let updated_state = client.snapshot().await;
2986 if updated_state.head > initial_state.head {
2987 break updated_state;
2988 }
2989 tracing::info!(retry, "waiting for state update");
2990 sleep(Duration::from_secs(1)).await;
2991 retry += 1;
2992 };
2993 tracing::info!(?updated_state, "state updated");
2994 }
2995
2996 #[test_log::test(tokio::test(flavor = "multi_thread"))]
2997 async fn test_failover_update_task_ws() {
2998 test_failover_update_task_helper(true).await;
2999 }
3000
3001 #[test_log::test(tokio::test(flavor = "multi_thread"))]
3002 async fn test_failover_update_task_http() {
3003 test_failover_update_task_helper(false).await;
3004 }
3005
3006 #[test_log::test(tokio::test(flavor = "multi_thread"))]
3007 async fn test_failover_consecutive_failures() {
3008 let anvil = Anvil::new().block_time(1).spawn();
3009
3010 let l1_options = L1ClientOptions {
3011 l1_polling_interval: Duration::from_secs(1),
3012 l1_frequent_failure_tolerance: Duration::from_millis(0),
3013 l1_consecutive_failure_tolerance: 3,
3014 ..Default::default()
3015 };
3016
3017 let provider = l1_options
3018 .connect(vec![
3019 "http://notarealurl:1234".parse().unwrap(),
3020 anvil.endpoint_url(),
3021 ])
3022 .expect("Failed to create L1 client");
3023
3024 for _ in 0..2 {
3026 provider.get_block_number().await.unwrap_err();
3027 assert!(get_failover_index(&provider) == 0);
3028 }
3029
3030 provider.get_block_number().await.unwrap_err();
3032 assert!(get_failover_index(&provider) == 1);
3033
3034 provider.get_block_number().await.unwrap();
3036 }
3037
3038 #[test_log::test(tokio::test(flavor = "multi_thread"))]
3039 async fn test_failover_frequent_failures() {
3040 let anvil = Anvil::new().block_time(1).spawn();
3041 let provider = L1ClientOptions {
3042 l1_polling_interval: Duration::from_secs(1),
3043 l1_frequent_failure_tolerance: Duration::from_millis(100),
3044 ..Default::default()
3045 }
3046 .connect(vec![
3047 "http://notarealurl:1234".parse().unwrap(),
3048 anvil.endpoint_url(),
3049 ])
3050 .expect("Failed to create L1 client");
3051
3052 provider.get_block_number().await.unwrap_err();
3054 sleep(Duration::from_secs(1)).await;
3055 provider.get_block_number().await.unwrap_err();
3056
3057 assert!(get_failover_index(&provider) == 0);
3059
3060 sleep(Duration::from_secs(1)).await;
3062
3063 provider.get_block_number().await.unwrap_err();
3065 provider.get_block_number().await.unwrap_err();
3066 provider.get_block_number().await.unwrap();
3067 assert!(get_failover_index(&provider) == 1);
3068 }
3069
3070 #[test_log::test(tokio::test(flavor = "multi_thread"))]
3071 async fn test_failover_revert() {
3072 let anvil = Anvil::new().block_time(1).spawn();
3073 let provider = L1ClientOptions {
3074 l1_polling_interval: Duration::from_secs(1),
3075 l1_consecutive_failure_tolerance: 1,
3076 l1_failover_revert: Duration::from_secs(2),
3077 ..Default::default()
3078 }
3079 .connect(vec![
3080 "http://notarealurl:1234".parse().unwrap(),
3081 anvil.endpoint_url(),
3082 ])
3083 .expect("Failed to create L1 client");
3084
3085 provider.get_block_number().await.unwrap_err();
3087 assert_eq!(get_failover_index(&provider), 1);
3088
3089 provider.get_block_number().await.unwrap();
3091
3092 sleep(Duration::from_millis(2100)).await;
3094 provider.get_block_number().await.unwrap_err();
3095 }
3096
3097 async fn serve_finalized_failing_proxy(target: Url) -> Url {
3101 let provider = ProviderBuilder::new().connect_http(target);
3102 let route = warp::post()
3103 .and(warp::body::json::<Value>())
3104 .then(move |req: Value| {
3105 let provider = provider.clone();
3106 async move {
3107 let id = req["id"].clone();
3108 let is_finalized_query = req["method"].as_str() == Some("eth_getBlockByNumber")
3109 && req["params"].get(0).and_then(Value::as_str) == Some("finalized");
3110 if is_finalized_query {
3111 return warp::reply::json(&json!({
3112 "jsonrpc": "2.0",
3113 "id": id,
3114 "error": {"code": -32000, "message": "finalized RPC unavailable"},
3115 }));
3116 }
3117
3118 let method = req["method"].as_str().unwrap_or_default().to_string();
3119 let result: Value = provider
3120 .raw_request(Cow::Owned(method), req["params"].clone())
3121 .await
3122 .expect("proxy request to anvil");
3123 jsonrpc_result(id, result)
3124 }
3125 });
3126 test_server::serve_on_random_port(route).await
3127 }
3128
3129 #[test_log::test(tokio::test(flavor = "multi_thread"))]
3130 async fn test_head_not_gated_by_finalized() {
3131 let anvil = Anvil::new().block_time(1).spawn();
3132 let proxy_url = serve_finalized_failing_proxy(anvil.endpoint_url()).await;
3133
3134 let client = L1ClientOptions {
3135 l1_polling_interval: Duration::from_millis(200),
3136 ..Default::default()
3137 }
3138 .connect(vec![proxy_url])
3139 .expect("Failed to create L1 client");
3140 client.spawn_tasks().await;
3141
3142 let initial = client.snapshot().await;
3143 let mut retry = 0;
3144 loop {
3145 assert!(
3146 retry < 30,
3147 "head did not advance despite a failing finalized RPC"
3148 );
3149 let snapshot = client.snapshot().await;
3150 if snapshot.head > initial.head {
3151 break;
3152 }
3153 sleep(Duration::from_millis(200)).await;
3154 retry += 1;
3155 }
3156
3157 assert!(
3158 client.snapshot().await.finalized.is_none(),
3159 "finalized RPC always fails in this test"
3160 );
3161 }
3162
3163 #[test_log::test(tokio::test(flavor = "multi_thread"))]
3167 async fn test_update_loop_initializes_l1_state() {
3168 let anvil = Arc::new(Anvil::new().port(0u16).spawn());
3170 let l1_client = new_l1_client(&anvil, true).await;
3171
3172 for _try in 0..10 {
3173 let mut state = l1_client.state.lock().await;
3174 let has_snapshot = state.snapshot.finalized.is_some();
3175 let has_cache = state.finalized.get(&0).is_some();
3176 drop(state);
3177 if has_snapshot && has_cache {
3178 return;
3179 }
3180 sleep(Duration::from_millis(200)).await;
3181 }
3182 panic!("L1 state of L1Client not initialized");
3183 }
3184}