Skip to main content

espresso_node/
catchup.rs

1use std::{
2    cmp::Ordering,
3    collections::HashMap,
4    fmt::{Debug, Display},
5    sync::Arc,
6    time::Duration,
7};
8
9use anyhow::{Context, anyhow, bail, ensure};
10use async_lock::RwLock;
11use async_trait::async_trait;
12use committable::{Commitment, Committable};
13use espresso_api::routes::v1 as paths;
14use espresso_types::{
15    BackoffParams, BlockMerkleCommitment, BlockMerkleTree, Certificate2, FeeAccount,
16    FeeAccountProof, FeeMerkleCommitment, FeeMerkleTree, Leaf2, NodeState, SeqTypes,
17    ValidatedState,
18    config::PublicNetworkConfig,
19    v0::traits::StateCatchup,
20    v0_3::{
21        ChainConfig, RewardAccountProofV1, RewardAccountV1, RewardMerkleCommitmentV1,
22        RewardMerkleTreeV1,
23    },
24    v0_4::{
25        PermittedRewardMerkleTreeV2, RewardAccountProofV2, RewardAccountV2,
26        RewardMerkleCommitmentV2, RewardMerkleTreeV2, forgotten_accounts_include,
27    },
28};
29use futures::{
30    StreamExt,
31    future::{Future, FutureExt, TryFuture, TryFutureExt},
32    stream::FuturesUnordered,
33};
34use hotshot_new_protocol::utils::verify_new_protocol_leaf_chain;
35use hotshot_types::{
36    ValidatorConfig,
37    data::ViewNumber,
38    epoch_membership::EpochMembershipCoordinator,
39    message::UpgradeLock,
40    network::NetworkConfig,
41    simple_certificate::LightClientStateUpdateCertificateV2,
42    traits::{
43        ValidatedState as ValidatedStateTrait,
44        metrics::{Counter, CounterFamily, Metrics},
45    },
46    utils::verify_leaf_chain,
47};
48use http_client::{Request, error::ClientErr};
49use itertools::Itertools;
50use jf_merkle_tree_compat::{ForgetableMerkleTreeScheme, MerkleTreeScheme, prelude::MerkleNode};
51use parking_lot::Mutex;
52use priority_queue::PriorityQueue;
53use serde::de::DeserializeOwned;
54use tokio::time::{sleep, timeout};
55use tokio_util::task::AbortOnDropHandle;
56use url::Url;
57use vbs::version::StaticVersionType;
58use versions::{EPOCH_VERSION, NEW_PROTOCOL_VERSION};
59
60use crate::api::{
61    BlocksFrontier, RewardMerkleTreeDataSource, RewardMerkleTreeV2Data, context::ConsensusSource,
62};
63
64// This newtype is probably not worth having. It's only used to be able to log
65// URLs before doing requests.
66#[derive(Debug, Clone)]
67struct Client<ApiVer: StaticVersionType> {
68    inner: http_client::Client<ClientErr, ApiVer>,
69    url: Url,
70    requests: Arc<Box<dyn Counter>>,
71    failures: Arc<Box<dyn Counter>>,
72}
73
74impl<ApiVer: StaticVersionType> Client<ApiVer> {
75    pub fn new(
76        url: Url,
77        requests: &(impl CounterFamily + ?Sized),
78        failures: &(impl CounterFamily + ?Sized),
79    ) -> Self {
80        Self {
81            inner: http_client::Client::new(url.clone()),
82            requests: Arc::new(requests.create(vec![url.to_string()])),
83            failures: Arc::new(failures.create(vec![url.to_string()])),
84            url,
85        }
86    }
87
88    pub fn get<T: DeserializeOwned>(&self, route: &str) -> Request<T, ClientErr, ApiVer> {
89        self.inner.get(route)
90    }
91}
92
93/// A score of a catchup peer, based on our interactions with that peer.
94///
95/// The score accounts for malicious peers -- i.e. peers that gave us an invalid response to a
96/// verifiable request -- and faulty/unreliable peers -- those that fail to respond to requests at
97/// all. The score has a comparison function where higher is better, or in other words `p1 > p2`
98/// means we believe we are more likely to successfully catch up using `p1` than `p2`. This makes it
99/// convenient and efficient to collect peers in a priority queue which we can easily convert to a
100/// list sorted by reliability.
101#[derive(Clone, Copy, Debug, Default)]
102struct PeerScore {
103    requests: usize,
104    failures: usize,
105}
106
107impl Ord for PeerScore {
108    fn cmp(&self, other: &Self) -> Ordering {
109        // Compare failure rates: `self` is better than `other` if
110        //      self.failures / self.requests < other.failures / other.requests
111        // or equivalently
112        //      other.failures * self.requests > self.failures * other.requests
113        (other.failures * self.requests).cmp(&(self.failures * other.requests))
114    }
115}
116
117impl PartialOrd for PeerScore {
118    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
119        Some(self.cmp(other))
120    }
121}
122
123impl PartialEq for PeerScore {
124    fn eq(&self, other: &Self) -> bool {
125        self.cmp(other).is_eq()
126    }
127}
128
129impl Eq for PeerScore {}
130
131#[derive(Debug, Clone, Default)]
132pub struct StatePeers<ApiVer: StaticVersionType> {
133    // Peer IDs, ordered by reliability score. Each ID is an index into `clients`.
134    scores: Arc<RwLock<PriorityQueue<usize, PeerScore>>>,
135    clients: Vec<Client<ApiVer>>,
136    backoff: BackoffParams,
137    /// Base timeout for per peer catchup request
138    base_timeout: Duration,
139}
140
141impl<ApiVer: StaticVersionType> StatePeers<ApiVer> {
142    async fn fetch<Fut>(
143        &self,
144        retry: usize,
145        f: impl Fn(Client<ApiVer>) -> Fut,
146    ) -> anyhow::Result<Fut::Ok>
147    where
148        Fut: TryFuture<Error: Display>,
149    {
150        // Since we have generally have multiple peers we can catch up from, we want a fairly
151        // aggressive timeout for requests: if a peer is not responding quickly, we're better off
152        // just trying the next one rather than waiting, and this prevents a malicious peer from
153        // delaying catchup for a long time.
154        //
155        // However, if we set the timeout _too_ aggressively, we might fail to catch up even from an
156        // honest peer, and thus never make progress. Thus, we start with a base timeout (default
157        // 2s), which is reasonable for an HTTP request. If that fails with all of our peers, we
158        // increase the timeout by the base amount for each successive retry, until we eventually
159        // succeed. The base timeout is configurable via ESPRESSO_NODE_CATCHUP_BASE_TIMEOUT.
160        let timeout_dur = self.base_timeout * (retry as u32 + 1);
161
162        // Keep track of which peers we make requests to and which succeed (`true`) or fail (`false`),
163        // so we can update reliability scores at the end.
164        let mut requests = HashMap::new();
165        let mut res = Err(anyhow!("failed fetching from every peer"));
166
167        // Try each peer in order of reliability score, until we succeed. We clone out of
168        // `self.scores` because it is small (contains only numeric IDs and scores), so this clone
169        // is a lot cheaper than holding the read lock the entire time we are making requests (which
170        // could be a while).
171        let mut scores = { (*self.scores.read().await).clone() };
172        let mut logs = vec!["Fetching failed.\n".to_string()];
173        while let Some((id, score)) = scores.pop() {
174            let client = &self.clients[id];
175            tracing::info!("fetching from {}", client.url);
176            match timeout(timeout_dur, TryFutureExt::into_future(f(client.clone()))).await {
177                Ok(Ok(t)) => {
178                    requests.insert(id, true);
179                    res = Ok(t);
180                    logs = Vec::new();
181                    break;
182                },
183                Ok(Err(err)) => {
184                    tracing::debug!(id, ?score, peer = %client.url, "error from peer: {err:#}");
185                    logs.push(format!(
186                        "Error from peer {} with id {id} and score {score:?}: {err:#}",
187                        client.url
188                    ));
189                    requests.insert(id, false);
190                },
191                Err(_) => {
192                    tracing::debug!(id, ?score, peer = %client.url, ?timeout_dur, "request timed out");
193                    logs.push(format!(
194                        "Error from peer {} with id {id} and score {score:?}: request timed out",
195                        client.url
196                    ));
197                    requests.insert(id, false);
198                },
199            }
200        }
201
202        if !logs.is_empty() {
203            tracing::warn!("{}", logs.join("\n"));
204        }
205
206        // Update client scores.
207        let mut scores = self.scores.write().await;
208        for (id, success) in requests {
209            scores.change_priority_by(&id, |score| {
210                score.requests += 1;
211                self.clients[id].requests.add(1);
212                if !success {
213                    score.failures += 1;
214                    self.clients[id].failures.add(1);
215                }
216            });
217        }
218
219        res
220    }
221
222    pub fn from_urls(
223        urls: Vec<Url>,
224        backoff: BackoffParams,
225        base_timeout: Duration,
226        metrics: &(impl Metrics + ?Sized),
227    ) -> Self {
228        if urls.is_empty() {
229            panic!("Cannot create StatePeers with no peers");
230        }
231
232        let metrics = metrics.subgroup("catchup".into());
233        let requests = metrics.counter_family("requests".into(), vec!["peer".into()]);
234        let failures = metrics.counter_family("request_failures".into(), vec!["peer".into()]);
235
236        let scores = urls
237            .iter()
238            .enumerate()
239            .map(|(i, _)| (i, PeerScore::default()))
240            .collect();
241        let clients = urls
242            .into_iter()
243            .map(|url| Client::new(url, &*requests, &*failures))
244            .collect();
245
246        Self {
247            clients,
248            scores: Arc::new(RwLock::new(scores)),
249            backoff,
250            base_timeout,
251        }
252    }
253
254    #[tracing::instrument(skip(self, my_own_validator_config))]
255    pub async fn fetch_config(
256        &self,
257        my_own_validator_config: ValidatorConfig<SeqTypes>,
258    ) -> anyhow::Result<NetworkConfig<SeqTypes>> {
259        self.backoff()
260            .retry(self, move |provider, retry| {
261                let my_own_validator_config = my_own_validator_config.clone();
262                async move {
263                    let cfg: PublicNetworkConfig = provider
264                        .fetch(retry, |client| {
265                            let url = client.url.join(&paths::config_hotshot()).unwrap();
266
267                            reqwest::get(url.clone())
268                        })
269                        .await?
270                        .json()
271                        .await?;
272                    cfg.into_network_config(my_own_validator_config)
273                        .context("fetched config, but failed to convert to private config")
274                }
275                .boxed()
276            })
277            .await
278    }
279}
280
281/// Verify a legacy (pre-V6) leaf chain
282pub(crate) async fn verify_legacy_leaf_chain(
283    leaf_chain: Vec<Leaf2>,
284    coordinator: &EpochMembershipCoordinator<SeqTypes>,
285    height: u64,
286) -> anyhow::Result<Leaf2> {
287    let upgrade_lock = UpgradeLock::<SeqTypes>::new(versions::Upgrade::trivial(EPOCH_VERSION));
288    verify_leaf_chain(leaf_chain, coordinator, height, &upgrade_lock)
289        .await
290        .with_context(|| format!("failed to verify leaf chain at height {height}"))
291}
292
293#[async_trait]
294impl<ApiVer: StaticVersionType> StateCatchup for StatePeers<ApiVer> {
295    #[tracing::instrument(skip(self, _instance))]
296    async fn try_fetch_accounts(
297        &self,
298        retry: usize,
299        _instance: &NodeState,
300        height: u64,
301        view: ViewNumber,
302        fee_merkle_tree_root: FeeMerkleCommitment,
303        accounts: &[FeeAccount],
304    ) -> anyhow::Result<Vec<FeeAccountProof>> {
305        self.fetch(retry, |client| async move {
306            let tree = client
307                .inner
308                .post::<FeeMerkleTree>(&paths::catchup_accounts(height, view.u64()))
309                .body_binary(&accounts.to_vec())?
310                .send()
311                .await?;
312
313            // Verify proofs.
314            let mut proofs = Vec::new();
315            for account in accounts {
316                let (proof, _) = FeeAccountProof::prove(&tree, (*account).into())
317                    .context(format!("response missing fee account {account}"))?;
318                proof.verify(&fee_merkle_tree_root).context(format!(
319                    "invalid proof for fee account {account}, root: {fee_merkle_tree_root}"
320                ))?;
321                proofs.push(proof);
322            }
323
324            anyhow::Ok(proofs)
325        })
326        .await
327    }
328
329    #[tracing::instrument(skip(self, _instance, mt))]
330    async fn try_remember_blocks_merkle_tree(
331        &self,
332        retry: usize,
333        _instance: &NodeState,
334        height: u64,
335        view: ViewNumber,
336        mt: &mut BlockMerkleTree,
337    ) -> anyhow::Result<()> {
338        *mt = self
339            .fetch(retry, |client| {
340                let mut mt = mt.clone();
341                async move {
342                    let frontier = client
343                        .get::<BlocksFrontier>(&paths::catchup_blocks(height, view.u64()))
344                        .send()
345                        .await?;
346                    let elem = frontier
347                        .elem()
348                        .context("provided frontier is missing leaf element")?;
349                    mt.remember(mt.num_leaves() - 1, *elem, &frontier)
350                        .context("verifying block proof")?;
351                    anyhow::Ok(mt)
352                }
353            })
354            .await?;
355        Ok(())
356    }
357
358    async fn try_fetch_chain_config(
359        &self,
360        retry: usize,
361        commitment: Commitment<ChainConfig>,
362    ) -> anyhow::Result<ChainConfig> {
363        self.fetch(retry, |client| async move {
364            let cf = client
365                .get::<ChainConfig>(&paths::catchup_chainconfig(commitment))
366                .send()
367                .await?;
368            ensure!(
369                cf.commit() == commitment,
370                "received chain config with mismatched commitment: expected {commitment}, got {}",
371                cf.commit()
372            );
373            Ok(cf)
374        })
375        .await
376    }
377
378    async fn try_fetch_leaf(
379        &self,
380        retry: usize,
381        coordinator: EpochMembershipCoordinator<SeqTypes>,
382        height: u64,
383    ) -> anyhow::Result<Leaf2> {
384        // Fetch the leaf chain. For new protocol heights this is a leaf range
385        // `[height..=cert2_height]`
386        // for legacy-protocol heights it's a 3-chain.
387        let leaf_chain = self
388            .fetch(retry, |client| async move {
389                let chain = client
390                    .get::<Vec<Leaf2>>(&paths::catchup_leafchain(height))
391                    .send()
392                    .await?;
393                anyhow::Ok(chain)
394            })
395            .await
396            .with_context(|| format!("failed to fetch leaf chain at height {height}"))?;
397
398        let first = leaf_chain
399            .first()
400            .ok_or_else(|| anyhow!("empty leaf chain returned for height {height}"))?;
401
402        if first.block_header().version() >= NEW_PROTOCOL_VERSION {
403            let upgrade_lock =
404                UpgradeLock::<SeqTypes>::new(versions::Upgrade::trivial(NEW_PROTOCOL_VERSION));
405
406            // The chain terminates at the leaf the cert2 finalizes, so fetch that cert2 by its
407            // exact height
408            let cert2_height = leaf_chain
409                .last()
410                .ok_or_else(|| anyhow!("empty leaf chain returned for height {height}"))?
411                .height();
412            let cert2 = self
413                .fetch(retry, |client| async move {
414                    let cert2 = client
415                        .get::<Certificate2<SeqTypes>>(&paths::catchup_cert2(cert2_height))
416                        .send()
417                        .await?;
418                    anyhow::Ok(cert2)
419                })
420                .await
421                .with_context(|| format!("failed to fetch cert2 for height {cert2_height}"))?;
422
423            verify_new_protocol_leaf_chain(leaf_chain, &coordinator, height, &upgrade_lock, cert2)
424                .await
425                .with_context(|| {
426                    format!("failed to verify leaf chain with cert2 at height {height}")
427                })
428        } else {
429            verify_legacy_leaf_chain(leaf_chain, &coordinator, height).await
430        }
431    }
432
433    async fn try_fetch_reward_merkle_tree_v2(
434        &self,
435        retry: usize,
436        height: u64,
437        view: ViewNumber,
438        reward_merkle_tree_root: RewardMerkleCommitmentV2,
439        accounts: Arc<Vec<RewardAccountV2>>,
440    ) -> anyhow::Result<PermittedRewardMerkleTreeV2> {
441        let result = self
442            .fetch(retry, |client| async move {
443                // Try the catchup endpoint first which returns tree from consensuss decided state
444                // if not present, then fall back to
445                // the reward-state-v2 endpoint which returns from storage decided state
446                let tree_bytes = match client
447                    .inner
448                    .get::<Vec<u8>>(&paths::catchup_reward_merkle_tree_v2(height, *view))
449                    .send()
450                    .await
451                {
452                    Ok(bytes) => bytes,
453                    Err(err) => {
454                        tracing::info!(
455                            "catchup endpoint failed, falling back to reward-state-v2: {err:#}"
456                        );
457                        client
458                            .inner
459                            .get::<Vec<u8>>(&paths::reward_merkle_tree_v2(height))
460                            .send()
461                            .await?
462                    },
463                };
464
465                Ok::<Vec<u8>, anyhow::Error>(tree_bytes)
466            })
467            .await
468            .context("Fetching from peer failed")?;
469
470        let tree_data = bincode::deserialize::<RewardMerkleTreeV2Data>(&result)
471            .context("Failed to deserialize merkle tree from catchup")?;
472
473        let tree: PermittedRewardMerkleTreeV2 =
474            PermittedRewardMerkleTreeV2::try_from_kv_set(tree_data.balances).await?;
475
476        ensure!(
477            tree.tree.commitment() == reward_merkle_tree_root,
478            "RewardMerkleTreeV2 from peer failed commitment check."
479        );
480        ensure!(!forgotten_accounts_include(&tree, &accounts));
481
482        Ok(tree)
483    }
484
485    #[tracing::instrument(skip(self, _instance))]
486    async fn try_fetch_reward_accounts_v1(
487        &self,
488        retry: usize,
489        _instance: &NodeState,
490        height: u64,
491        view: ViewNumber,
492        reward_merkle_tree_root: RewardMerkleCommitmentV1,
493        accounts: &[RewardAccountV1],
494    ) -> anyhow::Result<Vec<RewardAccountProofV1>> {
495        self.fetch(retry, |client| async move {
496            let tree = client
497                .inner
498                .post::<RewardMerkleTreeV1>(&paths::catchup_reward_accounts(height, view.u64()))
499                .body_binary(&accounts.to_vec())?
500                .send()
501                .await?;
502
503            // Verify proofs.
504            let mut proofs = Vec::new();
505            for account in accounts {
506                let (proof, _) = RewardAccountProofV1::prove(&tree, (*account).into())
507                    .context(format!("response missing reward account {account}"))?;
508                proof.verify(&reward_merkle_tree_root).context(format!(
509                    "invalid proof for v1 reward account {account}, root: \
510                     {reward_merkle_tree_root} height {height} view {view}"
511                ))?;
512                proofs.push(proof);
513            }
514
515            anyhow::Ok(proofs)
516        })
517        .await
518    }
519
520    async fn try_fetch_state_cert(
521        &self,
522        retry: usize,
523        epoch: u64,
524    ) -> anyhow::Result<LightClientStateUpdateCertificateV2<SeqTypes>> {
525        self.fetch(retry, |client| async move {
526            client
527                .get::<LightClientStateUpdateCertificateV2<SeqTypes>>(&paths::catchup_state_cert(
528                    epoch,
529                ))
530                .send()
531                .await
532        })
533        .await
534    }
535
536    fn backoff(&self) -> &BackoffParams {
537        &self.backoff
538    }
539
540    fn name(&self) -> String {
541        format!(
542            "StatePeers({})",
543            self.clients
544                .iter()
545                .map(|client| client.url.to_string())
546                .join(",")
547        )
548    }
549
550    fn is_local(&self) -> bool {
551        false
552    }
553}
554
555pub(crate) trait CatchupStorage: Sync {
556    /// Get the state of the requested `accounts`.
557    ///
558    /// The state is fetched from a snapshot at the given height and view, which _must_ correspond!
559    /// `height` is provided to simplify lookups for backends where data is not indexed by view.
560    /// This function is intended to be used for catchup, so `view` should be no older than the last
561    /// decided view.
562    ///
563    /// If successful, this function also returns the leaf from `view`, if it is available. This can
564    /// be used to add the recovered state to HotShot's state map, so that future requests can get
565    /// the state from memory rather than storage.
566    fn get_accounts(
567        &self,
568        _instance: &NodeState,
569        _height: u64,
570        _view: ViewNumber,
571        _accounts: &[FeeAccount],
572    ) -> impl Send + Future<Output = anyhow::Result<(FeeMerkleTree, Leaf2)>> {
573        // Merklized state catchup is only supported by persistence backends that provide merklized
574        // state storage. This default implementation is overridden for those that do. Otherwise,
575        // catchup can still be provided by fetching undecided merklized state from consensus
576        // memory.
577        async {
578            bail!("merklized state catchup is not supported for this data source");
579        }
580    }
581
582    /// Get the state of the requested `accounts` from the snapshot at `height` whose root is
583    /// `root`.
584    ///
585    /// Unlike [`get_accounts`](Self::get_accounts), this does not read the leaf or header at
586    /// `height`, so it works after the data pruner deleted them, as long as the snapshot is
587    /// stored.
588    fn get_accounts_at(
589        &self,
590        _height: u64,
591        _root: FeeMerkleCommitment,
592        _accounts: &[FeeAccount],
593    ) -> impl Send + Future<Output = anyhow::Result<FeeMerkleTree>> {
594        async {
595            bail!("merklized state catchup is not supported for this data source");
596        }
597    }
598
599    fn get_reward_accounts_v1(
600        &self,
601        _instance: &NodeState,
602        _height: u64,
603        _view: ViewNumber,
604        _accounts: &[RewardAccountV1],
605    ) -> impl Send + Future<Output = anyhow::Result<(RewardMerkleTreeV1, Leaf2)>> {
606        async {
607            bail!("merklized state catchup is not supported for this data source");
608        }
609    }
610
611    fn get_reward_accounts_v2(
612        &self,
613        _instance: &NodeState,
614        _height: u64,
615        _view: ViewNumber,
616        _accounts: &[RewardAccountV2],
617    ) -> impl Send + Future<Output = anyhow::Result<(RewardMerkleTreeV2, Leaf2)>> {
618        async {
619            bail!("merklized state catchup is not supported for this data source");
620        }
621    }
622
623    /// Get the blocks Merkle tree frontier.
624    ///
625    /// The state is fetched from a snapshot at the given height and view, which _must_ correspond!
626    /// `height` is provided to simplify lookups for backends where data is not indexed by view.
627    /// This function is intended to be used for catchup, so `view` should be no older than the last
628    /// decided view.
629    fn get_frontier(
630        &self,
631        _instance: &NodeState,
632        _height: u64,
633        _view: ViewNumber,
634    ) -> impl Send + Future<Output = anyhow::Result<BlocksFrontier>> {
635        // Merklized state catchup is only supported by persistence backends that provide merklized
636        // state storage. This default implementation is overridden for those that do. Otherwise,
637        // catchup can still be provided by fetching undecided merklized state from consensus
638        // memory.
639        async {
640            bail!("merklized state catchup is not supported for this data source");
641        }
642    }
643
644    /// Get the blocks Merkle tree frontier from the snapshot at `height` whose root is `root`.
645    ///
646    /// Unlike [`get_frontier`](Self::get_frontier), this does not read the header at `height`, so
647    /// it works after the data pruner deleted it, as long as the snapshot is stored.
648    fn get_frontier_at(
649        &self,
650        _height: u64,
651        _root: BlockMerkleCommitment,
652    ) -> impl Send + Future<Output = anyhow::Result<BlocksFrontier>> {
653        async {
654            bail!("merklized state catchup is not supported for this data source");
655        }
656    }
657
658    fn get_chain_config(
659        &self,
660        _commitment: Commitment<ChainConfig>,
661    ) -> impl Send + Future<Output = anyhow::Result<ChainConfig>> {
662        async {
663            bail!("chain config catchup is not supported for this data source");
664        }
665    }
666
667    fn get_leaf_chain(
668        &self,
669        _height: u64,
670    ) -> impl Send + Future<Output = anyhow::Result<Vec<Leaf2>>> {
671        async {
672            bail!("leaf chain catchup is not supported for this data source");
673        }
674    }
675
676    /// Load the cert2 stored at exactly `height`, if one exists.
677    fn load_cert2(
678        &self,
679        _height: u64,
680    ) -> impl Send + Future<Output = anyhow::Result<Option<Certificate2<SeqTypes>>>> {
681        async { Ok(None) }
682    }
683
684    /// Load a decided leaf at the given height.
685    fn get_leaf(&self, _height: u64) -> impl Send + Future<Output = anyhow::Result<Leaf2>> {
686        async {
687            bail!("leaf fetch is not supported for this data source");
688        }
689    }
690
691    /// The serialized tree the header at `height` commits to.
692    fn load_serialized_reward_merkle_tree_v2(
693        &self,
694        _height: u64,
695    ) -> impl Send + Future<Output = anyhow::Result<Vec<u8>>> {
696        async {
697            bail!("reward merkle tree is not supported for this data source");
698        }
699    }
700}
701
702impl CatchupStorage for hotshot_query_service::data_source::MetricsDataSource {}
703
704impl<T, S> CatchupStorage for hotshot_query_service::data_source::ExtensibleDataSource<T, S>
705where
706    T: CatchupStorage,
707    S: Sync,
708{
709    async fn get_accounts(
710        &self,
711        instance: &NodeState,
712        height: u64,
713        view: ViewNumber,
714        accounts: &[FeeAccount],
715    ) -> anyhow::Result<(FeeMerkleTree, Leaf2)> {
716        self.inner()
717            .get_accounts(instance, height, view, accounts)
718            .await
719    }
720
721    async fn get_accounts_at(
722        &self,
723        height: u64,
724        root: FeeMerkleCommitment,
725        accounts: &[FeeAccount],
726    ) -> anyhow::Result<FeeMerkleTree> {
727        self.inner().get_accounts_at(height, root, accounts).await
728    }
729
730    async fn get_reward_accounts_v2(
731        &self,
732        instance: &NodeState,
733        height: u64,
734        view: ViewNumber,
735        accounts: &[RewardAccountV2],
736    ) -> anyhow::Result<(RewardMerkleTreeV2, Leaf2)> {
737        self.inner()
738            .get_reward_accounts_v2(instance, height, view, accounts)
739            .await
740    }
741
742    async fn get_reward_accounts_v1(
743        &self,
744        instance: &NodeState,
745        height: u64,
746        view: ViewNumber,
747        accounts: &[RewardAccountV1],
748    ) -> anyhow::Result<(RewardMerkleTreeV1, Leaf2)> {
749        self.inner()
750            .get_reward_accounts_v1(instance, height, view, accounts)
751            .await
752    }
753
754    async fn get_frontier(
755        &self,
756        instance: &NodeState,
757        height: u64,
758        view: ViewNumber,
759    ) -> anyhow::Result<BlocksFrontier> {
760        self.inner().get_frontier(instance, height, view).await
761    }
762
763    async fn get_frontier_at(
764        &self,
765        height: u64,
766        root: BlockMerkleCommitment,
767    ) -> anyhow::Result<BlocksFrontier> {
768        self.inner().get_frontier_at(height, root).await
769    }
770
771    async fn get_chain_config(
772        &self,
773        commitment: Commitment<ChainConfig>,
774    ) -> anyhow::Result<ChainConfig> {
775        self.inner().get_chain_config(commitment).await
776    }
777    async fn get_leaf_chain(&self, height: u64) -> anyhow::Result<Vec<Leaf2>> {
778        self.inner().get_leaf_chain(height).await
779    }
780
781    async fn load_cert2(&self, height: u64) -> anyhow::Result<Option<Certificate2<SeqTypes>>> {
782        self.inner().load_cert2(height).await
783    }
784
785    async fn get_leaf(&self, height: u64) -> anyhow::Result<Leaf2> {
786        self.inner().get_leaf(height).await
787    }
788
789    async fn load_serialized_reward_merkle_tree_v2(&self, height: u64) -> anyhow::Result<Vec<u8>> {
790        self.inner()
791            .load_serialized_reward_merkle_tree_v2(height)
792            .await
793    }
794}
795
796#[derive(Debug)]
797pub(crate) struct SqlStateCatchup<T> {
798    db: Arc<T>,
799    backoff: BackoffParams,
800}
801
802impl<T> SqlStateCatchup<T> {
803    pub(crate) fn new(db: Arc<T>, backoff: BackoffParams) -> Self {
804        Self { db, backoff }
805    }
806}
807
808#[async_trait]
809impl<T> StateCatchup for SqlStateCatchup<T>
810where
811    T: CatchupStorage + RewardMerkleTreeDataSource + Send + Sync,
812{
813    async fn try_fetch_leaf(
814        &self,
815        _retry: usize,
816        _coordinator: EpochMembershipCoordinator<SeqTypes>,
817        height: u64,
818    ) -> anyhow::Result<Leaf2> {
819        // Leaves in our local DB were verified before they were stored, so we can return the leaf
820        // at `height` directly without re-verifying.
821        self.db
822            .get_leaf(height)
823            .await
824            .with_context(|| format!("failed to load leaf at height {height} from DB"))
825    }
826
827    // TODO: add a test for the account proof validation
828    // issue # 2102 (https://github.com/EspressoSystems/espresso-network/issues/2102)
829    #[tracing::instrument(skip(self, _retry, instance))]
830    async fn try_fetch_accounts(
831        &self,
832        _retry: usize,
833        instance: &NodeState,
834        block_height: u64,
835        view: ViewNumber,
836        fee_merkle_tree_root: FeeMerkleCommitment,
837        accounts: &[FeeAccount],
838    ) -> anyhow::Result<Vec<FeeAccountProof>> {
839        // Get the accounts. If the data pruner has deleted the leaf and header that `get_accounts`
840        // reads, the snapshot can still be stored, so read it by the root we were given.
841        let fee_merkle_tree_from_db = match self
842            .db
843            .get_accounts(instance, block_height, view, accounts)
844            .await
845        {
846            Ok((tree, _)) => tree,
847            Err(err) => {
848                let tree = self
849                    .db
850                    .get_accounts_at(block_height, fee_merkle_tree_root, accounts)
851                    .await
852                    .with_context(|| format!("failed to get fee accounts from DB: {err:#}"))?;
853                tracing::debug!("get_accounts failed, read the accounts by root instead: {err:#}");
854                tree
855            },
856        };
857
858        // Verify the accounts
859        let mut proofs = Vec::new();
860        for account in accounts {
861            let (proof, _) = FeeAccountProof::prove(&fee_merkle_tree_from_db, (*account).into())
862                .context(format!("response missing account {account}"))?;
863            proof.verify(&fee_merkle_tree_root).context(format!(
864                "invalid proof for fee account {account}, root: {fee_merkle_tree_root}"
865            ))?;
866            proofs.push(proof);
867        }
868
869        Ok(proofs)
870    }
871
872    #[tracing::instrument(skip(self, _retry, instance, mt))]
873    async fn try_remember_blocks_merkle_tree(
874        &self,
875        _retry: usize,
876        instance: &NodeState,
877        bh: u64,
878        view: ViewNumber,
879        mt: &mut BlockMerkleTree,
880    ) -> anyhow::Result<()> {
881        if bh == 0 {
882            return Ok(());
883        }
884
885        // As for accounts, read the snapshot by root if the header `get_frontier` reads is gone.
886        let proof = match self.db.get_frontier(instance, bh, view).await {
887            Ok(proof) => proof,
888            Err(err) => {
889                let proof = self
890                    .db
891                    .get_frontier_at(bh, mt.commitment())
892                    .await
893                    .with_context(|| format!("failed to get frontier from DB: {err:#}"))?;
894                tracing::debug!("get_frontier failed, read the frontier by root instead: {err:#}");
895                proof
896            },
897        };
898        match proof
899            .proof
900            .first()
901            .context(format!("empty proof for frontier at height {bh}"))?
902        {
903            MerkleNode::Leaf { pos, elem, .. } => mt
904                .remember(pos, elem, proof.clone())
905                .context("failed to remember proof"),
906            _ => bail!("invalid proof"),
907        }
908    }
909
910    async fn try_fetch_chain_config(
911        &self,
912        _retry: usize,
913        commitment: Commitment<ChainConfig>,
914    ) -> anyhow::Result<ChainConfig> {
915        let cf = self.db.get_chain_config(commitment).await?;
916
917        if cf.commit() != commitment {
918            panic!(
919                "Critical error: Mismatched chain config detected. Expected chain config: {:?}, \
920                 but got: {:?}.
921                This may indicate a compromised database",
922                commitment,
923                cf.commit()
924            )
925        }
926
927        Ok(cf)
928    }
929
930    async fn try_fetch_reward_merkle_tree_v2(
931        &self,
932        _retry: usize,
933        height: u64,
934        _view: ViewNumber,
935        reward_merkle_tree_root: RewardMerkleCommitmentV2,
936        accounts: Arc<Vec<RewardAccountV2>>,
937    ) -> anyhow::Result<PermittedRewardMerkleTreeV2> {
938        let tree: PermittedRewardMerkleTreeV2 = self.db.load_reward_merkle_tree_v2(height).await?;
939
940        ensure!(tree.tree.commitment() == reward_merkle_tree_root);
941        ensure!(!forgotten_accounts_include(&tree, &accounts));
942
943        Ok(tree)
944    }
945
946    #[tracing::instrument(skip(self, _retry, instance))]
947    async fn try_fetch_reward_accounts_v1(
948        &self,
949        _retry: usize,
950        instance: &NodeState,
951        block_height: u64,
952        view: ViewNumber,
953        reward_merkle_tree_root: RewardMerkleCommitmentV1,
954        accounts: &[RewardAccountV1],
955    ) -> anyhow::Result<Vec<RewardAccountProofV1>> {
956        // Get the accounts
957        let (reward_merkle_tree_from_db, _) = self
958            .db
959            .get_reward_accounts_v1(instance, block_height, view, accounts)
960            .await
961            .with_context(|| "failed to get reward accounts from DB")?;
962        // Verify the accounts
963        let mut proofs = Vec::new();
964        for account in accounts {
965            let (proof, _) =
966                RewardAccountProofV1::prove(&reward_merkle_tree_from_db, (*account).into())
967                    .context(format!("response missing account {account}"))?;
968            proof.verify(&reward_merkle_tree_root).context(format!(
969                "invalid proof for v1 reward account {account}, root: {reward_merkle_tree_root}"
970            ))?;
971            proofs.push(proof);
972        }
973
974        Ok(proofs)
975    }
976
977    async fn try_fetch_state_cert(
978        &self,
979        _retry: usize,
980        _epoch: u64,
981    ) -> anyhow::Result<LightClientStateUpdateCertificateV2<SeqTypes>> {
982        bail!("state cert catchup not supported for SqlStateCatchup");
983    }
984
985    fn backoff(&self) -> &BackoffParams {
986        &self.backoff
987    }
988
989    fn name(&self) -> String {
990        "SqlStateCatchup".into()
991    }
992
993    fn is_local(&self) -> bool {
994        true
995    }
996}
997
998/// Disable catchup entirely.
999#[derive(Clone, Debug)]
1000pub struct NullStateCatchup {
1001    backoff: BackoffParams,
1002    chain_configs: HashMap<Commitment<ChainConfig>, ChainConfig>,
1003}
1004
1005impl Default for NullStateCatchup {
1006    fn default() -> Self {
1007        Self {
1008            backoff: BackoffParams::disabled(),
1009            chain_configs: Default::default(),
1010        }
1011    }
1012}
1013
1014impl NullStateCatchup {
1015    /// Add a chain config preimage which can be fetched by hash during STF evaluation.
1016    ///
1017    /// [`NullStateCatchup`] is used to disable catchup entirely when evaluating the STF, which
1018    /// requires the [`ValidatedState`](espresso_types::ValidatedState) to be pre-seeded with all
1019    /// the dependencies of STF evaluation. However, the STF also depends on having the preimage of
1020    /// various [`ChainConfig`] commitments, which are not stored in the
1021    /// [`ValidatedState`](espresso_types::ValidatedState), but which instead must be supplied by a
1022    /// separate preimage oracle. Thus, [`NullStateCatchup`] may be populated with a set of
1023    /// [`ChainConfig`]s, which it can feed to the STF during evaluation.
1024    pub fn add_chain_config(&mut self, cf: ChainConfig) {
1025        self.chain_configs.insert(cf.commit(), cf);
1026    }
1027}
1028
1029#[async_trait]
1030impl StateCatchup for NullStateCatchup {
1031    async fn try_fetch_leaf(
1032        &self,
1033        _retry: usize,
1034        _coordinator: EpochMembershipCoordinator<SeqTypes>,
1035        _height: u64,
1036    ) -> anyhow::Result<Leaf2> {
1037        bail!("state catchup is disabled")
1038    }
1039
1040    async fn try_fetch_accounts(
1041        &self,
1042        _retry: usize,
1043        _instance: &NodeState,
1044        _height: u64,
1045        _view: ViewNumber,
1046        _fee_merkle_tree_root: FeeMerkleCommitment,
1047        _account: &[FeeAccount],
1048    ) -> anyhow::Result<Vec<FeeAccountProof>> {
1049        bail!("state catchup is disabled");
1050    }
1051
1052    async fn try_remember_blocks_merkle_tree(
1053        &self,
1054        _retry: usize,
1055        _instance: &NodeState,
1056        _height: u64,
1057        _view: ViewNumber,
1058        _mt: &mut BlockMerkleTree,
1059    ) -> anyhow::Result<()> {
1060        bail!("state catchup is disabled");
1061    }
1062
1063    async fn try_fetch_chain_config(
1064        &self,
1065        _retry: usize,
1066        commitment: Commitment<ChainConfig>,
1067    ) -> anyhow::Result<ChainConfig> {
1068        self.chain_configs
1069            .get(&commitment)
1070            .copied()
1071            .context(format!("chain config {commitment} not available"))
1072    }
1073
1074    async fn try_fetch_reward_merkle_tree_v2(
1075        &self,
1076        _retry: usize,
1077        _height: u64,
1078        _view: ViewNumber,
1079        _reward_merkle_tree_root: RewardMerkleCommitmentV2,
1080        _accounts: Arc<Vec<RewardAccountV2>>,
1081    ) -> anyhow::Result<PermittedRewardMerkleTreeV2> {
1082        bail!("state catchup is disabled");
1083    }
1084
1085    async fn try_fetch_reward_accounts_v1(
1086        &self,
1087        _retry: usize,
1088        _instance: &NodeState,
1089        _height: u64,
1090        _view: ViewNumber,
1091        _fee_merkle_tree_root: RewardMerkleCommitmentV1,
1092        _account: &[RewardAccountV1],
1093    ) -> anyhow::Result<Vec<RewardAccountProofV1>> {
1094        bail!("state catchup is disabled");
1095    }
1096
1097    async fn try_fetch_state_cert(
1098        &self,
1099        _retry: usize,
1100        _epoch: u64,
1101    ) -> anyhow::Result<LightClientStateUpdateCertificateV2<SeqTypes>> {
1102        bail!("state catchup is disabled");
1103    }
1104
1105    fn backoff(&self) -> &BackoffParams {
1106        &self.backoff
1107    }
1108
1109    fn name(&self) -> String {
1110        "NullStateCatchup".into()
1111    }
1112
1113    fn is_local(&self) -> bool {
1114        true
1115    }
1116}
1117
1118/// Catchup that sends each request once, for a caller that retries on its own.
1119///
1120/// The `fetch_*` methods of a [`ParallelStateCatchup`] race the `fetch_*` methods of its providers,
1121/// and remote ones retry until a peer answers. Peers may never answer, for example for a height
1122/// they have pruned, and then the caller waits for good. Here every `fetch_*` method makes one
1123/// `try_*` call on the inner provider and returns its error.
1124pub(crate) struct SingleAttemptCatchup<C> {
1125    inner: C,
1126    /// How many times the caller has tried before. It goes to the inner provider as the retry
1127    /// number, so that peers get longer to answer on each try, as on retries of a `fetch_*` call.
1128    retry: usize,
1129    backoff: BackoffParams,
1130}
1131
1132impl<C> SingleAttemptCatchup<C> {
1133    pub(crate) fn new(inner: C, retry: usize) -> Self {
1134        Self {
1135            inner,
1136            retry,
1137            backoff: BackoffParams::disabled(),
1138        }
1139    }
1140}
1141
1142#[async_trait]
1143impl<C: StateCatchup> StateCatchup for SingleAttemptCatchup<C> {
1144    async fn try_fetch_leaf(
1145        &self,
1146        retry: usize,
1147        coordinator: EpochMembershipCoordinator<SeqTypes>,
1148        height: u64,
1149    ) -> anyhow::Result<Leaf2> {
1150        self.inner
1151            .try_fetch_leaf(self.retry + retry, coordinator, height)
1152            .await
1153    }
1154
1155    async fn try_fetch_accounts(
1156        &self,
1157        retry: usize,
1158        instance: &NodeState,
1159        height: u64,
1160        view: ViewNumber,
1161        fee_merkle_tree_root: FeeMerkleCommitment,
1162        accounts: &[FeeAccount],
1163    ) -> anyhow::Result<Vec<FeeAccountProof>> {
1164        self.inner
1165            .try_fetch_accounts(
1166                self.retry + retry,
1167                instance,
1168                height,
1169                view,
1170                fee_merkle_tree_root,
1171                accounts,
1172            )
1173            .await
1174    }
1175
1176    async fn try_remember_blocks_merkle_tree(
1177        &self,
1178        retry: usize,
1179        instance: &NodeState,
1180        height: u64,
1181        view: ViewNumber,
1182        mt: &mut BlockMerkleTree,
1183    ) -> anyhow::Result<()> {
1184        self.inner
1185            .try_remember_blocks_merkle_tree(self.retry + retry, instance, height, view, mt)
1186            .await
1187    }
1188
1189    async fn try_fetch_chain_config(
1190        &self,
1191        retry: usize,
1192        commitment: Commitment<ChainConfig>,
1193    ) -> anyhow::Result<ChainConfig> {
1194        self.inner
1195            .try_fetch_chain_config(self.retry + retry, commitment)
1196            .await
1197    }
1198
1199    async fn try_fetch_reward_merkle_tree_v2(
1200        &self,
1201        retry: usize,
1202        height: u64,
1203        view: ViewNumber,
1204        reward_merkle_tree_root: RewardMerkleCommitmentV2,
1205        accounts: Arc<Vec<RewardAccountV2>>,
1206    ) -> anyhow::Result<PermittedRewardMerkleTreeV2> {
1207        self.inner
1208            .try_fetch_reward_merkle_tree_v2(
1209                self.retry + retry,
1210                height,
1211                view,
1212                reward_merkle_tree_root,
1213                accounts,
1214            )
1215            .await
1216    }
1217
1218    async fn try_fetch_reward_accounts_v1(
1219        &self,
1220        retry: usize,
1221        instance: &NodeState,
1222        height: u64,
1223        view: ViewNumber,
1224        reward_merkle_tree_root: RewardMerkleCommitmentV1,
1225        accounts: &[RewardAccountV1],
1226    ) -> anyhow::Result<Vec<RewardAccountProofV1>> {
1227        self.inner
1228            .try_fetch_reward_accounts_v1(
1229                self.retry + retry,
1230                instance,
1231                height,
1232                view,
1233                reward_merkle_tree_root,
1234                accounts,
1235            )
1236            .await
1237    }
1238
1239    async fn try_fetch_state_cert(
1240        &self,
1241        retry: usize,
1242        epoch: u64,
1243    ) -> anyhow::Result<LightClientStateUpdateCertificateV2<SeqTypes>> {
1244        self.inner
1245            .try_fetch_state_cert(self.retry + retry, epoch)
1246            .await
1247    }
1248
1249    fn backoff(&self) -> &BackoffParams {
1250        &self.backoff
1251    }
1252
1253    fn name(&self) -> String {
1254        self.inner.name()
1255    }
1256
1257    fn is_local(&self) -> bool {
1258        self.inner.is_local()
1259    }
1260}
1261
1262/// A provider and when it joins the race.
1263#[derive(Clone)]
1264struct Provider {
1265    catchup: Arc<dyn StateCatchup>,
1266    /// Starts after `fallback_delay` if a primary (non-fallback) remote provider is present.
1267    fallback: bool,
1268}
1269
1270/// A catchup implementation that races all providers.
1271/// It returns the first successful result.
1272#[derive(Clone)]
1273pub struct ParallelStateCatchup {
1274    providers: Arc<Mutex<Vec<Provider>>>,
1275    backoff: BackoffParams,
1276    /// Timeout for local provider requests
1277    local_timeout: Duration,
1278    fallback_delay: Duration,
1279}
1280
1281impl ParallelStateCatchup {
1282    /// Create a new [`ParallelStateCatchup`] with the given providers and local timeout.
1283    pub fn new(providers: &[Arc<dyn StateCatchup>], local_timeout: Duration) -> Self {
1284        Self {
1285            providers: Arc::new(Mutex::new(
1286                providers
1287                    .iter()
1288                    .map(|catchup| Provider {
1289                        catchup: catchup.clone(),
1290                        fallback: false,
1291                    })
1292                    .collect(),
1293            )),
1294            backoff: BackoffParams::disabled(),
1295            local_timeout,
1296            fallback_delay: Duration::ZERO,
1297        }
1298    }
1299
1300    /// Delay before fallback providers start; see [`Provider::fallback`].
1301    pub fn with_fallback_delay(mut self, delay: Duration) -> Self {
1302        self.fallback_delay = delay;
1303        self
1304    }
1305
1306    /// Add a provider to the list of providers
1307    pub fn add_provider(&self, provider: Arc<dyn StateCatchup>) {
1308        self.add(provider, false);
1309    }
1310
1311    /// Add a remote provider that races after the fallback delay; see [`Provider::fallback`].
1312    pub fn add_fallback_provider(&self, provider: Arc<dyn StateCatchup>) {
1313        self.add(provider, true);
1314    }
1315
1316    fn add(&self, catchup: Arc<dyn StateCatchup>, fallback: bool) {
1317        self.providers.lock().push(Provider { catchup, fallback });
1318    }
1319
1320    /// Perform an async operation on all providers, returning the first result to succeed
1321    async fn on_providers<C, F, RT>(&self, closure: C) -> anyhow::Result<RT>
1322    where
1323        C: Fn(Arc<dyn StateCatchup>) -> F + Clone + Send + Sync + 'static,
1324        F: Future<Output = anyhow::Result<RT>> + Send + 'static,
1325        RT: Send + Sync + 'static,
1326    {
1327        let providers = self.providers.lock().clone();
1328        if providers.is_empty() {
1329            return Err(anyhow::anyhow!("no providers were initialized"));
1330        }
1331
1332        let has_primary_remote = providers
1333            .iter()
1334            .any(|p| !p.fallback && !p.catchup.is_local());
1335        let local_timeout = self.local_timeout;
1336        let fallback_delay = self.fallback_delay;
1337
1338        let mut futures = FuturesUnordered::new();
1339        for Provider { catchup, fallback } in providers {
1340            let closure = closure.clone();
1341            let name = catchup.name();
1342            futures.push(AbortOnDropHandle::new(tokio::spawn(async move {
1343                let result = if catchup.is_local() {
1344                    timeout(local_timeout, closure(catchup))
1345                        .await
1346                        .unwrap_or_else(|_| {
1347                            let err = format!("{name} timed out after {local_timeout:?}");
1348                            tracing::warn!("{err}");
1349                            Err(anyhow::anyhow!(err))
1350                        })
1351                } else {
1352                    if fallback && has_primary_remote {
1353                        sleep(fallback_delay).await;
1354                    }
1355                    closure(catchup).await
1356                };
1357                (name, result)
1358            })));
1359        }
1360
1361        let mut logs = vec!["No providers returned a successful result.\n".to_string()];
1362        // Return the first successful result
1363        while let Some(result) = futures.next().await {
1364            // Unwrap the inner (join) result
1365            let (name, result) = match result {
1366                Ok(res) => res,
1367                Err(err) => {
1368                    tracing::debug!("Failed to join on provider: {err:#}.");
1369                    logs.push(format!("Failed to join on provider: {err:#}."));
1370                    continue;
1371                },
1372            };
1373
1374            // If a provider fails, print why
1375            let result = match result {
1376                Ok(res) => res,
1377                Err(err) => {
1378                    tracing::debug!("Failed to fetch data from {name}: {err:#}.");
1379                    logs.push(format!("Failed to fetch data from {name}: {err:#}."));
1380                    continue;
1381                },
1382            };
1383
1384            return Ok(result);
1385        }
1386
1387        Err(anyhow::anyhow!(logs.join("\n")))
1388    }
1389}
1390
1391macro_rules! clone {
1392    ( ($( $x:ident ),*) $y:expr ) => {
1393        {
1394            $(let $x = $x.clone();)*
1395            $y
1396        }
1397    };
1398}
1399
1400#[async_trait]
1401impl StateCatchup for ParallelStateCatchup {
1402    async fn try_fetch_leaf(
1403        &self,
1404        retry: usize,
1405        coordinator: EpochMembershipCoordinator<SeqTypes>,
1406        height: u64,
1407    ) -> anyhow::Result<Leaf2> {
1408        self.on_providers(clone! {(coordinator) move |provider| {
1409            clone!{(coordinator) async move {
1410                provider
1411                    .try_fetch_leaf(retry, coordinator, height)
1412                    .await
1413            }}
1414        }})
1415        .await
1416    }
1417
1418    async fn try_fetch_accounts(
1419        &self,
1420        retry: usize,
1421        instance: &NodeState,
1422        height: u64,
1423        view: ViewNumber,
1424        fee_merkle_tree_root: FeeMerkleCommitment,
1425        accounts: &[FeeAccount],
1426    ) -> anyhow::Result<Vec<FeeAccountProof>> {
1427        let accounts_vec = accounts.to_vec();
1428        self.on_providers(clone! {(instance, accounts_vec) move |provider| {
1429            clone! {(instance, accounts_vec) async move {
1430                provider
1431                    .try_fetch_accounts(
1432                        retry,
1433                        &instance,
1434                        height,
1435                        view,
1436                        fee_merkle_tree_root,
1437                        &accounts_vec,
1438                    )
1439                    .await
1440            }}
1441        }})
1442        .await
1443    }
1444
1445    async fn try_remember_blocks_merkle_tree(
1446        &self,
1447        retry: usize,
1448        instance: &NodeState,
1449        height: u64,
1450        view: ViewNumber,
1451        mt: &mut BlockMerkleTree,
1452    ) -> anyhow::Result<()> {
1453        *mt = self
1454            .on_providers(clone! {(mt, instance) move |provider| {
1455                let mut mt = mt.clone();
1456                clone! {(instance) async move {
1457                    provider
1458                        .try_remember_blocks_merkle_tree(
1459                            retry,
1460                            &instance,
1461                            height,
1462                            view,
1463                            &mut mt,
1464                        )
1465                        .await?;
1466
1467                    Ok(mt)
1468                }}
1469            }})
1470            .await?;
1471
1472        Ok(())
1473    }
1474
1475    async fn try_fetch_chain_config(
1476        &self,
1477        retry: usize,
1478        commitment: Commitment<ChainConfig>,
1479    ) -> anyhow::Result<ChainConfig> {
1480        self.on_providers(move |provider| async move {
1481            provider.try_fetch_chain_config(retry, commitment).await
1482        })
1483        .await
1484    }
1485
1486    async fn try_fetch_reward_merkle_tree_v2(
1487        &self,
1488        retry: usize,
1489        height: u64,
1490        view: ViewNumber,
1491        reward_merkle_tree_root: RewardMerkleCommitmentV2,
1492        accounts: Arc<Vec<RewardAccountV2>>,
1493    ) -> anyhow::Result<PermittedRewardMerkleTreeV2> {
1494        self.on_providers(clone! {(accounts) move |provider| {
1495            clone! {(accounts) async move {
1496                provider
1497                    .try_fetch_reward_merkle_tree_v2(
1498                        retry,
1499                        height,
1500                        view,
1501                        reward_merkle_tree_root,
1502                        accounts,
1503                    )
1504                    .await
1505            }}
1506        }})
1507        .await
1508    }
1509
1510    async fn try_fetch_reward_accounts_v1(
1511        &self,
1512        retry: usize,
1513        instance: &NodeState,
1514        height: u64,
1515        view: ViewNumber,
1516        reward_merkle_tree_root: RewardMerkleCommitmentV1,
1517        accounts: &[RewardAccountV1],
1518    ) -> anyhow::Result<Vec<RewardAccountProofV1>> {
1519        let accounts_vec = accounts.to_vec();
1520        self.on_providers(clone! {(instance, accounts_vec) move |provider| {
1521            clone! {(instance, accounts_vec) async move {
1522                provider
1523                    .try_fetch_reward_accounts_v1(
1524                        retry,
1525                        &instance,
1526                        height,
1527                        view,
1528                        reward_merkle_tree_root,
1529                        &accounts_vec,
1530                    )
1531                    .await
1532            }}
1533        }})
1534        .await
1535    }
1536
1537    async fn try_fetch_state_cert(
1538        &self,
1539        retry: usize,
1540        epoch: u64,
1541    ) -> anyhow::Result<LightClientStateUpdateCertificateV2<SeqTypes>> {
1542        self.on_providers(move |provider| async move {
1543            provider.try_fetch_state_cert(retry, epoch).await
1544        })
1545        .await
1546    }
1547
1548    fn backoff(&self) -> &BackoffParams {
1549        &self.backoff
1550    }
1551
1552    fn name(&self) -> String {
1553        format!(
1554            "[{}]",
1555            self.providers
1556                .lock()
1557                .iter()
1558                .map(|p| p.catchup.name())
1559                .collect::<Vec<_>>()
1560                .join(", ")
1561        )
1562    }
1563
1564    async fn fetch_accounts(
1565        &self,
1566        instance: &NodeState,
1567        height: u64,
1568        view: ViewNumber,
1569        fee_merkle_tree_root: FeeMerkleCommitment,
1570        accounts: Vec<FeeAccount>,
1571    ) -> anyhow::Result<Vec<FeeAccountProof>> {
1572        self.on_providers(clone! {(instance, accounts) move |provider| {
1573            clone! {(instance, accounts) async move {
1574                provider
1575                    .fetch_accounts(&instance, height, view, fee_merkle_tree_root, accounts)
1576                    .await
1577            }}
1578        }})
1579        .await
1580    }
1581
1582    async fn fetch_leaf(
1583        &self,
1584        coordinator: EpochMembershipCoordinator<SeqTypes>,
1585        height: u64,
1586    ) -> anyhow::Result<Leaf2> {
1587        self.on_providers(clone! {(coordinator) move |provider| {
1588            clone! {(coordinator) async move {
1589                provider.fetch_leaf(coordinator, height).await
1590            }}
1591        }})
1592        .await
1593    }
1594
1595    async fn fetch_chain_config(
1596        &self,
1597        commitment: Commitment<ChainConfig>,
1598    ) -> anyhow::Result<ChainConfig> {
1599        self.on_providers(
1600            move |provider| async move { provider.fetch_chain_config(commitment).await },
1601        )
1602        .await
1603    }
1604
1605    async fn fetch_reward_accounts_v1(
1606        &self,
1607        instance: &NodeState,
1608        height: u64,
1609        view: ViewNumber,
1610        reward_merkle_tree_root: RewardMerkleCommitmentV1,
1611        accounts: Vec<RewardAccountV1>,
1612    ) -> anyhow::Result<Vec<RewardAccountProofV1>> {
1613        self.on_providers(clone! {(instance, accounts) move |provider| {
1614            clone! {(instance, accounts) async move {
1615                provider
1616                    .fetch_reward_accounts_v1(
1617                        &instance,
1618                        height,
1619                        view,
1620                        reward_merkle_tree_root,
1621                        accounts,
1622                    )
1623                    .await
1624            }}
1625        }})
1626        .await
1627    }
1628
1629    async fn fetch_state_cert(
1630        &self,
1631        epoch: u64,
1632    ) -> anyhow::Result<LightClientStateUpdateCertificateV2<SeqTypes>> {
1633        self.on_providers(move |provider| async move { provider.fetch_state_cert(epoch).await })
1634            .await
1635    }
1636
1637    async fn remember_blocks_merkle_tree(
1638        &self,
1639        instance: &NodeState,
1640        height: u64,
1641        view: ViewNumber,
1642        mt: &mut BlockMerkleTree,
1643    ) -> anyhow::Result<()> {
1644        *mt = self
1645            .on_providers(clone! {(mt, instance) move |provider| {
1646                let mut mt = mt.clone();
1647                clone! {(instance) async move {
1648                    provider
1649                        .remember_blocks_merkle_tree(&instance, height, view, &mut mt)
1650                        .await?;
1651                    Ok(mt)
1652                }}
1653            }})
1654            .await?;
1655
1656        Ok(())
1657    }
1658
1659    fn is_local(&self) -> bool {
1660        self.providers.lock().iter().all(|p| p.catchup.is_local())
1661    }
1662
1663    fn with_local_timeout(&self, local_timeout: Duration) -> Option<Arc<dyn StateCatchup>> {
1664        // The copy shares the provider list, so it also sees providers added later.
1665        Some(Arc::new(Self {
1666            local_timeout,
1667            ..self.clone()
1668        }))
1669    }
1670}
1671
1672/// Add accounts to the in-memory consensus state.
1673/// We use this during catchup after receiving verified accounts.
1674pub async fn add_fee_accounts_to_state(
1675    consensus_handle: &dyn ConsensusSource,
1676    view: &ViewNumber,
1677    accounts: &[FeeAccount],
1678    tree: &FeeMerkleTree,
1679    leaf: Leaf2,
1680) -> anyhow::Result<()> {
1681    let (existing_state, delta) = consensus_handle.state_and_delta(*view).await;
1682    let (state, delta) = match existing_state {
1683        Some(existing) => {
1684            let mut state = (*existing).clone();
1685            for account in accounts {
1686                if let Some((proof, _)) = FeeAccountProof::prove(tree, (*account).into()) {
1687                    if let Err(err) = proof.remember(&mut state.fee_merkle_tree) {
1688                        tracing::warn!(
1689                            ?view,
1690                            %account,
1691                            "cannot update fetched account state: {err:#}"
1692                        );
1693                    }
1694                } else {
1695                    tracing::warn!(?view, %account, "cannot update fetched account state because account is not in the merkle tree");
1696                };
1697            }
1698            (Arc::new(state), delta)
1699        },
1700        None => {
1701            // If we don't already have a leaf for this view, or if we don't have the view
1702            // at all, we can create a new view based on the recovered leaf and add it to
1703            // our state map. In this case, we must also add the leaf to the saved leaves
1704            // map to ensure consistency.
1705            let mut state = ValidatedState::from_header(leaf.block_header());
1706            state.fee_merkle_tree = tree.clone();
1707            (Arc::new(state), None)
1708        },
1709    };
1710
1711    consensus_handle
1712        .update_leaf(leaf, state, delta)
1713        .await
1714        .with_context(|| "failed to update leaf")?;
1715
1716    Ok(())
1717}
1718
1719/// Add accounts to the in-memory consensus state.
1720/// We use this during catchup after receiving verified accounts.
1721pub async fn add_v2_reward_accounts_to_state(
1722    consensus_handle: &dyn ConsensusSource,
1723    view: &ViewNumber,
1724    accounts: &[RewardAccountV2],
1725    tree: &RewardMerkleTreeV2,
1726    leaf: Leaf2,
1727) -> anyhow::Result<()> {
1728    let (existing_state, delta) = consensus_handle.state_and_delta(*view).await;
1729    let (state, delta) = match existing_state {
1730        Some(existing) => {
1731            let mut state = (*existing).clone();
1732            for account in accounts {
1733                if let Some((proof, _)) = RewardAccountProofV2::prove(tree, (*account).into()) {
1734                    if let Err(err) = proof.remember(&mut state.reward_merkle_tree_v2) {
1735                        tracing::warn!(
1736                            ?view,
1737                            %account,
1738                            "cannot update fetched account state: {err:#}"
1739                        );
1740                    }
1741                } else {
1742                    tracing::warn!(?view, %account, "cannot update fetched account state because account is not in the merkle tree");
1743                };
1744            }
1745            (Arc::new(state), delta)
1746        },
1747        None => {
1748            // If we don't already have a leaf for this view, or if we don't have the view
1749            // at all, we can create a new view based on the recovered leaf and add it to
1750            // our state map. In this case, we must also add the leaf to the saved leaves
1751            // map to ensure consistency.
1752            let mut state = ValidatedState::from_header(leaf.block_header());
1753            state.reward_merkle_tree_v2 = tree.clone();
1754            (Arc::new(state), None)
1755        },
1756    };
1757
1758    consensus_handle
1759        .update_leaf(leaf, state, delta)
1760        .await
1761        .with_context(|| "failed to update leaf")?;
1762
1763    Ok(())
1764}
1765
1766/// Add accounts to the in-memory consensus state.
1767/// We use this during catchup after receiving verified accounts.
1768pub async fn add_v1_reward_accounts_to_state(
1769    consensus_handle: &dyn ConsensusSource,
1770    view: &ViewNumber,
1771    accounts: &[RewardAccountV1],
1772    tree: &RewardMerkleTreeV1,
1773    leaf: Leaf2,
1774) -> anyhow::Result<()> {
1775    let (existing_state, delta) = consensus_handle.state_and_delta(*view).await;
1776    let (state, delta) = match existing_state {
1777        Some(existing) => {
1778            let mut state = (*existing).clone();
1779            for account in accounts {
1780                if let Some((proof, _)) = RewardAccountProofV1::prove(tree, (*account).into()) {
1781                    if let Err(err) = proof.remember(&mut state.reward_merkle_tree_v1) {
1782                        tracing::warn!(
1783                            ?view,
1784                            %account,
1785                            "cannot update fetched account state: {err:#}"
1786                        );
1787                    }
1788                } else {
1789                    tracing::warn!(?view, %account, "cannot update fetched account state because account is not in the merkle tree");
1790                };
1791            }
1792            (Arc::new(state), delta)
1793        },
1794        None => {
1795            // If we don't already have a leaf for this view, or if we don't have the view
1796            // at all, we can create a new view based on the recovered leaf and add it to
1797            // our state map. In this case, we must also add the leaf to the saved leaves
1798            // map to ensure consistency.
1799            let mut state = ValidatedState::from_header(leaf.block_header());
1800            state.reward_merkle_tree_v1 = tree.clone();
1801            (Arc::new(state), None)
1802        },
1803    };
1804
1805    consensus_handle
1806        .update_leaf(leaf, state, delta)
1807        .await
1808        .with_context(|| "failed to update leaf")?;
1809
1810    Ok(())
1811}
1812
1813#[cfg(test)]
1814mod test {
1815    use std::{
1816        sync::atomic::{AtomicUsize, Ordering as AtomicOrdering},
1817        time::Instant,
1818    };
1819
1820    use espresso_types::v0::mock::MockStateCatchup;
1821    use hotshot_types::traits::metrics::NoMetrics;
1822    use test_utils::reserve_tcp_port;
1823
1824    use super::*;
1825    use crate::SequencerApiVersion;
1826
1827    /// A peer that never answers keeps the `fetch_*` methods of a [`ParallelStateCatchup`] retrying
1828    /// for good, so a caller that retries on its own never gets an error to retry from.
1829    /// [`SingleAttemptCatchup`] returns the error after one request.
1830    #[test_log::test(tokio::test(flavor = "multi_thread"))]
1831    async fn single_attempt_catchup_does_not_retry_peers() {
1832        // Nothing listens on this port, so every request to the peer fails.
1833        let port = reserve_tcp_port().expect("OS should have ephemeral ports available");
1834        let peer = StatePeers::<SequencerApiVersion>::from_urls(
1835            vec![format!("http://127.0.0.1:{port}").parse().unwrap()],
1836            BackoffParams::default(),
1837            Duration::from_secs(1),
1838            &NoMetrics,
1839        );
1840        let peers = ParallelStateCatchup::new(&[Arc::new(peer)], Duration::from_secs(1));
1841        let chain_config = ChainConfig::default().commit();
1842        assert!(
1843            timeout(
1844                Duration::from_secs(3),
1845                peers.fetch_chain_config(chain_config)
1846            )
1847            .await
1848            .is_err(),
1849            "the parallel catchup should retry the peer until the timeout"
1850        );
1851
1852        let once = SingleAttemptCatchup::new(peers, 0);
1853        let res = timeout(
1854            Duration::from_secs(3),
1855            once.fetch_chain_config(chain_config),
1856        )
1857        .await
1858        .expect("fetching the chain config should not retry");
1859        assert!(res.is_err());
1860        let instance = NodeState::mock();
1861        let fee_root = ValidatedState::default().fee_merkle_tree.commitment();
1862        let res = timeout(
1863            Duration::from_secs(3),
1864            once.fetch_accounts(
1865                &instance,
1866                1,
1867                ViewNumber::new(1),
1868                fee_root,
1869                vec![FeeAccount::default()],
1870            ),
1871        )
1872        .await
1873        .expect("fetching fee accounts should not retry");
1874        assert!(res.is_err());
1875        let mut frontier = ValidatedState::default().block_merkle_tree;
1876        let res = timeout(
1877            Duration::from_secs(3),
1878            once.remember_blocks_merkle_tree(&instance, 1, ViewNumber::new(1), &mut frontier),
1879        )
1880        .await
1881        .expect("fetching the block frontier should not retry");
1882        assert!(res.is_err());
1883    }
1884
1885    /// The try number the caller passes in reaches the peers, so that they get longer to answer on
1886    /// later tries.
1887    #[test_log::test(tokio::test(flavor = "multi_thread"))]
1888    async fn single_attempt_catchup_passes_the_try_number_on() {
1889        const BASE_TIMEOUT: Duration = Duration::from_millis(200);
1890        // The peer accepts connections but never answers, so each request takes its whole timeout.
1891        let silent = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1892        let peer = StatePeers::<SequencerApiVersion>::from_urls(
1893            vec![
1894                format!("http://{}", silent.local_addr().unwrap())
1895                    .parse()
1896                    .unwrap(),
1897            ],
1898            BackoffParams::default(),
1899            BASE_TIMEOUT,
1900            &NoMetrics,
1901        );
1902        let peers = ParallelStateCatchup::new(&[Arc::new(peer)], Duration::from_secs(1));
1903
1904        let start = Instant::now();
1905        let res = SingleAttemptCatchup::new(peers, 4)
1906            .fetch_chain_config(ChainConfig::default().commit())
1907            .await;
1908        assert!(res.is_err());
1909        // On the fifth try, the peer gets five times the base timeout.
1910        let elapsed = start.elapsed();
1911        assert!(
1912            elapsed >= 5 * BASE_TIMEOUT,
1913            "the peer got only {elapsed:?} to answer"
1914        );
1915    }
1916
1917    /// A copy with a longer local timeout waits that long on a slow local provider, where the
1918    /// original gives up on it.
1919    #[test_log::test(tokio::test(flavor = "multi_thread"))]
1920    async fn with_local_timeout_waits_longer_on_local_providers() {
1921        let slow = MockStateCatchup::default().with_delay(Duration::from_millis(500));
1922        let catchup = ParallelStateCatchup::new(&[Arc::new(slow)], Duration::from_millis(50));
1923        let chain_config = ChainConfig::default().commit();
1924        // There is no remote provider, so a local read cut short fails.
1925        assert!(
1926            catchup
1927                .try_fetch_chain_config(0, chain_config)
1928                .await
1929                .is_err()
1930        );
1931
1932        let longer = catchup
1933            .with_local_timeout(Duration::from_secs(5))
1934            .expect("a parallel catchup has a local timeout");
1935        longer
1936            .try_fetch_chain_config(0, chain_config)
1937            .await
1938            .expect("the local provider answers within the longer timeout");
1939
1940        // The copy shares the provider list, so it also sees providers added later.
1941        catchup.add_provider(Arc::new(StatePeers::<SequencerApiVersion>::from_urls(
1942            vec!["http://127.0.0.1:1".parse().unwrap()],
1943            BackoffParams::default(),
1944            Duration::from_secs(1),
1945            &NoMetrics,
1946        )));
1947        assert!(!longer.is_local());
1948    }
1949
1950    #[test]
1951    fn test_peer_priority() {
1952        let good_peer = PeerScore {
1953            requests: 1000,
1954            failures: 2,
1955        };
1956        let bad_peer = PeerScore {
1957            requests: 10,
1958            failures: 1,
1959        };
1960        assert!(good_peer > bad_peer);
1961
1962        let mut peers: PriorityQueue<_, _> = [(0, good_peer), (1, bad_peer)].into_iter().collect();
1963        assert_eq!(peers.pop(), Some((0, good_peer)));
1964        assert_eq!(peers.pop(), Some((1, bad_peer)));
1965    }
1966
1967    /// Serves a chain config after `delay`; every other request fails.
1968    struct DelayedCatchup {
1969        local: bool,
1970        delay: Duration,
1971        backoff: BackoffParams,
1972        calls: Arc<AtomicUsize>,
1973    }
1974
1975    impl DelayedCatchup {
1976        fn provider(local: bool, delay: Duration) -> Arc<dyn StateCatchup> {
1977            Self::counted(local, delay).0
1978        }
1979
1980        fn counted(local: bool, delay: Duration) -> (Arc<dyn StateCatchup>, Arc<AtomicUsize>) {
1981            let calls = Arc::new(AtomicUsize::new(0));
1982            let provider = Arc::new(Self {
1983                local,
1984                delay,
1985                backoff: BackoffParams::disabled(),
1986                calls: calls.clone(),
1987            });
1988            (provider, calls)
1989        }
1990    }
1991
1992    #[async_trait]
1993    impl StateCatchup for DelayedCatchup {
1994        async fn try_fetch_chain_config(
1995            &self,
1996            _retry: usize,
1997            _commitment: Commitment<ChainConfig>,
1998        ) -> anyhow::Result<ChainConfig> {
1999            self.calls.fetch_add(1, AtomicOrdering::SeqCst);
2000            tokio::time::sleep(self.delay).await;
2001            Ok(ChainConfig::default())
2002        }
2003
2004        async fn try_fetch_leaf(
2005            &self,
2006            _retry: usize,
2007            _coordinator: EpochMembershipCoordinator<SeqTypes>,
2008            _height: u64,
2009        ) -> anyhow::Result<Leaf2> {
2010            bail!("unsupported")
2011        }
2012
2013        async fn try_fetch_accounts(
2014            &self,
2015            _retry: usize,
2016            _instance: &NodeState,
2017            _height: u64,
2018            _view: ViewNumber,
2019            _fee_merkle_tree_root: FeeMerkleCommitment,
2020            _account: &[FeeAccount],
2021        ) -> anyhow::Result<Vec<FeeAccountProof>> {
2022            bail!("unsupported")
2023        }
2024
2025        async fn try_remember_blocks_merkle_tree(
2026            &self,
2027            _retry: usize,
2028            _instance: &NodeState,
2029            _height: u64,
2030            _view: ViewNumber,
2031            _mt: &mut BlockMerkleTree,
2032        ) -> anyhow::Result<()> {
2033            bail!("unsupported")
2034        }
2035
2036        async fn try_fetch_reward_merkle_tree_v2(
2037            &self,
2038            _retry: usize,
2039            _height: u64,
2040            _view: ViewNumber,
2041            _reward_merkle_tree_root: RewardMerkleCommitmentV2,
2042            _accounts: Arc<Vec<RewardAccountV2>>,
2043        ) -> anyhow::Result<PermittedRewardMerkleTreeV2> {
2044            bail!("unsupported")
2045        }
2046
2047        async fn try_fetch_reward_accounts_v1(
2048            &self,
2049            _retry: usize,
2050            _instance: &NodeState,
2051            _height: u64,
2052            _view: ViewNumber,
2053            _reward_merkle_tree_root: RewardMerkleCommitmentV1,
2054            _accounts: &[RewardAccountV1],
2055        ) -> anyhow::Result<Vec<RewardAccountProofV1>> {
2056            bail!("unsupported")
2057        }
2058
2059        async fn try_fetch_state_cert(
2060            &self,
2061            _retry: usize,
2062            _epoch: u64,
2063        ) -> anyhow::Result<LightClientStateUpdateCertificateV2<SeqTypes>> {
2064            bail!("unsupported")
2065        }
2066
2067        fn backoff(&self) -> &BackoffParams {
2068            &self.backoff
2069        }
2070
2071        fn name(&self) -> String {
2072            format!(
2073                "DelayedCatchup({})",
2074                if self.local { "local" } else { "remote" }
2075            )
2076        }
2077
2078        fn is_local(&self) -> bool {
2079            self.local
2080        }
2081    }
2082
2083    #[tokio::test(start_paused = true)]
2084    async fn test_remote_result_does_not_wait_for_local() {
2085        let local = DelayedCatchup::provider(true, Duration::from_secs(60));
2086        let remote = DelayedCatchup::provider(false, Duration::ZERO);
2087        let catchup = ParallelStateCatchup::new(&[local, remote], Duration::from_secs(30));
2088
2089        let config = ChainConfig::default();
2090        timeout(
2091            Duration::from_secs(1),
2092            catchup.try_fetch_chain_config(0, config.commit()),
2093        )
2094        .await
2095        .expect("remote result should not wait for the local provider")
2096        .unwrap();
2097    }
2098
2099    #[tokio::test(start_paused = true)]
2100    async fn test_local_result_does_not_wait_for_remote() {
2101        let local = DelayedCatchup::provider(true, Duration::ZERO);
2102        let remote = DelayedCatchup::provider(false, Duration::from_secs(60));
2103        let catchup = ParallelStateCatchup::new(&[local, remote], Duration::from_secs(30));
2104
2105        let config = ChainConfig::default();
2106        timeout(
2107            Duration::from_secs(1),
2108            catchup.fetch_chain_config(config.commit()),
2109        )
2110        .await
2111        .expect("local result should not wait for the remote provider")
2112        .unwrap();
2113    }
2114
2115    #[tokio::test(start_paused = true)]
2116    async fn test_all_providers_failing_reports_every_error() {
2117        let local = DelayedCatchup::provider(true, Duration::ZERO);
2118        let remote = DelayedCatchup::provider(false, Duration::ZERO);
2119        let catchup = ParallelStateCatchup::new(&[local, remote], Duration::from_secs(30));
2120
2121        let err = format!("{:#}", catchup.fetch_state_cert(1).await.unwrap_err());
2122        assert_eq!(err.matches("unsupported").count(), 2, "{err}");
2123        assert!(err.contains("DelayedCatchup(local)"), "{err}");
2124        assert!(err.contains("DelayedCatchup(remote)"), "{err}");
2125    }
2126
2127    const FALLBACK_DELAY: Duration = Duration::from_secs(2);
2128
2129    fn fallback_catchup(
2130        primary: Arc<dyn StateCatchup>,
2131        fallback: Arc<dyn StateCatchup>,
2132    ) -> ParallelStateCatchup {
2133        let catchup = ParallelStateCatchup::new(&[primary], Duration::from_secs(30))
2134            .with_fallback_delay(FALLBACK_DELAY);
2135        catchup.add_fallback_provider(fallback);
2136        catchup
2137    }
2138
2139    #[tokio::test(start_paused = true)]
2140    async fn test_fallback_not_called_when_primary_answers_first() {
2141        let (primary, _) = DelayedCatchup::counted(false, Duration::from_secs(1));
2142        let (fallback, fallback_calls) = DelayedCatchup::counted(false, Duration::ZERO);
2143        let catchup = fallback_catchup(primary, fallback);
2144
2145        catchup
2146            .fetch_chain_config(ChainConfig::default().commit())
2147            .await
2148            .unwrap();
2149        tokio::time::sleep(FALLBACK_DELAY * 2).await;
2150        assert_eq!(fallback_calls.load(AtomicOrdering::SeqCst), 0);
2151    }
2152
2153    #[tokio::test(start_paused = true)]
2154    async fn test_fallback_answers_after_delay_when_primary_hangs() {
2155        let (primary, _) = DelayedCatchup::counted(false, Duration::from_secs(3600));
2156        let (fallback, fallback_calls) = DelayedCatchup::counted(false, Duration::ZERO);
2157        let catchup = fallback_catchup(primary, fallback);
2158
2159        let start = tokio::time::Instant::now();
2160        catchup
2161            .fetch_chain_config(ChainConfig::default().commit())
2162            .await
2163            .unwrap();
2164        assert_eq!(start.elapsed(), FALLBACK_DELAY);
2165        assert_eq!(fallback_calls.load(AtomicOrdering::SeqCst), 1);
2166    }
2167
2168    #[tokio::test(start_paused = true)]
2169    async fn test_fallback_immediate_without_primary_remote() {
2170        let local = DelayedCatchup::provider(true, Duration::from_secs(3600));
2171        let (fallback, fallback_calls) = DelayedCatchup::counted(false, Duration::ZERO);
2172        let catchup = fallback_catchup(local, fallback);
2173
2174        let start = tokio::time::Instant::now();
2175        catchup
2176            .fetch_chain_config(ChainConfig::default().commit())
2177            .await
2178            .unwrap();
2179        assert_eq!(start.elapsed(), Duration::ZERO);
2180        assert_eq!(fallback_calls.load(AtomicOrdering::SeqCst), 1);
2181    }
2182
2183    #[tokio::test(start_paused = true)]
2184    async fn test_fallback_delay_is_decided_per_call() {
2185        let (fallback, fallback_calls) = DelayedCatchup::counted(false, Duration::ZERO);
2186        let catchup = ParallelStateCatchup::new(&[], Duration::from_secs(30))
2187            .with_fallback_delay(FALLBACK_DELAY);
2188        catchup.add_fallback_provider(fallback);
2189        let config = ChainConfig::default();
2190
2191        let start = tokio::time::Instant::now();
2192        catchup.fetch_chain_config(config.commit()).await.unwrap();
2193        assert_eq!(start.elapsed(), Duration::ZERO);
2194
2195        catchup.add_provider(DelayedCatchup::provider(false, Duration::from_secs(3600)));
2196        let start = tokio::time::Instant::now();
2197        catchup.fetch_chain_config(config.commit()).await.unwrap();
2198        assert_eq!(start.elapsed(), FALLBACK_DELAY);
2199        assert_eq!(fallback_calls.load(AtomicOrdering::SeqCst), 2);
2200    }
2201}