Skip to main content

espresso_types/v0/impls/
l1.rs

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            // `RawCommitmentBuilder` doesn't have a `u256_field` method, so we simulate it:
107            .constant_str("timestamp")
108            .fixed_size_bytes(&timestamp)
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    /// Hash-chain verify every block, no matter how old.
135    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    /// Use the given metrics collector to publish metrics related to the L1 client.
189    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    /// Instantiate an `L1Client` for a given list of provider `Url`s.
195    #[cfg(feature = "node")]
196    pub fn connect(self, urls: Vec<Url>) -> anyhow::Result<L1Client> {
197        // create custom transport
198        let t = SwitchingTransport::new(self, urls)
199            .with_context(|| "failed to create switching transport")?;
200        // Create a new L1 client with the transport
201        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        // Create a counter family for the failures per URL
214        let failures = metrics.counter_family("failed_requests".into(), vec!["provider".into()]);
215
216        // Create a counter for each URL
217        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    /// Create a new `SwitchingTransport` with the given options and URLs
237    pub fn new(opt: L1ClientOptions, urls: Vec<Url>) -> anyhow::Result<Self> {
238        // Return early if there were no URLs provided
239        let Some(first_url) = urls.first().cloned() else {
240            return Err(anyhow::anyhow!("No valid URLs provided"));
241        };
242
243        // Create the metrics
244        let metrics = L1ClientMetrics::new(&**opt.metrics, urls.len());
245
246        // Create a new `SingleTransport` for the first URL
247        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    /// Returns when the transport has been switched
259    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    /// Log a successful call to the inner transport
275    fn log_success(&mut self) {
276        self.consecutive_failures = 0;
277        self.consecutive_rate_limits = 0;
278    }
279
280    /// Log a rate limit from the inner transport, backing off until `until`. Returns whether or
281    /// not the transport should be switched to the next URL.
282    ///
283    /// One rate limit is not a provider fault: the caller backs off and retries. One that
284    /// outlives a backoff is, because every request inside the backoff window short-circuits
285    /// without reaching the provider, and nothing else in `call` moves off it: `should_revert`
286    /// only reverts *towards* the primary.
287    ///
288    /// Only a rate limit arriving outside a backoff window counts. Requests already in flight
289    /// when the window opened tell us nothing new, and would otherwise reach
290    /// [`MAX_CONSECUTIVE_RATE_LIMITS`] in a single burst with no backoff having elapsed at all.
291    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    /// Log a failure to call the inner transport. Returns whether or not the transport should be switched to the next URL
309    fn log_failure(&mut self, opt: &L1ClientOptions) -> bool {
310        // Increment the consecutive failures
311        self.consecutive_failures += 1;
312
313        // Check if we should switch to the next URL
314        let should_switch = self.should_switch(opt);
315
316        // Update the last failure time
317        self.last_failure = Some(Instant::now());
318
319        // Return whether or not we should switch
320        should_switch
321    }
322
323    /// Whether or not the transport should be switched to the next URL
324    fn should_switch(&mut self, opt: &L1ClientOptions) -> bool {
325        // If someone else already beat us to switching, return false
326        if self.shutting_down {
327            return false;
328        }
329
330        // If we've reached the max number of consecutive failures, switch to the next URL
331        if self.consecutive_failures >= opt.l1_consecutive_failure_tolerance {
332            self.shutting_down = true;
333            return true;
334        }
335
336        // If we've failed recently, switch to the next URL
337        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    /// Whether or not the transport should be switched back to the primary URL.
349    fn should_revert(&mut self, revert_at: Option<Instant>) -> bool {
350        if self.shutting_down {
351            // We have already switched away from this transport in another thread.
352            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    /// Create a new `SingleTransport` with the given URL
369    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/// alloy returns a non-2xx response with a parseable JSON-RPC body as `Ok`, so the payload, not
381/// the `Result` arm, decides whether a provider is healthy.
382///
383/// This is a partial classification, deliberately. It covers only major error flavors the fleet
384/// dumps actually captured. Much remains open: handling the most common providers and RPC flavors,
385/// block ranges that adapt to what a provider will serve, separating a dead API key from a
386/// transient failure, etc.
387///
388/// Unless we're fixing urgent bugs we should not add to this list until we have a satisfactory
389/// design for robust handling of the whole RPC spectrum, because every provider and RPC flavor has
390/// its own quirks.
391#[cfg(feature = "node")]
392#[derive(Debug)]
393enum ResponseOutcome {
394    Healthy,
395    /// Provider is rate limiting. Back off rather than fail over, up to
396    /// [`MAX_CONSECUTIVE_RATE_LIMITS`].
397    RateLimited {
398        retry_after: Option<Duration>,
399    },
400    /// The request earned the rejection on its own terms. Scores neither way: every backup
401    /// rejects the same request identically, so failing over neither helps nor indicates
402    /// anything about the provider.
403    RequestRejected,
404    Failed,
405}
406
407/// How many rate limits, each outliving the backoff the previous one imposed, before a provider
408/// is treated as failed.
409///
410/// A provider still rate limiting after a backoff of its own is not merely busy. Counting it
411/// bounds how long the client stays pinned to a provider whose quota is exhausted for the day;
412/// without it, `rate_limited_until` short-circuits every request forever and the healthy backup
413/// is never reached.
414#[cfg(feature = "node")]
415const MAX_CONSECUTIVE_RATE_LIMITS: usize = 2;
416
417/// Cap on a server-requested `Retry-After`, which alloy parses verbatim. Honoring a
418/// daily-quota delay of hours would stall `wait_for_l1` for its whole duration.
419///
420/// Matches alloy's own private `MAX_BACKOFF_HINT`, which `RpcErrorExt::backoff_hint` clamps to
421/// so "a misbehaving server cannot stall retries"
422/// ([alloy-transport-2.4.1/src/error.rs:266-272](https://github.com/alloy-rs/alloy/blob/v2.4.1/crates/transport/src/error.rs#L266-L272)).
423#[cfg(feature = "node")]
424const MAX_RATE_LIMIT_BACKOFF: Duration = Duration::from_secs(300);
425
426/// The configured delay is operator-set and used as-is; a server-provided one is capped.
427#[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/// Whether the response rejects the request itself: the result set it asks for is over the
436/// provider's cap.
437///
438/// This recurs against a perfectly healthy provider. `l1_events_max_block_range` defaults to
439/// 10000, exactly infura's result cap, and `get_finalized_deposits` retries a rejected chunk in
440/// a loop at `l1_retry_delay`, so scoring it would burn a generation every couple of seconds and
441/// cycle the whole provider list against a request every one of them rejects.
442///
443/// Only result-count rejections qualify. A block-range cap reads the same way but is a plan or
444/// node-config limit, not a property of the request: alchemy caps `eth_getLogs` at 10 blocks on
445/// the free tier and not at all on paid ones, so a backup may well serve the identical request
446/// and failing over is the right move.
447///
448/// Matched on the message because infura reuses `-32005` for both a result-count rejection and a
449/// rate limit. Claiming it before [`ErrorPayload::is_retry_err`], which calls every `-32005` a
450/// rate limit, is what makes deferring to alloy's matcher safe here.
451#[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/// The JSON-RPC error a response carries, if any.
458///
459/// Logged in place of the whole `Result`: a scored failure arrives as `Ok(Failure(..))`, which
460/// reads as a success in a log search, and the packet holds the provider's entire response body.
461#[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    /// Score a transport response for provider health. A batch is scored by the first error in
471    /// it; no batch callers exist today.
472    fn classify(result: &StdResult<ResponsePacket, RpcError<TransportErrorKind>>) -> Self {
473        use ResponseOutcome::*;
474        match result {
475            // Ordered before the rate-limit check: alloy treats every `-32005` as a rate limit,
476            // and infura reuses that code for result-count rejections.
477            Ok(res) if is_request_rejection(res) => RequestRejected,
478            // alloy returns an HTTP 429 whose body parses as `Ok`, dropping the status and
479            // `Retry-After`, so the payload is the only signal left.
480            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/// `SwitchingTransport` is an alternative [`Client`](https://docs.rs/alloy/0.12.5/alloy/transports/http/struct.Client.html)
500/// which by implementing `tower_service::Service`, traits like [`Transport`](https://docs.rs/alloy/0.12.5/alloy/transports/trait.Transport.html)
501/// are auto-derived, thus can be used as an alt [`RpcClient`](https://docs.rs/alloy/0.12.5/alloy/rpc/client/struct.RpcClient.html#method.new)
502/// that can be further hooked with `Provider` via `Provider::on_client()`.
503#[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        // Just poll the (current) inner client
516        self.current_transport.read().clone().client.poll_ready(cx)
517    }
518
519    fn call(&mut self, req: RequestPacket) -> Self::Future {
520        // Clone ourselves
521        let self_clone = self.clone();
522
523        // Pin and box, which turns this into a future
524        Box::pin(async move {
525            // Clone the current transport
526            let mut current_transport = self_clone.current_transport.read().clone();
527
528            // Revert back to the primary transport if it's time.
529            let should_revert = current_transport
530                .status
531                .write()
532                .should_revert(current_transport.revert_at);
533            if should_revert {
534                // Switch to the next generation which maps to index 0.
535                let n = self_clone.urls.len();
536                // Rounding down to a multiple of n gives us the last generation of the primary transport.
537                let prev_primary_gen = (current_transport.generation / n) * n;
538                // Adding n jumps to the next generation.
539                let next_gen = prev_primary_gen + n;
540                current_transport = self_clone.switch_to(next_gen, current_transport);
541            }
542
543            // If we've been rate limited, back off until the limit (hopefully) expires.
544            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 an error with a non-standard code to indicate client-side rate limit.
548                    return Err(RpcError::Transport(TransportErrorKind::Custom(
549                        "Rate limit exceeded".into(),
550                    )));
551                } else {
552                    // Reset the rate limit if we are passed it so we don't check every time
553                    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            // Leaves the failure counters untouched, in both directions: the request would be
566            // rejected by every other provider too, so it is evidence about neither. Warned
567            // rather than counted, because the caller has to shrink the request to make progress
568            // and the failover metrics would say nothing about that.
569            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            // Increment the failure metric
579            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            // We don't need to worry about race conditions here, since either of these will only
588            // return true once.
589            let should_switch = if let ResponseOutcome::RateLimited { retry_after } = outcome {
590                // Back off on this provider rather than failing over, until it has kept rate
591                // limiting through a backoff of its own.
592                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                // Warned only when it ends in a failover, which is otherwise unexplainable from
597                // production logs: a routine backoff is too frequent to warn about.
598                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                // Increment the failovers metric
628                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            // If we are reverting to the primary transport, clear our scheduled revert time.
646            None
647        } else if current_transport.generation.is_multiple_of(self.urls.len()) {
648            // If we are failing over from the primary transport, schedule a time to automatically
649            // revert back.
650            Some(Instant::now() + self.opt.l1_failover_revert)
651        } else {
652            // Otherwise keep the currently scheduled revert time.
653            current_transport.revert_at
654        };
655
656        // Create a new transport from the next URL and index
657        let new_transport = SingleTransport::new(&url, next_gen, revert_at);
658
659        // Switch to the next URL
660        *self.current_transport.write() = new_transport.clone();
661
662        // Notify the transport that it has been switched
663        self.switch_notify.notify_waiters();
664
665        new_transport
666    }
667}
668
669/// Which of `L1State`'s independent refresh throttles `throttle_refresh` bounds.
670#[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        // Create a new provider with that RPC client using the custom transport
680        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    /// Construct a new L1 client with the default options.
700    pub fn new(url: Vec<Url>) -> anyhow::Result<Self> {
701        L1ClientOptions::default().connect(url)
702    }
703
704    /// test only
705    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    /// Start the background tasks which keep the L1 client up to date.
714    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    /// Shut down background tasks associated with this L1 client.
722    ///
723    /// The L1 client will still be usable, but will stop updating until [`start`](Self::start) is
724    /// called again.
725    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                // Fetch current L1 head block for the first value of the stream to avoid having
753                // to wait for new L1 blocks until the update loop starts processing blocks.
754                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                // Subscribe to new blocks.
775                let mut block_stream = {
776                    let res = match &ws_urls {
777                        Some(urls) => {
778                            // Use a new WebSockets host each time we retry in case there is a
779                            // problem with one of the hosts specifically.
780                            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                                // Configure it and get the stream
804                                let stream = poller_builder.with_poll_interval(polling_interval).into_stream();
805
806                                let rpc = rpc.clone();
807
808                                // For HTTP, we simulate a subscription by polling. The polling
809                                // stream provided by ethers only yields block hashes, so for each
810                                // one, we have to go fetch the block itself.
811                                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                                                // If we can't fetch the block for some reason, we can
819                                                // just skip it.
820                                                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 the transport is switched, so we will call `watch_blocks` instantly on it
835                            }.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                    // Wait for a block, timing out if we don't get one soon enough
852                    let block_timeout = tokio::time::timeout(subscription_timeout, block_stream.next()).await;
853                    match block_timeout {
854                        // We got a block
855                        Ok(Some(head)) => {
856                            let head = head.number;
857                            tracing::debug!(head, "Received L1 block");
858
859                            // Apply the head before touching finalized, so a failing finalized
860                            // RPC never stalls the head.
861                            {
862                                let mut state = state.lock().await;
863                                apply_head(&mut state, head, &metrics, &sender).await;
864                            }
865
866                            // A new block has been produced. This happens fairly rarely, so it is
867                            // now ok to poll to see if a new block has been finalized. One attempt
868                            // per head: a failure retries on the next head instead of blocking here.
869                            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                        // The stream ended
883                        Ok(None) => {
884                            tracing::error!("L1 block stream ended unexpectedly, trying to re-establish block stream");
885                            break;
886                        }
887                        // We timed out waiting for a block
888                        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    /// Get a snapshot from the l1.
901    pub async fn snapshot(&self) -> L1Snapshot {
902        self.state.lock().await.snapshot
903    }
904
905    /// Wait until the highest L1 block number reaches at least `number`.
906    ///
907    /// This function does not return any information about the block, since the block is not
908    /// necessarily finalized when it returns. It is only used to guarantee that some block at
909    /// height `number` exists, possibly in the unsafe part of the L1 chain.
910    ///
911    /// While behind, refreshes the head from the RPC directly rather than only waiting on the
912    /// poller.
913    pub async fn wait_for_block(&self, number: u64) {
914        'outer: loop {
915            // Subscribe to events before checking the current state, to ensure we don't miss a
916            // relevant event.
917            let mut events = self.receiver.activate_cloned();
918
919            // Check if the block we are waiting for already exists.
920            {
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            // Wait for the block, refreshing from the RPC on a fixed tick so a lagging poller
929            // doesn't stall the wait. The tick runs on its own schedule so a stream of events
930            // that don't satisfy `number` can't keep deferring it.
931            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; // the first tick fires immediately
934
935            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                                // This should not happen: the event stream ended. All we can do
949                                // is try again.
950                                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    /// Get information about the given block.
970    ///
971    /// If the desired block number is not finalized yet, this function will block until it becomes
972    /// finalized.
973    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    /// Wait until the finalized block number reaches at least `number` and return the latest
979    /// finalized block, without fetching block `number` itself.
980    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    /// Wait until the finalized block number reaches at least `number`, returning the locked
985    /// state and the latest finalized block.
986    ///
987    /// While behind, refreshes the finalized block from the RPC directly.
988    async fn lock_once_finalized_reaches(
989        &self,
990        number: u64,
991    ) -> (MutexGuard<'_, L1State>, L1BlockInfo) {
992        'outer: loop {
993            // Subscribe to events before checking the current state, to ensure we don't miss a relevant
994            // event.
995            let mut events = self.receiver.activate_cloned();
996
997            // Check if the block we are waiting for already exists.
998            {
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            // Wait for the block, refreshing from the RPC on a fixed tick so a lagging poller
1013            // doesn't stall the wait. The tick runs on its own schedule so a stream of events
1014            // that don't satisfy `number` can't keep deferring it.
1015            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; // the first tick fires immediately
1018
1019            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                                // This should not happen: the event stream ended. All we can do
1035                                // is try again.
1036                                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    /// Get information about the first finalized block with timestamp greater than or equal
1058    /// `timestamp`.
1059    pub async fn wait_for_finalized_block_with_timestamp(&self, timestamp: U256) -> L1BlockInfo {
1060        // Wait until the finalized block has timestamp >= `timestamp`.
1061        let (mut state, mut block) = 'outer: loop {
1062            // Subscribe to events before checking the current state, to ensure we don't miss a
1063            // relevant event.
1064            let mut events = self.receiver.activate_cloned();
1065
1066            // Check if the block we are waiting for already exists.
1067            {
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            // Wait for the block.
1082            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            // This should not happen: the event stream ended. All we can do is try again.
1094            tracing::warn!(%timestamp, "L1 event stream ended unexpectedly; retry",);
1095            self.retry_delay().await;
1096        };
1097
1098        // It is possible there is some earlier block that also has the proper timestamp. Binary
1099        // search until we find the true earliest block with timestamp >= `timestamp`.
1100        //
1101        // Invariants:
1102        // * `upper_bound <= lower_bound`
1103        // * `upper_bound = block.number`
1104        // * Block number `lower_bound - 1` has timestamp < `timestamp` (strictly)
1105        // * `block` has timestamp >= `timestamp`
1106        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    /// Queries the L1 head directly from the RPC and applies it, throttled by
1136    /// [`Self::throttle_refresh`]. Bounded to `l1_wait_refresh_timeout` so a hung request can't
1137    /// park a waiter past the point where the poller has already published the head.
1138    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    /// Queries the L1 finalized block directly from the RPC and applies it, throttled by
1158    /// [`Self::throttle_refresh`]. Bounded to `l1_wait_refresh_timeout` so a hung request can't
1159    /// park a waiter past the point where the poller has already published the finalized block.
1160    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    /// Bounds `refresh_head` or `refresh_finalized` (per `kind`) to one RPC call per
1180    /// `l1_wait_refresh_interval`, shared across every concurrent waiter of that kind, so a burst
1181    /// of waiters on a stalled L1 doesn't turn into a busy loop of RPC calls. Head and finalized
1182    /// are throttled independently, so a wait on one can't starve a wait on the other.
1183    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            // If the requested block height is so old that we can assume all L1 providers have
1217            // finalized it, we don't need to worry about failing over to a lagging L1 provider
1218            // which has yet to finalize the block, so we don't need to bother with the
1219            // expensive hash chaining logic below. Just look up the block by number and assume
1220            // the response is finalized.
1221            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        // To get this block and be sure we are getting the correct finalized block, we first need
1233        // to find an equal or later block so we can find the expected hash of this block. If we
1234        // were to just look up the block by number, there could be problems if we failed over to a
1235        // different (lagging) L1 provider, which has yet to finalize this block and reports a
1236        // different block with the same number.
1237        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                // We don't have any cached finalized block after the requested one; fetch the
1245                // current finalized block from the network.
1246                // Don't hold state lock while fetching from network.
1247                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        // Work backwards from the known finalized successor, fetching blocks by parent hash so we
1278        // know we are getting the correct block.
1279        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        // Don't hold state lock while fetching from network.
1299        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    /// Get fee info for each `Deposit` occurring between `prev`
1332    /// and `new`. Returns `Vec<FeeInfo>`
1333    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        // No new blocks have been finalized, therefore there are no
1340        // new deposits.
1341        if prev_finalized >= Some(new_finalized) {
1342            return vec![];
1343        }
1344
1345        let opt = self.options();
1346
1347        // `prev` should have already been processed unless we
1348        // haven't processed *any* blocks yet.
1349        let prev = prev_finalized.map(|prev| prev + 1).unwrap_or(0);
1350
1351        // Divide the range `prev_finalized..=new_finalized` into chunks of size
1352        // `events_max_block_range`.
1353        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        // Fetch events for each chunk.
1368        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                // query for deposit events, loop until successful.
1375                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    /// Check if the given address is a proxy contract.
1401    pub async fn is_proxy_contract(&self, proxy_address: Address) -> anyhow::Result<bool> {
1402        // confirm that the proxy_address is a proxy
1403        // using the implementation slot, 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc, which is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1
1404        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        // when the implementation address is not equal to zero, it's a proxy
1416        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/// Applies `head` to `state` if it advances the snapshot, updating the metric and broadcasting
1495/// `NewHead`. Never lowers the head.
1496#[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    // Emit an event about the new L1 head. Ignore send errors; it just means no one is
1510    // listening to events right now.
1511    sender
1512        .broadcast_direct(L1Event::NewHead { head })
1513        .await
1514        .ok();
1515}
1516
1517/// Applies `finalized` to `state` if it advances the snapshot, updating the metric and
1518/// broadcasting `NewFinalized`. Never lowers the finalized block.
1519///
1520/// Also raises the head to the finalized block's number if the head is behind it, since a
1521/// finalized block always exists on L1 and thus is truthfully also the head, or below it.
1522#[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/// Bounded by `timeout`, so a provider that never answers can't park a caller indefinitely.
1550#[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        // This can happen in rare cases where the L1 chain is very young and has not finalized a
1560        // block yet. This is more common in testing and demo environments. In any case, we proceed
1561        // with a null L1 block rather than wait for the L1 to finalize a block, which can take a
1562        // long time. Callers log at a level appropriate to their context.
1563        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    /// JSON-RPC bodies recovered from production log dumps, except where marked synthetic. The
1593    /// alchemy bodies are raw captures (including the `***` redaction applied by the telemetry
1594    /// pipeline); the infura envelopes are reconstructed around the exact code/message/data
1595    /// captured.
1596    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        /// Synthetic: telemetry has never captured a JSON-RPC body carrying code 429 from any
1601        /// provider. The message is alchemy's real throughput-limit text; the code is invented,
1602        /// to cover a 429 body that alloy parses and therefore hands back as `Ok`.
1603        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        /// Infura's 429 body is a bare error object, not a JSON-RPC response, so alloy fails to
1605        /// parse it and it arrives as `Err(HttpError)`.
1606        pub const INFURA_RATE_LIMIT: &str = r#"{"code":-32005,"message":"Too Many Requests","data":{"see":"https://infura.io/dashboard"}}"#;
1607        /// Infura's per-second rate limit, as a well-formed JSON-RPC response, so alloy parses it
1608        /// and it arrives as `Ok`. Not a production capture: body from
1609        /// <https://github.com/INFURA/infura/issues/201>.
1610        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        /// What infura returns for the rest of the UTC day once the credit quota is spent. Not a
1612        /// production capture: the message is the one alloy's `is_retry_err` special-cases as
1613        /// "thrown by infura if out of budget for the day and ratelimited".
1614        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        /// Infura load-balancer artifact: the node that served the request had not yet seen the
1616        /// head block. Not a production capture: the message is the one alloy's `is_retry_err`
1617        /// special-cases as "a load balancer issue".
1618        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    /// A block-range cap is a plan limit, not a request property: a paid backup serves the
1646    /// identical request, so this must stay scored and fail over.
1647    #[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    /// The shape a node hits when its infura credits run out: 429 for the rest of the UTC day.
1672    /// `RateLimited` rather than `Failed` keeps the backoff, and `MAX_CONSECUTIVE_RATE_LIMITS`
1673    /// still gets the node onto a working provider.
1674    #[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    /// Deferring to alloy's matcher pulls this in, and `RateLimited` is the handling it wants:
1683    /// the node that answered is behind the head, so back off instead of hammering it, and fail
1684    /// over if it stays behind past the backoff.
1685    #[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    // Same -32005 as the result-count rejection, and it parses, so only the message separates
1694    // this from a request the backup would reject too.
1695    #[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    // Same -32005 as infura's rate limit, different message: a result-count rejection is the
1709    // request's fault, not the provider's.
1710    #[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    // No captured provider response has ever carried JSON-RPC code 429 (alchemy sends -32600,
1727    // infura sends -32005); `ErrorResp` is not rate-limit signal here regardless of code.
1728    #[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    /// The configured delay is operator-set, so it is used as-is even beyond the cap.
1791    #[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        // a provider that holds both anvil (to avoid accidental drop) and wallet-enabled L1 provider
1925        let provider = AnvilProvider::new(inner_provider, Arc::new(anvil));
1926        // cache store for deployed contracts
1927        let mut contracts = Contracts::new();
1928
1929        // init and kick off the L1Client which wraps around standard L1 provider with more app-specific state management
1930        let l1_client = new_l1_client(provider.anvil(), false).await;
1931
1932        // Initialize a contract with some deposits
1933        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        // make some deposits.
1938        for n in 1..=num_deposits {
1939            // Varied amounts are less boring.
1940            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        // Set prev deposits to `None` so `Filter` will start at block
1955        // 0. The test would also succeed if we pass `0` (b/c first
1956        // block did not deposit).
1957        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        // check a few more cases
1971        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        // what happens if `new_finalized` is `0`?
2001        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        // Wait for a block 10 blocks in the future.
2015        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        // Compare against underlying provider.
2020        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        // Wait for anvil to finalize a few blocks.
2056        l1_client.wait_for_finalized_block(2).await;
2057
2058        // Get an old finalized block.
2059        let block = l1_client.wait_for_finalized_block(0).await;
2060
2061        // Compare against underlying provider.
2062        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        // Wait for a block 5 blocks in the future.
2082        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        // Compare against underlying provider.
2105        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        // Get the timestamp of the first block.
2132        let true_block = l1_client.wait_for_finalized_block(0).await;
2133        let timestamp = true_block.timestamp;
2134
2135        // Wait for some more blocks to be produced.
2136        l1_client.wait_for_finalized_block(10).await;
2137
2138        // Get the old block by timestamp.
2139        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        // Wait for a block 10 blocks in the future.
2161        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        // One slot per epoch advances finalization quickly; five blocks is comfortably enough.
2207        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    /// An RPC that pruned old history returns null for block `number`; waiting for a later
2226    /// finalized block must not request it.
2227    #[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        // Margin 1 routes `pruned_number` through the by-number fetch, so a regression shows in
2236        // the counter.
2237        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    /// Proxies every JSON-RPC request to `target` except `eth_getBlockByNumber` with a numeric
2274    /// block, which returns null as on an RPC that pruned old history, counting those requests.
2275    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    /// Never responds, to prove a hung L1 RPC can't stall a bounded call indefinitely.
2309    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    /// `throttle_refresh` must bound `Head` and `Finalized` independently, so a wait on one kind
2343    /// can't starve a wait on the other.
2344    ///
2345    /// `throttle_refresh` gates on `std::time::Instant`, not tokio time, so this uses a short
2346    /// configured interval and a real sleep rather than `tokio::time::pause`/`advance`.
2347    #[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    /// Wraps `result` in a JSON-RPC 2.0 success envelope for `id`.
2383    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    /// Answers every JSON-RPC request with head `0x0`, counting how many requests it receives.
2388    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    /// A refresh hitting `Rate limit exceeded` must not panic or busy-loop.
2465    #[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        // The rate-limited provider must stop receiving requests once refresh fails over to the
2502        // healthy one: `MAX_CONSECUTIVE_RATE_LIMITS` requests trigger the failover, then every
2503        // later tick goes to anvil instead.
2504        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    /// Must not trip `put_finalized`'s assertion that a finalized block is already known.
2512    #[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        // Head already covers the finalized height, so this test exercises only the finalized
2612        // update; head-raising is covered by `test_apply_finalized_raises_head_to_match`.
2613        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        // Use port 0 to let OS assign a port, avoiding race conditions
2678        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        // Check the state is updating.
2686        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        // Disconnect the WebSocket and reconnect it. Technically this spawns a whole new Anvil
2701        // chain, but for the purposes of this test it should look to the client like an L1 server
2702        // closing a WebSocket connection.
2703        drop(anvil);
2704
2705        // Let the connection stay down for a little while: Ethers internally tries to reconnect,
2706        // and starting up to fast again might hit that and cause a false positive. The problem is,
2707        // Ethers doesn't try very hard, and if we wait a bit, we will test the worst possible case
2708        // where the internal retry logic gives up and just kills the whole provider.
2709        tracing::info!("sleep 5");
2710        sleep(Duration::from_secs(5)).await;
2711
2712        // Once a connection is reestablished, the state will eventually start to update again.
2713        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_log::test(tokio::test(flavor = "multi_thread"))]
2742    // async fn test_fetch_stake_table() -> anyhow::Result<()> {
2743
2744    //     let anvil = Anvil::new().spawn();
2745    //     let wallet = anvil.wallet().unwrap();
2746    //     let inner_provider = ProviderBuilder::new()
2747    //         .wallet(wallet)
2748    //         .on_http(anvil.endpoint_url());
2749    //     let provider = AnvilProvider::new(inner_provider, Arc::new(anvil));
2750
2751    //     let l1_client = new_l1_client(provider.anvil(), false).await;
2752    //     let mut contracts = Contracts::new();
2753
2754    //     let stake_table_addr =
2755    //         deploy_permissioned_stake_table(&provider, &mut contracts, vec![]).await?;
2756    //     let stake_table_contract = PermissionedStakeTable::new(stake_table_addr, &provider);
2757
2758    //     let mut rng = rand::thread_rng();
2759    //     let node = NodeInfoSol::rand(&mut rng);
2760
2761    //     let new_nodes: Vec<NodeInfoSol> = vec![node];
2762    //     stake_table_contract
2763    //         .update(vec![], new_nodes)
2764    //         .send()
2765    //         .await?
2766    //         .watch()
2767    //         .await?;
2768
2769    //     let block = l1_client.get_block(BlockId::latest()).await?.unwrap();
2770    //     let nodes = l1_client
2771    //         .get_stake_table(stake_table_addr, block.header.inner.number)
2772    //         .await?;
2773
2774    //     let result = nodes.stake_table.0[0].clone();
2775    //     assert_eq!(result.stake_table_entry.stake_amount.to::<u64>(), 1);
2776    //     Ok(())
2777    // }
2778
2779    /// Requests already in flight when the backoff window opened are not new evidence about the
2780    /// provider: without this, two concurrent callers fail over on a single burst.
2781    #[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    /// The window elapsing between them is what makes rate limits count towards a failover.
2794    #[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    /// A success in between clears the count, so rate limits a provider recovers from never
2805    /// accumulate into a failover.
2806    #[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    /// A helper function to get the index of the current provider in the failover list.
2818    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    /// A provider that always answers with a parseable JSON-RPC error must be scored unhealthy
2824    /// and trigger failover, not be treated as healthy (see [`ResponseOutcome`]).
2825    #[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    /// A rate-limited provider that a failover-happy config would abandon on the first error.
2861    /// The backoff is long enough that every call after the first short-circuits client-side.
2862    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    /// A rate limit backs off on the current provider instead of failing over.
2884    #[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        // Inside the backoff window, so this never leaves the client, and still does not fail
2896        // over even though a single failure would.
2897        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    /// A provider still rate limiting after its own backoff is treated as failed. Otherwise
2906    /// `rate_limited_until` short-circuits every later request and the healthy backup, which the
2907    /// client already holds, is never reached.
2908    #[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    /// A range rejection is the request's fault: every backup rejects it identically, so it must
2924    /// not burn a generation. `l1_consecutive_failure_tolerance: 1` fails over on any scored
2925    /// failure, so a stable index is the whole assertion.
2926    #[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        // Create an L1 client with fake providers, and check that the state is still updated after
2953        // it correctly fails over to the real providers.
2954        let client = L1ClientOptions {
2955            l1_polling_interval: Duration::from_secs(1),
2956            // Use a very long subscription timeout, so that we only succeed by triggering a
2957            // failover.
2958            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        // Check the state is updating.
2981        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        // Make just enough failed requests not to trigger a failover.
3025        for _ in 0..2 {
3026            provider.get_block_number().await.unwrap_err();
3027            assert!(get_failover_index(&provider) == 0);
3028        }
3029
3030        // The final request triggers failover.
3031        provider.get_block_number().await.unwrap_err();
3032        assert!(get_failover_index(&provider) == 1);
3033
3034        // Now requests succeed.
3035        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        // Two failed requests that are not within the tolerance window do not trigger a failover.
3053        provider.get_block_number().await.unwrap_err();
3054        sleep(Duration::from_secs(1)).await;
3055        provider.get_block_number().await.unwrap_err();
3056
3057        // Check that we didn't fail over.
3058        assert!(get_failover_index(&provider) == 0);
3059
3060        // Reset the window.
3061        sleep(Duration::from_secs(1)).await;
3062
3063        // Two failed requests in a row trigger failover.
3064        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        // The first request fails and triggers a failover.
3086        provider.get_block_number().await.unwrap_err();
3087        assert_eq!(get_failover_index(&provider), 1);
3088
3089        // The next request succeeds from the other provider.
3090        provider.get_block_number().await.unwrap();
3091
3092        // Eventually we revert back to the primary and requests fail again.
3093        sleep(Duration::from_millis(2100)).await;
3094        provider.get_block_number().await.unwrap_err();
3095    }
3096
3097    /// Proxies every JSON-RPC request to `target` except `eth_getBlockByNumber("finalized", ..)`,
3098    /// which always errors. Lets a test drive a real anvil chain while forcing the finalized RPC
3099    /// to fail.
3100    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    // Checks that the L1 client initialized the state on startup even
3164    // if the L1 is not currently mining blocks. It's useful for testing that we
3165    // don't require an L1 that is continuously mining blocks.
3166    #[test_log::test(tokio::test(flavor = "multi_thread"))]
3167    async fn test_update_loop_initializes_l1_state() {
3168        // Use port 0 to let OS assign a port, avoiding race conditions
3169        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}