Skip to main content

light_client/
state.rs

1//! Client-side state used to implement light client fetching and verification.
2
3use std::{
4    collections::{BTreeMap, HashMap},
5    future::Future,
6    ops::Range,
7    sync::Arc,
8};
9
10use anyhow::{Context, Result, bail, ensure};
11use async_lock::RwLock;
12use committable::Committable;
13use espresso_types::{
14    Certificate2, ChainId, DECAF_CHAIN_ID, DrbAndHeaderUpgradeVersion, Header, Leaf2, NamespaceId,
15    PubKey, SeqTypes, StakeTableState, Transaction, ValidatorSet,
16};
17use futures::future::try_join;
18use hotshot_query_service_types::{
19    HeightIndexed,
20    availability::{BlockQueryData, LeafId, LeafQueryData, PayloadQueryData, VidCommonQueryData},
21    node::BlockId,
22};
23use hotshot_types::{data::EpochNumber, stake_table::StakeTableEntry, utils::root_block_in_epoch};
24use serde::{Deserialize, Serialize};
25use vbs::version::{StaticVersionType, Version};
26
27use crate::{
28    client::Client,
29    consensus::{
30        leaf::LeafProofHint,
31        quorum::{Quorum, StakeTable, StakeTablePair, StakeTableQuorum},
32    },
33    storage::{LeafRequest, Storage},
34};
35
36/// Initial state for a [`LightClient`].
37///
38/// This [`Genesis`] forms the root of trust for a light client. It defines the initial stake table
39/// which is used to verify blocks before the first epoch, which in turn allows it to verify
40/// transitions to subsequent stake tables. Thus, this genesis must be configured correctly (i.e.
41/// matching the genesis state of honest HotShot nodes) or else the light client may not operate
42/// correctly.
43#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
44#[cfg_attr(
45    feature = "rlp",
46    derive(alloy_rlp::RlpEncodable, alloy_rlp::RlpDecodable)
47)]
48pub struct Genesis {
49    /// The number of blocks in an epoch.
50    pub epoch_height: u64,
51
52    /// The first epoch where the stake table came from the contract, rather than the genesis stake
53    /// table.
54    pub first_epoch_with_dynamic_stake_table: EpochNumber,
55
56    /// The fixed stake table used before epochs begin.
57    pub stake_table: Vec<StakeTableEntry<PubKey>>,
58
59    /// Genesis chain id, used to derive Decaf-specific behavior.
60    pub chain_id: ChainId,
61}
62
63/// Maximum distance between a requested leaf and the `finalized` hint sent to the server. Servers
64/// reject or ignore distant hints; beyond this we omit the hint and verify against a quorum.
65const MAX_FINALIZED_HINT_DISTANCE: u64 = 500;
66
67/// Whether a range fetch writes the leaves it chains to a proven leaf into the cache.
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69enum CacheLeaves {
70    Yes,
71    No,
72}
73
74#[derive(Clone, Debug, Serialize, Deserialize)]
75#[cfg_attr(feature = "clap", derive(clap::Parser))]
76pub struct LightClientOptions {
77    /// Maximum number of stake tables to cache in memory at any given time.
78    ///
79    /// The light client database keeps every stake table, so one evicted from memory is reloaded
80    /// from disk rather than rebuilt from peers.
81    #[cfg_attr(
82        feature = "clap",
83        clap(
84            long = "light-client-num-stake-tables",
85            env = "LIGHT_CLIENT_NUM_STAKE_TABLES",
86            default_value = "100",
87        )
88    )]
89    pub num_stake_tables_in_memory: usize,
90}
91
92impl Default for LightClientOptions {
93    fn default() -> Self {
94        Self {
95            num_stake_tables_in_memory: 100,
96        }
97    }
98}
99
100/// Client-side state required to implement the light client interface.
101///
102/// A [`LightClient`] can always be created [from scratch](Self::from_genesis), with no state, since
103/// ultimately all data is fetched and verified from external query nodes. However, having some
104/// persistent state can make it more efficient to use a [`LightClient`] over a long period of time,
105/// as important artifacts can be cached locally, avoiding the need to frequently re-fetch and
106/// verify them.
107#[derive(Debug)]
108pub struct LightClient<P, S> {
109    db: P,
110    server: S,
111    opt: LightClientOptions,
112
113    chain_id: ChainId,
114    epoch_height: u64,
115    first_epoch_with_dynamic_stake_table: EpochNumber,
116    genesis_stake_table: Arc<StakeTable>,
117
118    // We cache stake tables in memory since they are large and expensive to load from the database.
119    stake_tables: RwLock<BTreeMap<EpochNumber, Arc<StakeTable>>>,
120}
121
122impl<P, S> LightClient<P, S> {
123    pub fn into_storage(self) -> P {
124        self.db
125    }
126
127    pub fn storage(&self) -> &P {
128        &self.db
129    }
130
131    pub fn into_inner(self) -> S {
132        self.server
133    }
134
135    pub fn inner(&self) -> &S {
136        &self.server
137    }
138}
139
140impl<P, S> LightClient<P, S>
141where
142    P: Storage,
143    S: Client,
144{
145    /// Create a light client from scratch, with no state.
146    ///
147    /// State will automatically be populated as queries are made. The provided genesis becomes the
148    /// root of trust for verifying all state that is subsequently loaded by the light client. If
149    /// the genesis is not correct (i.e. matching the genesis used by honest HotShot nodes) the
150    /// light client may verify incorrect data, or fail to verify correct data.
151    pub fn from_genesis(db: P, server: S, genesis: Genesis) -> Self {
152        Self::from_genesis_with_options(db, server, genesis, Default::default())
153    }
154
155    /// Create a light client from scratch, with no state, using the given options.
156    ///
157    /// State will automatically be populated as queries are made. The provided genesis becomes the
158    /// root of trust for verifying all state that is subsequently loaded by the light client. If
159    /// the genesis is not correct (i.e. matching the genesis used by honest HotShot nodes) the
160    /// light client may verify incorrect data, or fail to verify correct data.
161    pub fn from_genesis_with_options(
162        db: P,
163        server: S,
164        genesis: Genesis,
165        opt: LightClientOptions,
166    ) -> Self {
167        Self {
168            db,
169            server,
170            opt,
171            chain_id: genesis.chain_id,
172            epoch_height: genesis.epoch_height,
173            genesis_stake_table: Arc::new(genesis.stake_table.into()),
174            first_epoch_with_dynamic_stake_table: genesis.first_epoch_with_dynamic_stake_table,
175            stake_tables: Default::default(),
176        }
177    }
178
179    pub fn genesis(&self) -> Genesis {
180        Genesis {
181            epoch_height: self.epoch_height,
182            first_epoch_with_dynamic_stake_table: self.first_epoch_with_dynamic_stake_table,
183            stake_table: (*self.genesis_stake_table).clone().into(),
184            chain_id: self.chain_id,
185        }
186    }
187
188    /// Get the number of known blocks in the chain.
189    ///
190    /// This is equivalent to one more than the block number of the latest known block. The latest
191    /// known block may come either from the local light client database or from an untrusted query
192    /// service (in which case the corresponding leaf is fetched and verified to ensure there is in
193    /// fact such a block). Note however that it is always possible that neither this light client
194    /// nor the connected server is aware of the true latest block, and so in rare cases the result
195    /// of this method may be an underestimate.
196    pub async fn block_height(&self) -> Result<u64> {
197        let latest_known = self.db.block_height().await?;
198        let latest_from_server = self.server.block_height().await?;
199        if latest_from_server > latest_known {
200            // The server claims there is a newer block than we previously knew about. Verify that
201            // this is the case by requesting a finality proof for the corresponding leaf.
202            if let Err(err) = self
203                .fetch_leaf(LeafId::Number(latest_from_server as usize - 1))
204                .await
205            {
206                tracing::warn!(
207                    latest_known,
208                    latest_from_server,
209                    "failed to verify block height claimed by server: {err:#}"
210                );
211                return Ok(latest_known);
212            } else {
213                return Ok(latest_from_server);
214            }
215        }
216        Ok(latest_known)
217    }
218
219    /// Fetch and verify the requested leaf.
220    pub async fn fetch_leaf(&self, id: LeafId<SeqTypes>) -> Result<LeafQueryData<SeqTypes>> {
221        self.fetch_leaf_with_quorum(id, |epoch| {
222            StakeTableQuorum::new((epoch, self), self.epoch_height)
223        })
224        .await
225    }
226
227    async fn fetch_leaf_with_quorum<Q>(
228        &self,
229        id: LeafId<SeqTypes>,
230        quorum: impl Send + FnOnce(EpochNumber) -> Q,
231    ) -> Result<LeafQueryData<SeqTypes>>
232    where
233        Q: Send + Quorum,
234    {
235        let upper_bound = self.db.leaf_upper_bound(id).await?;
236        let known_finalized = if let Some(upper_bound) = upper_bound {
237            if leaf_matches_id(&upper_bound, id) {
238                return Ok(upper_bound);
239            }
240
241            Some(upper_bound)
242        } else {
243            None
244        };
245        let known_finalized = known_finalized.filter(|anchor| match id {
246            LeafId::Number(n) => {
247                anchor.height().saturating_sub(n as u64) <= MAX_FINALIZED_HINT_DISTANCE
248            },
249            // Hash lookups only ever return the requested leaf itself, handled above.
250            LeafId::Hash(_) => true,
251        });
252        let known_finalized = known_finalized.as_ref().map(LeafQueryData::leaf);
253        self.fetch_leaf_from_server(id, known_finalized, quorum)
254            .await
255    }
256
257    /// Fetches leaves in range [start_height, end_height)
258    pub async fn fetch_leaves_in_range(
259        &self,
260        start_height: usize,
261        end_height: usize,
262    ) -> Result<Vec<LeafQueryData<SeqTypes>>> {
263        self.fetch_leaves_in_range_with(start_height, end_height, CacheLeaves::Yes)
264            .await
265    }
266
267    /// [`fetch_leaves_in_range`](Self::fetch_leaves_in_range) for a caller that needs each leaf
268    /// once, such as the state loop replaying history.
269    ///
270    /// Only the proven last leaf goes into the cache. Writing the others costs one transaction per
271    /// leaf, more than fetching the whole range, and a range longer than the cache would evict
272    /// every leaf other callers keep there as anchors.
273    pub async fn fetch_leaves_in_range_uncached(
274        &self,
275        start_height: usize,
276        end_height: usize,
277    ) -> Result<Vec<LeafQueryData<SeqTypes>>> {
278        self.fetch_leaves_in_range_with(start_height, end_height, CacheLeaves::No)
279            .await
280    }
281
282    async fn fetch_leaves_in_range_with(
283        &self,
284        start_height: usize,
285        end_height: usize,
286        cache: CacheLeaves,
287    ) -> Result<Vec<LeafQueryData<SeqTypes>>> {
288        ensure!(
289            start_height < end_height,
290            "invalid range: start must be < end"
291        );
292
293        // first try to fetch all the leaves from the local database
294        let leaves = self
295            .db
296            .get_leaves_in_range(start_height as u32, end_height as u32)
297            .await?;
298
299        if leaves.len() == end_height - start_height {
300            // we have all the leaves in the range
301            return Ok(leaves);
302        }
303
304        // Fetch the last leaf in the range as our known finalized anchor point
305        let known_end_leaf = self.fetch_leaf(LeafId::Number(end_height - 1)).await?;
306        if end_height - start_height == 1 {
307            return Ok(vec![known_end_leaf]);
308        }
309
310        // at this point, we know the end leaf is valid and is finalized
311        // now we need to fetch all leaves from start to end - 1 from the server
312        let leaves = self
313            .fetch_leaves_in_range_from_server(start_height, end_height - 1, &known_end_leaf, cache)
314            .await
315            // add the known end leaf to the result
316            .map(|mut leaves| {
317                leaves.push(known_end_leaf);
318                leaves
319            })?;
320        Ok(leaves)
321    }
322
323    /// Fetches headers in range [start_height, end_height)
324    pub async fn fetch_headers_in_range(
325        &self,
326        start_height: usize,
327        end_height: usize,
328    ) -> Result<Vec<Header>> {
329        ensure!(
330            start_height < end_height,
331            "invalid range: start must be < end"
332        );
333
334        // Reuse the verified leaf path to guarantee header correctness.
335        let leaves = self.fetch_leaves_in_range(start_height, end_height).await?;
336        Ok(leaves
337            .into_iter()
338            .map(|leaf| leaf.header().clone())
339            .collect())
340    }
341
342    /// Fetch and verify the [`Certificate2`] at the given block height.
343    ///
344    /// Returns `Ok(None)` if the server reports that no cert2 exists at this height
345    pub async fn fetch_certificate2(&self, height: u64) -> Result<Option<Certificate2<SeqTypes>>> {
346        let Some(cert2) = self.server.cert2(height).await? else {
347            return Ok(None);
348        };
349        let header = self
350            .fetch_header(BlockId::Number(height as usize))
351            .await
352            .context("fetching header to determine cert2 version")?;
353        self.verify_certificate2(cert2, &header).await.map(Some)
354    }
355
356    /// [`fetch_certificate2`](Self::fetch_certificate2) for a caller that already holds the
357    /// verified header of the block, which saves fetching it again.
358    pub async fn fetch_certificate2_for_header(
359        &self,
360        header: &Header,
361    ) -> Result<Option<Certificate2<SeqTypes>>> {
362        let Some(cert2) = self.server.cert2(header.height()).await? else {
363            return Ok(None);
364        };
365        self.verify_certificate2(cert2, header).await.map(Some)
366    }
367
368    async fn verify_certificate2(
369        &self,
370        cert2: Certificate2<SeqTypes>,
371        header: &Header,
372    ) -> Result<Certificate2<SeqTypes>> {
373        ensure!(
374            cert2.data.block_number == header.height(),
375            "cert2 block number {} does not match requested height {}",
376            cert2.data.block_number,
377            header.height(),
378        );
379        let quorum = StakeTableQuorum::new((cert2.data.epoch, self), self.epoch_height);
380        quorum
381            .verify_cert2(&cert2, header.version())
382            .await
383            .context("verifying cert2 signature")?;
384        Ok(cert2)
385    }
386
387    /// Fetches leaves from the server in range [start_height, end_height) and verifies them by
388    /// walking backwards from the known finalized leaf, ensuring each leaf's hash matches the
389    /// parent commitment of the subsequent leaf.
390    async fn fetch_leaves_in_range_from_server(
391        &self,
392        start_height: usize,
393        end_height: usize,
394        known_finalized: &LeafQueryData<SeqTypes>,
395        cache: CacheLeaves,
396    ) -> Result<Vec<LeafQueryData<SeqTypes>>> {
397        // we will fetch all the leaves till the known finalized leaf
398        let leaves = self
399            .server
400            // `get_leaves_in_range` is exclusive of the end height
401            // which we dont need because we already know the end leaf
402            .get_leaves_in_range(start_height, end_height)
403            .await?;
404
405        ensure!(
406            leaves.len() == end_height.saturating_sub(start_height),
407            "server returned {} leaves for range [{}, {})",
408            leaves.len(),
409            start_height,
410            end_height
411        );
412
413        self.verify_leaf_chain(&leaves, known_finalized, cache)
414            .await?;
415        Ok(leaves)
416    }
417
418    /// Verify unproven leaves by chaining them to a leaf already known to be finalized.
419    ///
420    /// `leaves` must be consecutive and end just below `known_finalized`, which the caller has
421    /// verified. Each leaf is then pinned by the parent commitment of the one above it, so the
422    /// whole run inherits the anchor's finality without a proof of its own.
423    async fn verify_leaf_chain(
424        &self,
425        leaves: &[LeafQueryData<SeqTypes>],
426        known_finalized: &LeafQueryData<SeqTypes>,
427        cache: CacheLeaves,
428    ) -> Result<()> {
429        let mut expected_parent = known_finalized.leaf().parent_commitment();
430        for leaf in leaves.iter().rev() {
431            let leaf_hash = leaf.hash();
432            ensure!(
433                leaf_hash == expected_parent,
434                "leaf hash mismatch: expected parent hash {:?}, got leaf hash {:?}",
435                expected_parent,
436                leaf_hash
437            );
438            expected_parent = leaf.leaf().parent_commitment();
439        }
440
441        if cache == CacheLeaves::No {
442            return Ok(());
443        }
444        // Cache the verified leaves, but do not fail the fetch if caching does.
445        for leaf in leaves {
446            if let Err(err) = self.db.insert_leaf(leaf.clone()).await {
447                tracing::warn!(
448                    "failed to cache leaf at height {}: {:#?}",
449                    leaf.height(),
450                    err
451                )
452            }
453        }
454
455        Ok(())
456    }
457
458    /// Fetch and verify the leaves in a set of height ranges.
459    ///
460    /// The bulk of the leaves come from one ranges request, which is what a fragmented set of
461    /// heights is worth batching for. Verification is still per contiguous run: each run needs one
462    /// leaf whose finality is proven, and the rest of the run chains to it. A run of a single
463    /// height is therefore just a proof fetch, with nothing left for the ranges request to carry.
464    pub async fn fetch_leaves_for_ranges(
465        &self,
466        ranges: &[Range<u64>],
467    ) -> Result<Vec<LeafQueryData<SeqTypes>>> {
468        ensure!(
469            ranges.iter().all(|range| range.start < range.end)
470                && ranges.is_sorted_by(|a, b| a.end <= b.start),
471            "each range must be non-empty, and the ranges ascending and disjoint"
472        );
473
474        // Fetching the whole span needs one finality proof, where fetching the runs separately
475        // needs one each, and a proof costs about what a hundred leaves cost. So take the span
476        // when the gaps are small: at most a few leaves wasted per leaf wanted, and no wider than
477        // the leaf range endpoint serves in one request.
478        if let (Some(lo), Some(hi)) = (
479            ranges.iter().map(|range| range.start).min(),
480            ranges.iter().map(|range| range.end).max(),
481        ) {
482            let wanted: u64 = ranges.iter().map(|range| range.end - range.start).sum();
483            let span = hi - lo;
484            // hotshot-query-service's default small-object range limit, which the zkVM build
485            // cannot import; test_fetch_leaves_for_ranges pins the two together. Should they
486            // drift anyway, the server rejects the span and the fetch falls back to per-run.
487            let limit = 500;
488            if span <= limit && span <= wanted * 4 {
489                match self.fetch_leaves_in_range(lo as usize, hi as usize).await {
490                    Ok(leaves) => {
491                        return Ok(leaves
492                            .into_iter()
493                            .filter(|leaf| {
494                                ranges.iter().any(|range| range.contains(&leaf.height()))
495                            })
496                            .collect());
497                    },
498                    // The span is an optimization over the runs, not the only way to get them,
499                    // so a failed span costs the shortcut, never the call: the per-run path below
500                    // fetches and verifies the same heights independently.
501                    Err(err) => {
502                        tracing::info!(
503                            err = %format_args!("{err:#}"),
504                            lo,
505                            hi,
506                            "span fetch failed, fetching each run instead"
507                        );
508                    },
509                }
510            }
511        }
512
513        // Only runs with something below their anchor have bulk to fetch; the anchors themselves
514        // are fetched with a proof.
515        let bulk = ranges
516            .iter()
517            .filter(|range| range.end - range.start > 1)
518            .map(|range| range.start..range.end - 1)
519            .collect::<Vec<_>>();
520        let mut fetched: HashMap<u64, LeafQueryData<SeqTypes>> = HashMap::new();
521        if !bulk.is_empty() {
522            match self.server.get_leaves_for_ranges(&bulk).await {
523                Ok(leaves) => {
524                    for leaf in leaves {
525                        fetched.insert(leaf.height(), leaf);
526                    }
527                },
528                Err(err) => {
529                    // A peer that predates the ranges endpoint answers it with an error, so
530                    // propagating here would wedge catchup until an upstream is upgraded. The
531                    // range fetch uses only endpoints every release serves.
532                    tracing::info!(
533                        err = %format_args!("{err:#}"),
534                        "leaf ranges fetch failed, fetching each range instead"
535                    );
536                    let mut leaves = vec![];
537                    for range in ranges {
538                        leaves.extend(
539                            self.fetch_leaves_in_range(range.start as usize, range.end as usize)
540                                .await?,
541                        );
542                    }
543                    return Ok(leaves);
544                },
545            }
546        }
547
548        let mut leaves = vec![];
549        for range in ranges {
550            // The anchor is proven finalized on its own, and pins the rest of the run.
551            let anchor = self
552                .fetch_leaf(LeafId::Number(range.end as usize - 1))
553                .await?;
554
555            let mut run = vec![];
556            for height in range.start..range.end - 1 {
557                let leaf = match fetched.remove(&height) {
558                    Some(leaf) => leaf,
559                    None => self.fetch_leaf(LeafId::Number(height as usize)).await?,
560                };
561                run.push(leaf);
562            }
563            self.verify_leaf_chain(&run, &anchor, CacheLeaves::Yes)
564                .await?;
565            leaves.extend(run);
566            leaves.push(anchor);
567        }
568
569        Ok(leaves)
570    }
571
572    /// Fetch and verify the requested header.
573    pub async fn fetch_header(&self, id: BlockId<SeqTypes>) -> Result<Header> {
574        self.fetch_header_with_quorum(id, |epoch| {
575            StakeTableQuorum::new((epoch, self), self.epoch_height)
576        })
577        .await
578    }
579
580    async fn fetch_header_with_quorum<Q>(
581        &self,
582        id: BlockId<SeqTypes>,
583        quorum: impl Send + FnOnce(EpochNumber) -> Q,
584    ) -> Result<Header>
585    where
586        Q: Send + Quorum,
587    {
588        let Some(upper_bound) = self.db.leaf_upper_bound(id).await? else {
589            // We have neither the requested header nor an upper bound for it. All we can do is fetch
590            // the corresponding leaf from the server (verifying a leaf proof) and then extract the
591            // header from there.
592            let leaf = self.fetch_leaf_from_server(id, None, quorum).await?;
593            return Ok(leaf.header().clone());
594        };
595
596        // If we have the leaf for the requested header in our database already, we can just
597        // extract the header.
598        if leaf_matches_id(&upper_bound, id) {
599            return Ok(upper_bound.header().clone());
600        }
601
602        // Otherwise, if we have a leaf that is known to be greater than the requested
603        // header, we can ask the server for an inclusion proof for the requested header
604        // relative to the Merkle root in the upper bound leaf.
605        let proof = match self.server.header_proof(upper_bound.height(), id).await {
606            Ok(proof) => proof,
607            // Archive nodes prune old merklized state, which a leaf proof doesn't need.
608            Err(err) => {
609                tracing::info!(
610                    %id,
611                    root = upper_bound.height(),
612                    err = %format_args!("{err:#}"),
613                    "header proof fetch failed, fetching a leaf proof instead"
614                );
615                let known_finalized = match id {
616                    BlockId::Number(n) => {
617                        upper_bound.height().saturating_sub(n as u64) <= MAX_FINALIZED_HINT_DISTANCE
618                    },
619                    // Hash lookups only ever return the requested header itself, handled above.
620                    BlockId::Hash(_) | BlockId::PayloadHash(_) => true,
621                }
622                .then(|| upper_bound.leaf());
623                let leaf = self
624                    .fetch_leaf_from_server(id, known_finalized, quorum)
625                    .await
626                    .with_context(|| format!("header proof also failed: {err:#}"))?;
627                return Ok(leaf.header().clone());
628            },
629        };
630        let header = proof
631            .verify(upper_bound.header().block_merkle_tree_root())
632            .context("invalid header proof")?;
633
634        // The server has given us a header and correctly proved it finalized, but we still
635        // need to verify that it actually gave us the header we requested.
636        ensure!(
637            header_matches_id(&header, id),
638            "server returned a valid header proof for the wrong header (requested header {id}, \
639             got header {} with hash {})",
640            header.height(),
641            header.commit(),
642        );
643
644        Ok(header)
645    }
646
647    /// Fetch and verify the requested payload.
648    pub async fn fetch_payload(&self, id: BlockId<SeqTypes>) -> Result<PayloadQueryData<SeqTypes>> {
649        Ok(self.fetch_block(id).await?.into())
650    }
651
652    /// Fetch and verify the full payload with the requested header.
653    pub async fn fetch_payload_for_header(
654        &self,
655        header: Header,
656    ) -> Result<PayloadQueryData<SeqTypes>> {
657        Ok(self.fetch_block_for_header(header).await?.into())
658    }
659
660    /// Fetch and verify the requested block.
661    pub async fn fetch_block(&self, id: BlockId<SeqTypes>) -> Result<BlockQueryData<SeqTypes>> {
662        Ok(self.fetch_block_and_vid_common(id).await?.0)
663    }
664
665    /// Fetch and verify the full block with the requested header.
666    pub async fn fetch_block_for_header(&self, header: Header) -> Result<BlockQueryData<SeqTypes>> {
667        Ok(self.fetch_block_and_vid_common_for_header(header).await?.0)
668    }
669
670    /// Fetch and verify the VID common data for the requested block.
671    pub async fn fetch_vid_common(
672        &self,
673        id: BlockId<SeqTypes>,
674    ) -> Result<VidCommonQueryData<SeqTypes>> {
675        Ok(self.fetch_block_and_vid_common(id).await?.1)
676    }
677
678    /// Fetch and verify the VID common data for the requested header.
679    pub async fn fetch_vid_common_for_header(
680        &self,
681        header: Header,
682    ) -> Result<VidCommonQueryData<SeqTypes>> {
683        Ok(self.fetch_block_and_vid_common_for_header(header).await?.1)
684    }
685
686    /// Fetch and verify the requested payload and the associated VID common data.
687    pub async fn fetch_block_and_vid_common(
688        &self,
689        id: BlockId<SeqTypes>,
690    ) -> Result<(BlockQueryData<SeqTypes>, VidCommonQueryData<SeqTypes>)> {
691        let header = self.fetch_header(id).await?;
692        self.fetch_block_and_vid_common_for_header(header).await
693    }
694
695    /// Fetch and verify the payload corresponding to `header`, and the associated VID common data.
696    pub async fn fetch_block_and_vid_common_for_header(
697        &self,
698        header: Header,
699    ) -> Result<(BlockQueryData<SeqTypes>, VidCommonQueryData<SeqTypes>)> {
700        let proof = self.server.payload_proof(header.height()).await?;
701        let (payload, vid_common) = proof.verify_with_vid_common(&header)?;
702        Ok((
703            BlockQueryData::new(header.clone(), payload),
704            VidCommonQueryData::new(header, vid_common),
705        ))
706    }
707
708    /// Fetch and verify the blocks in the height range `[start, end)`.
709    pub async fn fetch_blocks_in_range(
710        &self,
711        start: usize,
712        end: usize,
713    ) -> Result<Vec<BlockQueryData<SeqTypes>>> {
714        let res = self
715            .fetch_blocks_and_vid_common_in_range(start, end)
716            .await?;
717        Ok(res.into_iter().map(|(block, _)| block).collect())
718    }
719
720    /// Fetch and verify the VID common data for blocks in the height range `[start, end)`.
721    pub async fn fetch_vid_common_in_range(
722        &self,
723        start: usize,
724        end: usize,
725    ) -> Result<Vec<VidCommonQueryData<SeqTypes>>> {
726        let res = self
727            .fetch_blocks_and_vid_common_in_range(start, end)
728            .await?;
729        Ok(res.into_iter().map(|(_, vid)| vid).collect())
730    }
731
732    /// Fetch and verify the blocks and VID common data in the height range `[start, end)`.
733    pub async fn fetch_blocks_and_vid_common_in_range(
734        &self,
735        start: usize,
736        end: usize,
737    ) -> Result<Vec<(BlockQueryData<SeqTypes>, VidCommonQueryData<SeqTypes>)>> {
738        let (headers, proofs) = try_join(
739            self.fetch_headers_in_range(start, end),
740            self.server
741                .payload_proofs_in_range(start as u64, end as u64),
742        )
743        .await?;
744        ensure!(
745            headers.len() == proofs.len(),
746            "server returned wrong number of payload proofs for range {start}..{end} ({})",
747            proofs.len(),
748        );
749        headers
750            .into_iter()
751            .zip(proofs)
752            .map(|(header, proof)| {
753                let (payload, vid_common) = proof.verify_with_vid_common(&header)?;
754                Ok((
755                    BlockQueryData::new(header.clone(), payload),
756                    VidCommonQueryData::new(header, vid_common),
757                ))
758            })
759            .collect()
760    }
761
762    /// Fetch and verify the blocks and VID common data for a set of height ranges.
763    ///
764    /// One request for the proofs covering every range, rather than one per range. Each proof is
765    /// still verified against its own header, so batching changes only how they arrive.
766    pub async fn fetch_blocks_and_vid_common_for_ranges(
767        &self,
768        ranges: &[Range<u64>],
769    ) -> Result<Vec<(BlockQueryData<SeqTypes>, VidCommonQueryData<SeqTypes>)>> {
770        let proofs = async {
771            match self.server.payload_proofs_for_ranges(ranges).await {
772                Ok(proofs) => Ok(proofs),
773                Err(err) => {
774                    // Same fallback as the leaf ranges request, on the range endpoint every release serves.
775                    tracing::info!(
776                        err = %format_args!("{err:#}"),
777                        "payload proof ranges fetch failed, fetching each range instead"
778                    );
779                    let mut proofs = vec![];
780                    for range in ranges {
781                        proofs.extend(
782                            self.server
783                                .payload_proofs_in_range(range.start, range.end)
784                                .await?,
785                        );
786                    }
787                    Ok(proofs)
788                },
789            }
790        };
791        let (leaves, proofs) = try_join(self.fetch_leaves_for_ranges(ranges), proofs).await?;
792        ensure!(
793            leaves.len() == proofs.len(),
794            "server returned {} payload proofs for {} heights",
795            proofs.len(),
796            leaves.len(),
797        );
798
799        leaves
800            .into_iter()
801            .zip(proofs)
802            .map(|(leaf, proof)| {
803                let header = leaf.header().clone();
804                let (payload, vid_common) = proof.verify_with_vid_common(&header)?;
805                Ok((
806                    BlockQueryData::new(header.clone(), payload),
807                    VidCommonQueryData::new(header, vid_common),
808                ))
809            })
810            .collect()
811    }
812
813    /// Fetch and verify the transactions in the given namespace of the requested block.
814    pub async fn fetch_namespace(
815        &self,
816        id: BlockId<SeqTypes>,
817        namespace: NamespaceId,
818    ) -> Result<Vec<Transaction>> {
819        let header = self.fetch_header(id).await?;
820        self.fetch_namespace_for_header(&header, namespace).await
821    }
822
823    /// Fetch and verify the transactions in the given namespace of the requested header.
824    pub async fn fetch_namespace_for_header(
825        &self,
826        header: &Header,
827        namespace: NamespaceId,
828    ) -> Result<Vec<Transaction>> {
829        let proof = self
830            .server
831            .namespace_proof(header.height(), namespace)
832            .await?;
833        proof.verify(header, namespace)
834    }
835
836    /// Fetch and verify the transactions in the given namespace of blocks in the range
837    /// `[start_height, end_height)`.
838    pub async fn fetch_namespaces_in_range(
839        &self,
840        start_height: usize,
841        end_height: usize,
842        namespace: NamespaceId,
843    ) -> Result<Vec<Vec<Transaction>>> {
844        let headers = self
845            .fetch_headers_in_range(start_height, end_height)
846            .await?;
847        let proofs = self
848            .server
849            .namespace_proofs_in_range(start_height as u64, end_height as u64, namespace)
850            .await?;
851        ensure!(
852            proofs.len() == headers.len(),
853            "server returned wrong number of namespace proofs (expected {}, got {})",
854            headers.len(),
855            proofs.len()
856        );
857        proofs
858            .into_iter()
859            .zip(&headers)
860            .map(|(proof, header)| proof.verify(header, namespace))
861            .collect()
862    }
863
864    /// Fetch and verify the transactions in the given namespaces of blocks in the range
865    /// `[start_height, end_height)`.
866    ///
867    /// For each block, the result maps a namespace ID to its verified transactions, including only
868    /// those requested namespaces that are actually present in that block.
869    pub async fn fetch_all_namespaces_in_range(
870        &self,
871        start_height: usize,
872        end_height: usize,
873        namespaces: &[NamespaceId],
874    ) -> Result<Vec<HashMap<NamespaceId, Vec<Transaction>>>> {
875        let headers = self
876            .fetch_headers_in_range(start_height, end_height)
877            .await?;
878        let proofs = self
879            .server
880            .namespaces_proofs_in_range(start_height as u64, end_height as u64, namespaces)
881            .await?;
882        ensure!(
883            proofs.len() == headers.len(),
884            "server returned wrong number of namespace proofs (expected {}, got {})",
885            headers.len(),
886            proofs.len()
887        );
888        proofs
889            .into_iter()
890            .zip(&headers)
891            .map(|(proofs, header)| {
892                proofs
893                    .into_iter()
894                    .map(|(namespace, proof)| Ok((namespace, proof.verify(header, namespace)?)))
895                    .collect()
896            })
897            .collect()
898    }
899
900    /// Fetch and verify the stake table for the requested epoch.
901    pub async fn quorum_for_epoch(&self, epoch: EpochNumber) -> Result<Arc<StakeTable>> {
902        if epoch < self.first_epoch_with_dynamic_stake_table {
903            return Ok(self.genesis_stake_table.clone());
904        }
905
906        // Check cache for the desired stake table.
907        {
908            let cache = self.stake_tables.read().await;
909            if let Some(stake_table) = cache.get(&epoch) {
910                tracing::debug!(%epoch, "found stake table in cache");
911                return Ok(stake_table.clone());
912            }
913        }
914
915        // Look up an earlier stake table we can catch up from, in cache or DB.
916        //
917        // `epoch_root_protocol_version` is the version of the snapshot root header (root in epoch
918        // `k-2` for the epoch `k` we are computing). On DB load we seed it from the stored
919        // `next_snapshot_version` of `lower_bound`, which is the snapshot for `lower_bound + 1`
920        // (= iter 1's target). On genesis we fetch the root at `first_dynamic - 2` (signed by
921        // the genesis stake table) and use its version.
922        let (lower_bound, mut stake_table, mut prev_quorum, mut epoch_root_protocol_version) =
923            if let Some((lower_bound, stake_table, stored_version, next_stored_version)) =
924                self.db.stake_table_lower_bound(epoch).await?
925            {
926                if lower_bound == epoch {
927                    tracing::debug!(%epoch, "found stake table in database");
928                    let quorum = stake_table_state_to_quorum(&stake_table, stored_version)?;
929                    return Ok(self.cache_stake_table(epoch, Arc::new(quorum)).await);
930                }
931
932                let quorum = stake_table_state_to_quorum(&stake_table, stored_version)?;
933                (
934                    lower_bound,
935                    stake_table,
936                    Arc::new(quorum),
937                    next_stored_version,
938                )
939            } else {
940                // We don't have any stake table earlier than `epoch` as a starting point, so we must
941                // start from the genesis state.
942                let bootstrap_epoch = (*self.first_epoch_with_dynamic_stake_table)
943                    .checked_sub(2)
944                    .context(
945                        "first_epoch_with_dynamic_stake_table must be >= 2 for genesis catchup",
946                    )?;
947                let bootstrap_height = root_block_in_epoch(bootstrap_epoch, self.epoch_height);
948                let bootstrap_quorum = (
949                    self.genesis_stake_table.clone(),
950                    self.genesis_stake_table.clone(),
951                );
952                let bootstrap_root = self
953                    .fetch_header_with_quorum(
954                        BlockId::Number(bootstrap_height as usize),
955                        move |_epoch| StakeTableQuorum::new(bootstrap_quorum, self.epoch_height),
956                    )
957                    .await
958                    .context("fetching snapshot root header for genesis catchup")?;
959                (
960                    self.first_epoch_with_dynamic_stake_table - 1,
961                    StakeTableState::default(),
962                    self.genesis_stake_table.clone(),
963                    bootstrap_root.version(),
964                )
965            };
966        tracing::info!(from = %lower_bound, to = %epoch, "performing stake table catchup");
967
968        // Replay one epoch at a time from the lower bound stake table to the requested epoch.
969        for epoch in *lower_bound + 1..=*epoch {
970            let events = self
971                .server
972                .stake_table_events(EpochNumber::new(epoch))
973                .await?;
974            let num_events = events.len();
975            tracing::debug!(epoch, num_events, "reconstruct stake table");
976            for event in events {
977                tracing::debug!(epoch, ?event, "replay event");
978                if let Err(err) = stake_table.apply_event(event).context("applying event")? {
979                    tracing::warn!("allowed error in event: {err:#}");
980                }
981            }
982
983            let next_quorum = Arc::new(stake_table_state_to_quorum(
984                &stake_table,
985                epoch_root_protocol_version,
986            )?);
987
988            let root_height = root_block_in_epoch(epoch - 1, self.epoch_height);
989            let root = {
990                let prev_quorum = prev_quorum.clone();
991                let next_quorum = next_quorum.clone();
992                self.fetch_header_with_quorum(
993                    BlockId::Number(root_height as usize),
994                    move |_epoch| {
995                        StakeTableQuorum::new((prev_quorum, next_quorum), self.epoch_height)
996                    },
997                )
998                .await
999                .context(format!("fetching epoch root for {epoch}"))?
1000            };
1001            if let Some(hash) = root.next_stake_table_hash() {
1002                ensure!(
1003                    hash == stake_table.commit(),
1004                    "epoch {epoch} root {root_height} stake table hash {hash} does not match \
1005                     reconstructed hash {}",
1006                    stake_table.commit(),
1007                );
1008            } else if self.decaf() && root.version() < DrbAndHeaderUpgradeVersion::VERSION {
1009                // Decaf briefly ran a version of PoS where epoch root headers did not contain the
1010                // hash of the expected stake table, and so we can not verify that we computed the
1011                // correct stake table. Since this applies only to Decaf, which is a testnet, it is
1012                // acceptable to trust that the server supplied the correct events in this case.
1013                tracing::debug!(
1014                    ?epoch,
1015                    root_height,
1016                    computed_hash = %stake_table.commit(),
1017                    "trusting stake table prior to DRB upgrade on Decaf",
1018                );
1019            } else {
1020                bail!("epoch {epoch} root {root_height} does not have next stake table hash");
1021            }
1022
1023            // Cache the reconstructed stake table in the database. We store
1024            // `epoch_root_protocol_version` (snapshot for this epoch, used by cache hits) and
1025            // `root.version()` (snapshot for `epoch + 1`, used to seed iter 1 of a future
1026            // catchup that resumes from this row).
1027            if let Err(err) = self
1028                .db
1029                .insert_stake_table(
1030                    EpochNumber::new(epoch),
1031                    &stake_table,
1032                    epoch_root_protocol_version,
1033                    root.version(),
1034                )
1035                .await
1036            {
1037                // If this fails, we can continue with the stake table that we have in memory right
1038                // now, so this is just a warning.
1039                tracing::warn!(epoch, "failed to cache stake table: {err:#}");
1040            }
1041
1042            tracing::info!(epoch, num_events, "finished stake table catchup for epoch");
1043            prev_quorum = next_quorum;
1044            epoch_root_protocol_version = root.version();
1045        }
1046
1047        Ok(self.cache_stake_table(epoch, prev_quorum).await)
1048    }
1049
1050    fn fetch_leaf_from_server<'a, 'b, Q>(
1051        &'a self,
1052        id: impl Send + Into<LeafRequest> + 'a,
1053        known_finalized: Option<&'b Leaf2>,
1054        make_quorum: impl 'a + Send + FnOnce(EpochNumber) -> Q,
1055    ) -> impl 'b + Send + Future<Output = Result<LeafQueryData<SeqTypes>>>
1056    where
1057        'a: 'b,
1058        Q: Send + Quorum,
1059    {
1060        async move {
1061            let id = id.into();
1062            let proof = self
1063                .server
1064                .leaf_proof(id, known_finalized.map(Leaf2::height))
1065                .await?;
1066            let quorum;
1067            let hint = match proof.epoch(self.epoch_height)? {
1068                Some(epoch) => {
1069                    quorum = make_quorum(epoch);
1070                    LeafProofHint::Quorum(&quorum)
1071                },
1072                None => LeafProofHint::Assumption(known_finalized.context(
1073                    "server returned proof with assumption, but we have no finalized upper bound \
1074                     to verify assumption",
1075                )?),
1076            };
1077            let leaf = proof.verify(hint).await?;
1078
1079            // The server has given us a leaf and correctly proved it finalized, but we still need to
1080            // verify that it actually gave us the leaf we requested.
1081            ensure!(
1082                leaf_matches_id(&leaf, id),
1083                "server returned a valid leaf proof for the wrong leaf (requested leaf {id}, got \
1084                 leaf {} with hash {})",
1085                leaf.height(),
1086                leaf.hash(),
1087            );
1088
1089            // Having fetched and verified the leaf from the server, we can now cache it locally to
1090            // improve future requests.
1091            if let Err(err) = self.db.insert_leaf(leaf.clone()).await {
1092                // If this fails, we can still successfully return the leaf that we have in memory right
1093                // now, so this is just a warning.
1094                tracing::warn!("failed to cache fetched leaf: {err:#}");
1095            }
1096
1097            Ok(leaf)
1098        }
1099    }
1100
1101    async fn cache_stake_table(
1102        &self,
1103        epoch: EpochNumber,
1104        stake_table: Arc<StakeTable>,
1105    ) -> Arc<StakeTable> {
1106        let mut cache = self.stake_tables.write().await;
1107
1108        // If inserting the new stake table would cause the cache to exceed its maximum size, first
1109        // delete an old stake table.
1110        if cache.len() >= self.opt.num_stake_tables_in_memory {
1111            // Always delete the _second oldest_ stake table. We want to keep the oldest around
1112            // because it is the hardest to catch up for if we need it again (we would have to go
1113            // all the way back to genesis). The second oldest is the least likely to be used again
1114            // after the oldest, while still being easy to replay if we do need it (because we can
1115            // just replay from the cached oldest).
1116            if let Some(&second_oldest_epoch) = cache.keys().nth(1) {
1117                cache.remove(&second_oldest_epoch);
1118            }
1119        }
1120
1121        cache.entry(epoch).insert_entry(stake_table).get().clone()
1122    }
1123
1124    fn decaf(&self) -> bool {
1125        self.chain_id == DECAF_CHAIN_ID
1126    }
1127}
1128
1129fn stake_table_state_to_quorum(
1130    state: &StakeTableState,
1131    protocol_version: Version,
1132) -> Result<StakeTable> {
1133    let vs = ValidatorSet::from_state(state, protocol_version)?;
1134    Ok(vs
1135        .active_validators()
1136        .values()
1137        .map(|validator| StakeTableEntry {
1138            stake_key: *validator.stake_table_key(),
1139            stake_amount: validator.stake,
1140        })
1141        .collect())
1142}
1143
1144impl<P, S> StakeTablePair for (EpochNumber, &LightClient<P, S>)
1145where
1146    P: Storage,
1147    S: Client,
1148{
1149    async fn stake_table(&self) -> Result<Arc<StakeTable>> {
1150        self.1.quorum_for_epoch(self.0).await
1151    }
1152
1153    async fn next_epoch_stake_table(&self) -> Result<Arc<StakeTable>> {
1154        self.1.quorum_for_epoch(self.0 + 1).await
1155    }
1156}
1157
1158fn leaf_matches_id(leaf: &LeafQueryData<SeqTypes>, id: impl Into<LeafRequest>) -> bool {
1159    match id.into() {
1160        LeafRequest::Leaf(LeafId::Number(h)) | LeafRequest::Header(BlockId::Number(h)) => {
1161            (h as u64) == leaf.height()
1162        },
1163        LeafRequest::Leaf(LeafId::Hash(h)) => h == leaf.hash(),
1164        LeafRequest::Header(BlockId::Hash(h)) => h == leaf.block_hash(),
1165        LeafRequest::Header(BlockId::PayloadHash(h)) => h == leaf.payload_hash(),
1166    }
1167}
1168
1169fn header_matches_id(header: &Header, id: BlockId<SeqTypes>) -> bool {
1170    match id {
1171        BlockId::Number(n) => header.height() == (n as u64),
1172        BlockId::Hash(h) => header.commit() == h,
1173        BlockId::PayloadHash(h) => header.payload_commitment() == h,
1174    }
1175}
1176
1177#[cfg(test)]
1178mod test {
1179    use std::collections::HashSet;
1180
1181    use espresso_types::{
1182        NsIndex, RegisteredValidatorMap, StakeTableState, ValidatorSet, v0_3::RegisteredValidator,
1183    };
1184    use hotshot_query_service_types::availability::TransactionIndex;
1185    use hotshot_types::{addr::NetAddr, x25519};
1186    use itertools::izip;
1187    use pretty_assertions::assert_eq;
1188    use versions::{DRB_AND_HEADER_UPGRADE_VERSION, EPOCH_VERSION, NEW_PROTOCOL_VERSION};
1189
1190    use super::*;
1191    use crate::{
1192        storage::SqlStorage,
1193        testing::{TestClient, leaf_chain, random_validator},
1194    };
1195
1196    #[tokio::test]
1197    #[test_log::test]
1198    async fn test_block_height() {
1199        let client = TestClient::default();
1200        let db = SqlStorage::default().await.unwrap();
1201
1202        let lc = LightClient::from_genesis(db.clone(), client.clone(), client.genesis().await);
1203
1204        // Test empty block height.
1205        assert_eq!(lc.block_height().await.unwrap(), 0);
1206
1207        // Local block height greater than server.
1208        let leaf = leaf_chain(1..2, DRB_AND_HEADER_UPGRADE_VERSION)
1209            .await
1210            .remove(0);
1211        db.insert_leaf(leaf).await.unwrap();
1212        assert_eq!(lc.block_height().await.unwrap(), 2);
1213
1214        // Server block height greater than local.
1215        client.leaf(2).await;
1216        assert_eq!(lc.block_height().await.unwrap(), 3);
1217        // In the process of verifying the block height, we should have fetched and stored the
1218        // latest leaf.
1219        assert_eq!(db.block_height().await.unwrap(), 3);
1220
1221        // Server lies about the block height.
1222        client.mock_block_height(10).await;
1223        client.forget_leaf(9).await;
1224        assert_eq!(lc.block_height().await.unwrap(), 3);
1225    }
1226
1227    #[tokio::test]
1228    #[test_log::test]
1229    async fn test_fetch_leaf_twice() {
1230        let client = TestClient::default();
1231        let lc = LightClient::from_genesis(
1232            SqlStorage::default().await.unwrap(),
1233            client.clone(),
1234            client.genesis().await,
1235        );
1236
1237        // Fetch the leaf for the first time. We will need to get it from the server.
1238        let leaf = client.remember_leaf(1).await;
1239        assert_eq!(lc.fetch_leaf(LeafId::Number(1)).await.unwrap(), leaf);
1240
1241        // Fetching the leaf again hits the cache.
1242        client.forget_leaf(1).await;
1243        assert_eq!(lc.fetch_leaf(LeafId::Number(1)).await.unwrap(), leaf);
1244    }
1245
1246    #[tokio::test]
1247    #[test_log::test]
1248    async fn test_fetch_leaf_upper_bound() {
1249        let client = TestClient::default();
1250
1251        let db = SqlStorage::default().await.unwrap();
1252        db.insert_leaf(client.leaf(2).await).await.unwrap();
1253
1254        let lc = LightClient::from_genesis(db, client.clone(), client.genesis().await);
1255        assert_eq!(
1256            lc.fetch_leaf(LeafId::Number(1)).await.unwrap(),
1257            client.leaf(1).await,
1258        );
1259    }
1260
1261    #[tokio::test]
1262    #[test_log::test]
1263    async fn test_fetch_leaf_distant_upper_bound() {
1264        let client = TestClient::default();
1265        client.reject_distant_finalized_hints(16).await;
1266
1267        // A cached leaf far above the requested one must not be sent as the finalized hint; the
1268        // fetch should use a quorum-verified proof instead.
1269        let distant = MAX_FINALIZED_HINT_DISTANCE + 2;
1270        let distant_leaf = leaf_chain(distant..=distant, DRB_AND_HEADER_UPGRADE_VERSION)
1271            .await
1272            .remove(0);
1273        let db = SqlStorage::default().await.unwrap();
1274        db.insert_leaf(distant_leaf).await.unwrap();
1275
1276        let lc = LightClient::from_genesis(db, client.clone(), client.genesis().await);
1277        assert_eq!(
1278            lc.fetch_leaf(LeafId::Number(1)).await.unwrap(),
1279            client.leaf(1).await,
1280        );
1281    }
1282
1283    #[tokio::test]
1284    #[test_log::test]
1285    async fn test_fetch_leaf_invalid_proof() {
1286        let client = TestClient::default();
1287        let lc = LightClient::from_genesis(
1288            SqlStorage::default().await.unwrap(),
1289            client.clone(),
1290            client.genesis().await,
1291        );
1292        client.return_invalid_proof(1).await;
1293        lc.fetch_leaf(LeafId::Number(1)).await.unwrap_err();
1294        lc.fetch_leaf(LeafId::Hash(client.leaf(1).await.hash()))
1295            .await
1296            .unwrap_err();
1297    }
1298
1299    #[tokio::test]
1300    #[test_log::test]
1301    async fn test_fetch_leaf_wrong_leaf() {
1302        let client = TestClient::default();
1303        let lc = LightClient::from_genesis(
1304            SqlStorage::default().await.unwrap(),
1305            client.clone(),
1306            client.genesis().await,
1307        );
1308        client.return_wrong_leaf(1, 2).await;
1309        lc.fetch_leaf(LeafId::Number(1)).await.unwrap_err();
1310        lc.fetch_leaf(LeafId::Hash(client.leaf(1).await.hash()))
1311            .await
1312            .unwrap_err();
1313    }
1314
1315    #[tokio::test]
1316    #[test_log::test]
1317    async fn test_fetch_header_twice() {
1318        let client = TestClient::default();
1319        let lc = LightClient::from_genesis(
1320            SqlStorage::default().await.unwrap(),
1321            client.clone(),
1322            client.genesis().await,
1323        );
1324
1325        // Fetch the header for the first time. We will need to get it from the server.
1326        let leaf = client.remember_leaf(1).await;
1327        assert_eq!(
1328            lc.fetch_header(BlockId::Number(1)).await.unwrap(),
1329            *leaf.header()
1330        );
1331
1332        // Fetching the header again hits the cache.
1333        client.forget_leaf(1).await;
1334        assert_eq!(
1335            lc.fetch_header(BlockId::Number(1)).await.unwrap(),
1336            *leaf.header()
1337        );
1338    }
1339
1340    #[tokio::test]
1341    #[test_log::test]
1342    async fn test_fetch_header_upper_bound() {
1343        let client = TestClient::default();
1344
1345        let db = SqlStorage::default().await.unwrap();
1346        db.insert_leaf(client.leaf(2).await).await.unwrap();
1347
1348        let lc = LightClient::from_genesis(db, client.clone(), client.genesis().await);
1349        assert_eq!(
1350            lc.fetch_header(BlockId::Number(1)).await.unwrap(),
1351            *client.leaf(1).await.header(),
1352        );
1353    }
1354
1355    #[tokio::test]
1356    #[test_log::test]
1357    async fn test_fetch_header_falls_back_to_leaf_proof_when_header_proof_fails() {
1358        let client = TestClient::default();
1359
1360        let db = SqlStorage::default().await.unwrap();
1361        db.insert_leaf(client.leaf(2).await).await.unwrap();
1362        client.fail_header_proofs().await;
1363
1364        let lc = LightClient::from_genesis(db, client.clone(), client.genesis().await);
1365        assert_eq!(
1366            lc.fetch_header(BlockId::Number(1)).await.unwrap(),
1367            *client.leaf(1).await.header(),
1368        );
1369    }
1370
1371    #[tokio::test]
1372    #[test_log::test]
1373    async fn test_fetch_header_fallback_anchors_on_cached_successor_without_cert2() {
1374        let client = TestClient::default();
1375        client.set_upgrade(1, NEW_PROTOCOL_VERSION).await;
1376
1377        let db = SqlStorage::default().await.unwrap();
1378        db.insert_leaf(client.leaf(2).await).await.unwrap();
1379        client.fail_header_proofs().await;
1380        client.forget_cert2(1).await;
1381
1382        let lc = LightClient::from_genesis(db, client.clone(), client.genesis().await);
1383        assert_eq!(
1384            lc.fetch_header(BlockId::Number(1)).await.unwrap(),
1385            *client.leaf(1).await.header(),
1386        );
1387    }
1388
1389    #[tokio::test]
1390    #[test_log::test]
1391    async fn test_fetch_header_reports_header_proof_error_when_fallback_fails() {
1392        let client = TestClient::default();
1393
1394        let db = SqlStorage::default().await.unwrap();
1395        db.insert_leaf(client.leaf(2).await).await.unwrap();
1396        client.fail_header_proofs().await;
1397        client.forget_leaf(1).await;
1398
1399        let lc = LightClient::from_genesis(db, client.clone(), client.genesis().await);
1400        let err = lc.fetch_header(BlockId::Number(1)).await.unwrap_err();
1401        assert!(
1402            format!("{err:#}").contains("header proof also failed: no merklized state at root 2"),
1403            "{err:#}"
1404        );
1405    }
1406
1407    #[tokio::test]
1408    #[test_log::test]
1409    async fn test_fetch_header_invalid_proof() {
1410        let client = TestClient::default();
1411        let db = SqlStorage::default().await.unwrap();
1412
1413        // Start with an upper bound, so that `fetch_header` goes through the `client.header_proof`
1414        // path.
1415        db.insert_leaf(client.leaf(2).await).await.unwrap();
1416
1417        let lc = LightClient::from_genesis(db, client.clone(), client.genesis().await);
1418        client.return_invalid_proof(1).await;
1419
1420        let err = lc.fetch_header(BlockId::Number(1)).await.unwrap_err();
1421        assert!(err.to_string().contains("invalid header proof"), "{err:#}");
1422    }
1423
1424    #[tokio::test]
1425    #[test_log::test]
1426    async fn test_fetch_header_wrong_header() {
1427        let client = TestClient::default();
1428        let db = SqlStorage::default().await.unwrap();
1429
1430        // Start with an upper bound, so that `fetch_header` goes through the `client.header_proof`
1431        // path.
1432        db.insert_leaf(client.leaf(2).await).await.unwrap();
1433
1434        let lc = LightClient::from_genesis(db, client.clone(), client.genesis().await);
1435        client.return_wrong_leaf(1, 0).await;
1436
1437        let err = lc.fetch_header(BlockId::Number(1)).await.unwrap_err();
1438        assert!(
1439            err.to_string()
1440                .contains("server returned a valid header proof for the wrong header"),
1441            "{err:#}"
1442        );
1443    }
1444
1445    #[tokio::test]
1446    #[test_log::test]
1447    async fn test_fetch_leaves_in_range() {
1448        let client = TestClient::default();
1449        let lc = LightClient::from_genesis(
1450            SqlStorage::default().await.unwrap(),
1451            client.clone(),
1452            client.genesis().await,
1453        );
1454
1455        // Fetch leaves in range [1,3) for the first time. We will need to get them from the server.
1456        let leaf1 = client.remember_leaf(1).await;
1457        let leaf2 = client.remember_leaf(2).await;
1458        client.remember_leaf(3).await;
1459
1460        let leaves = lc.fetch_leaves_in_range(1, 3).await.unwrap();
1461
1462        assert_eq!(leaves, vec![leaf1.clone(), leaf2.clone()]);
1463
1464        // now remove from server and this time it should be able to fetch from local db
1465        client.forget_leaf(1).await;
1466        client.forget_leaf(2).await;
1467        let leaves = lc.fetch_leaves_in_range(1, 3).await.unwrap();
1468        assert_eq!(leaves, vec![leaf1, leaf2]);
1469    }
1470
1471    #[tokio::test]
1472    #[test_log::test]
1473    async fn test_fetch_leaves_in_range_uncached() {
1474        let client = TestClient::default();
1475        let lc = LightClient::from_genesis(
1476            SqlStorage::default().await.unwrap(),
1477            client.clone(),
1478            client.genesis().await,
1479        );
1480        let leaf1 = client.remember_leaf(1).await;
1481        let leaf2 = client.remember_leaf(2).await;
1482        let leaf3 = client.remember_leaf(3).await;
1483
1484        let leaves = lc.fetch_leaves_in_range_uncached(1, 4).await.unwrap();
1485        assert_eq!(leaves, vec![leaf1, leaf2, leaf3.clone()]);
1486
1487        // Only the proven last leaf is cached; the ones chained to it are not.
1488        assert_eq!(lc.db.get_leaves_in_range(1, 4).await.unwrap(), vec![leaf3]);
1489    }
1490
1491    #[tokio::test]
1492    #[test_log::test]
1493    async fn test_fetch_headers_in_range() {
1494        let client = TestClient::default();
1495        let lc = LightClient::from_genesis(
1496            SqlStorage::default().await.unwrap(),
1497            client.clone(),
1498            client.genesis().await,
1499        );
1500
1501        // Fetch headers in range [1,3) for the first time. We will need to get them from the server.
1502        let leaf1 = client.remember_leaf(1).await;
1503        let leaf2 = client.remember_leaf(2).await;
1504        client.remember_leaf(3).await;
1505
1506        let headers = lc.fetch_headers_in_range(1, 3).await.unwrap();
1507
1508        assert_eq!(
1509            headers,
1510            vec![leaf1.header().clone(), leaf2.header().clone()]
1511        );
1512
1513        // now remove from server and this time it should be able to fetch from local db
1514        client.forget_leaf(1).await;
1515        client.forget_leaf(2).await;
1516        let headers = lc.fetch_headers_in_range(1, 3).await.unwrap();
1517        assert_eq!(
1518            headers,
1519            vec![leaf1.header().clone(), leaf2.header().clone()]
1520        );
1521    }
1522
1523    #[tokio::test]
1524    #[test_log::test]
1525    async fn test_fetch_leaves_in_range_invalid_proof() {
1526        let client = TestClient::default();
1527        let lc = LightClient::from_genesis(
1528            SqlStorage::default().await.unwrap(),
1529            client.clone(),
1530            client.genesis().await,
1531        );
1532        client.return_invalid_proof(2).await;
1533        let err = lc.fetch_leaves_in_range(1, 3).await.unwrap_err();
1534        assert!(
1535            err.to_string().contains(
1536                "server returned proof with assumption, but we have no finalized upper bound to \
1537                 verify assumption"
1538            ),
1539            "{err:#}"
1540        );
1541    }
1542
1543    #[tokio::test]
1544    #[test_log::test]
1545    async fn test_fetch_headers_in_range_invalid_proof() {
1546        let client = TestClient::default();
1547        let lc = LightClient::from_genesis(
1548            SqlStorage::default().await.unwrap(),
1549            client.clone(),
1550            client.genesis().await,
1551        );
1552        client.return_invalid_proof(2).await;
1553        let err = lc.fetch_headers_in_range(1, 3).await.unwrap_err();
1554        assert!(
1555            err.to_string().contains(
1556                "server returned proof with assumption, but we have no finalized upper bound to \
1557                 verify assumption"
1558            ),
1559            "{err:#}"
1560        );
1561    }
1562
1563    #[tokio::test]
1564    #[test_log::test]
1565    async fn test_fetch_leaves_in_range_wrong_leaf() {
1566        let client = TestClient::default();
1567        let lc = LightClient::from_genesis(
1568            SqlStorage::default().await.unwrap(),
1569            client.clone(),
1570            client.genesis().await,
1571        );
1572        client.return_wrong_leaf(1, 2).await;
1573        let err = lc.fetch_leaves_in_range(1, 4).await.unwrap_err();
1574        assert!(err.to_string().contains("leaf hash mismatch"), "{err:#}");
1575    }
1576
1577    #[tokio::test]
1578    #[test_log::test]
1579    async fn test_fetch_leaves_for_ranges() {
1580        let client = TestClient::default();
1581        let lc = LightClient::from_genesis(
1582            SqlStorage::default().await.unwrap(),
1583            client.clone(),
1584            client.genesis().await,
1585        );
1586
1587        let mut expected = vec![];
1588        for height in [1usize, 2, 5, 6, 7] {
1589            expected.push(client.leaf(height).await);
1590        }
1591        let fetched = lc.fetch_leaves_for_ranges(&[1..3, 5..8]).await.unwrap();
1592        assert_eq!(fetched, expected);
1593    }
1594
1595    // Pins the coalescing cap written out in fetch_leaves_for_ranges to the default it copies.
1596    #[test]
1597    fn coalescing_cap_matches_query_service_default() {
1598        assert_eq!(
1599            hotshot_query_service::availability::Options::default().small_object_range_limit,
1600            500
1601        );
1602    }
1603
1604    // Touching ranges are what the scanner sends when a run straddles a chunk boundary, so they
1605    // must pass; unordered or overlapping ones must not, or the positional pairing downstream
1606    // would silently misalign.
1607    #[tokio::test]
1608    #[test_log::test]
1609    async fn test_fetch_leaves_for_ranges_ordering() {
1610        let client = TestClient::default();
1611        let lc = LightClient::from_genesis(
1612            SqlStorage::default().await.unwrap(),
1613            client.clone(),
1614            client.genesis().await,
1615        );
1616
1617        let mut expected = vec![];
1618        for height in 1..5usize {
1619            expected.push(client.leaf(height).await);
1620        }
1621        assert_eq!(
1622            lc.fetch_leaves_for_ranges(&[1..3, 3..5]).await.unwrap(),
1623            expected
1624        );
1625        for ranges in [[5..8, 1..3], [1..4, 3..6]] {
1626            let err = lc.fetch_leaves_for_ranges(&ranges).await.unwrap_err();
1627            assert!(err.to_string().contains("ascending"), "{err:#}");
1628        }
1629    }
1630
1631    #[tokio::test]
1632    #[test_log::test]
1633    async fn test_fetch_leaves_for_ranges_wrong_leaf() {
1634        let client = TestClient::default();
1635        let lc = LightClient::from_genesis(
1636            SqlStorage::default().await.unwrap(),
1637            client.clone(),
1638            client.genesis().await,
1639        );
1640
1641        // A substituted leaf fails the span's chain walk. The per-run path that failure falls
1642        // through to must reject it too: the leaf self-reports its real height, so it lands in the
1643        // bulk map under the wrong key and height 1 goes to an individually proven fetch.
1644        client.return_wrong_leaf(1, 2).await;
1645        #[allow(clippy::single_range_in_vec_init)]
1646        let err = lc.fetch_leaves_for_ranges(&[1..4]).await.unwrap_err();
1647        assert!(err.to_string().contains("wrong leaf"), "{err:#}");
1648    }
1649
1650    // Gaps too wide to coalesce into one span: the bulk of the leaves come from the ranges request
1651    // endpoint and each run is anchored by its own proof.
1652    #[tokio::test]
1653    #[test_log::test]
1654    async fn test_fetch_leaves_for_ranges_wide_span() {
1655        let client = TestClient::default();
1656        let lc = LightClient::from_genesis(
1657            SqlStorage::default().await.unwrap(),
1658            client.clone(),
1659            client.genesis().await,
1660        );
1661
1662        let mut expected = vec![];
1663        for height in [1usize, 2, 60, 61, 62] {
1664            expected.push(client.leaf(height).await);
1665        }
1666        let fetched = lc.fetch_leaves_for_ranges(&[1..3, 60..63]).await.unwrap();
1667        assert_eq!(fetched, expected);
1668    }
1669
1670    // The gaps are too wide to coalesce, so this reaches the ranges endpoint and exercises the
1671    // fallback when it fails; a coalesced span never asks the ranges endpoint at all.
1672    #[tokio::test]
1673    #[test_log::test]
1674    async fn test_fetch_leaves_for_ranges_without_ranges_endpoint() {
1675        let client = TestClient::default();
1676        let lc = LightClient::from_genesis(
1677            SqlStorage::default().await.unwrap(),
1678            client.clone(),
1679            client.genesis().await,
1680        );
1681        client.fail_leaf_ranges().await;
1682
1683        let mut expected = vec![];
1684        for height in [1usize, 2, 60, 61, 62] {
1685            expected.push(client.leaf(height).await);
1686        }
1687        let fetched = lc.fetch_leaves_for_ranges(&[1..3, 60..63]).await.unwrap();
1688        assert_eq!(fetched, expected);
1689    }
1690
1691    #[tokio::test]
1692    #[test_log::test]
1693    async fn test_fetch_headers_in_range_wrong_leaf() {
1694        let client = TestClient::default();
1695        let lc = LightClient::from_genesis(
1696            SqlStorage::default().await.unwrap(),
1697            client.clone(),
1698            client.genesis().await,
1699        );
1700        client.return_wrong_leaf(1, 2).await;
1701        let err = lc.fetch_headers_in_range(1, 4).await.unwrap_err();
1702        assert!(err.to_string().contains("leaf hash mismatch"), "{err:#}");
1703    }
1704
1705    #[tokio::test]
1706    #[test_log::test]
1707    async fn test_fetch_stake_table_twice_cache() {
1708        let client = TestClient::default();
1709        let genesis = client.genesis().await;
1710        let lc = LightClient::from_genesis(
1711            SqlStorage::default().await.unwrap(),
1712            client.clone(),
1713            genesis.clone(),
1714        );
1715
1716        // Fetch a dynamic stake table for the first time. We will need to get it from the server.
1717        let epoch = genesis.first_epoch_with_dynamic_stake_table + 5;
1718        let expected = client.quorum_for_epoch(epoch).await.into();
1719        assert_eq!(*lc.quorum_for_epoch(epoch).await.unwrap(), expected);
1720
1721        // Fetching the stake table again hits the cache.
1722        client.forget_quorum(epoch).await;
1723        assert_eq!(*lc.quorum_for_epoch(epoch).await.unwrap(), expected);
1724    }
1725
1726    #[tokio::test]
1727    #[test_log::test]
1728    async fn test_fetch_stake_table_twice_storage() {
1729        let db = SqlStorage::default().await.unwrap();
1730        let client = TestClient::default();
1731        let genesis = client.genesis().await;
1732        let lc = LightClient::from_genesis(db.clone(), client.clone(), genesis.clone());
1733
1734        // Fetch a dynamic stake table for the first time. We will need to get it from the server.
1735        let epoch = genesis.first_epoch_with_dynamic_stake_table + 5;
1736        let expected = client.quorum_for_epoch(epoch).await.into();
1737        assert_eq!(*lc.quorum_for_epoch(epoch).await.unwrap(), expected);
1738
1739        // Even if the in-memory cache is cleared, we can still get this stake table without hitting
1740        // the server, but fetching from the database.
1741        client.forget_quorum(epoch).await;
1742        let lc = LightClient::from_genesis(db, client.clone(), genesis);
1743        assert_eq!(*lc.quorum_for_epoch(epoch).await.unwrap(), expected);
1744    }
1745
1746    #[tokio::test]
1747    #[test_log::test]
1748    async fn test_fetch_stake_table_catchup_from_lower_bound() {
1749        let db = SqlStorage::default().await.unwrap();
1750        let client = TestClient::default();
1751        let genesis = client.genesis().await;
1752        let lc = LightClient::from_genesis(db.clone(), client.clone(), genesis.clone());
1753
1754        // Fetch a stake table, causing it to be stored in the stake table, making it usable as a
1755        // lower bound for later catchup.
1756        let epoch = genesis.first_epoch_with_dynamic_stake_table + 5;
1757        lc.quorum_for_epoch(epoch).await.unwrap();
1758
1759        // Cause the server to forget older epochs' stake table events, so that we can only catch up
1760        // successfully if we start from the saved lower bound.
1761        for epoch in 0..*epoch - 1 {
1762            client.forget_quorum(EpochNumber::new(epoch)).await;
1763        }
1764
1765        // Fetch a future stake table, catching up from the saved lower bound.
1766        let expected = client.quorum_for_epoch(epoch + 5).await.into();
1767        assert_eq!(*lc.quorum_for_epoch(epoch + 5).await.unwrap(), expected);
1768    }
1769
1770    #[tokio::test]
1771    #[test_log::test]
1772    async fn test_fetch_stake_table_catchup_without_header_proofs() {
1773        let client = TestClient::with_epoch_height(10);
1774        let genesis = client.genesis().await;
1775        let epoch = genesis.first_epoch_with_dynamic_stake_table + 5;
1776
1777        let db = SqlStorage::default().await.unwrap();
1778        let above_every_root = root_block_in_epoch(*epoch + 1, 10) as usize;
1779        db.insert_leaf(client.leaf(above_every_root).await)
1780            .await
1781            .unwrap();
1782        client.fail_header_proofs().await;
1783
1784        let lc = LightClient::from_genesis(db, client.clone(), genesis);
1785        let expected = client.quorum_for_epoch(epoch).await.into();
1786        assert_eq!(*lc.quorum_for_epoch(epoch).await.unwrap(), expected);
1787    }
1788
1789    #[tokio::test]
1790    #[test_log::test]
1791    async fn test_fetch_stake_table_cache_removal() {
1792        let db = SqlStorage::default().await.unwrap();
1793        let client = TestClient::default();
1794        let genesis = client.genesis().await;
1795        let lc = LightClient::from_genesis_with_options(
1796            db.clone(),
1797            client.clone(),
1798            genesis.clone(),
1799            LightClientOptions {
1800                num_stake_tables_in_memory: 2,
1801            },
1802        );
1803
1804        // Fetch two stake tables, causing the cache to be filled up.
1805        for i in 0..2 {
1806            let epoch = genesis.first_epoch_with_dynamic_stake_table + 5 + i;
1807            assert_eq!(
1808                *lc.quorum_for_epoch(epoch).await.unwrap(),
1809                client.quorum_for_epoch(epoch).await.into(),
1810            );
1811        }
1812        assert_eq!(lc.stake_tables.read().await.len(), 2);
1813
1814        // Fetch a third stake table, causing the second-oldest stake table to be deleted.
1815        assert_eq!(
1816            *lc.quorum_for_epoch(genesis.first_epoch_with_dynamic_stake_table + 10)
1817                .await
1818                .unwrap(),
1819            client
1820                .quorum_for_epoch(genesis.first_epoch_with_dynamic_stake_table + 10)
1821                .await
1822                .into(),
1823        );
1824        assert_eq!(lc.stake_tables.read().await.len(), 2);
1825
1826        // Even if the server forgets all earlier stake tables, we can still catch up because we
1827        // didn't remove the lower bound stake table.
1828        let epoch = genesis.first_epoch_with_dynamic_stake_table + 6;
1829        for i in 0..*epoch {
1830            client.forget_quorum(EpochNumber::new(i)).await;
1831        }
1832        assert_eq!(
1833            *lc.quorum_for_epoch(epoch).await.unwrap(),
1834            client.quorum_for_epoch(epoch).await.into(),
1835        );
1836    }
1837
1838    #[tokio::test]
1839    #[test_log::test]
1840    async fn test_fetch_stake_table_invalid() {
1841        let db = SqlStorage::default().await.unwrap();
1842        let client = TestClient::default();
1843        let genesis = client.genesis().await;
1844        let lc = LightClient::from_genesis(db.clone(), client.clone(), genesis.clone());
1845
1846        let epoch = genesis.first_epoch_with_dynamic_stake_table + 5;
1847        client.return_invalid_quorum(epoch).await;
1848        let err = lc.quorum_for_epoch(epoch).await.unwrap_err();
1849        assert!(
1850            err.to_string()
1851                .contains("does not match reconstructed hash"),
1852            "{err:#}"
1853        );
1854    }
1855
1856    /// Regression: epoch-root leaf proofs must locate a 2-chain when views immediately after
1857    /// the root are non-contiguous. Alternates failed view (gap) and ok view past the root.
1858    #[tokio::test]
1859    #[test_log::test]
1860    async fn test_fetch_stake_table_catchup_with_view_gaps_after_root() {
1861        let client = TestClient::with_epoch_height(10);
1862        let genesis = client.genesis().await;
1863        let lc = LightClient::from_genesis(
1864            SqlStorage::default().await.unwrap(),
1865            client.clone(),
1866            genesis.clone(),
1867        );
1868
1869        let target_epoch = genesis.first_epoch_with_dynamic_stake_table + 1;
1870        let root_height = root_block_in_epoch(*target_epoch - 1, 10) as usize;
1871        for i in [0, 2, 4] {
1872            client.add_view_gap_after(root_height + i).await;
1873        }
1874
1875        let expected = client.quorum_for_epoch(target_epoch).await.into();
1876        assert_eq!(*lc.quorum_for_epoch(target_epoch).await.unwrap(), expected);
1877    }
1878
1879    /// Regression: epoch-transition QC verification must actually check `next_epoch_qc`
1880    /// against the next epoch's quorum.
1881    #[tokio::test]
1882    #[test_log::test]
1883    async fn test_fetch_stake_table_catchup_rejects_tampered_next_epoch_qc() {
1884        let client = TestClient::with_epoch_height(10);
1885        let genesis = client.genesis().await;
1886        let lc = LightClient::from_genesis(
1887            SqlStorage::default().await.unwrap(),
1888            client.clone(),
1889            genesis.clone(),
1890        );
1891
1892        let target_epoch = genesis.first_epoch_with_dynamic_stake_table + 1;
1893        let root_height = root_block_in_epoch(*target_epoch - 1, 10) as usize;
1894        client.add_view_gap_after(root_height).await;
1895        client.corrupt_next_epoch_qc().await;
1896
1897        let err = lc.quorum_for_epoch(target_epoch).await.unwrap_err();
1898        assert!(
1899            err.to_string().contains("verifying next epoch QC")
1900                || err
1901                    .chain()
1902                    .any(|e| e.to_string().contains("verifying next epoch QC")),
1903            "{err:#}"
1904        );
1905    }
1906
1907    /// Regression: from-genesis catchup must seed the snapshot version by fetching the root at
1908    /// `first_dynamic - 2` (verified by the genesis stake table), not by reusing the version of
1909    /// the root in epoch `first_dynamic - 1`. Upgrades the root at `first_dynamic - 1` to
1910    /// CLIQUENET so it differs from the bootstrap root at `first_dynamic - 2`.
1911    #[tokio::test]
1912    #[test_log::test]
1913    async fn test_fetch_stake_table_catchup_genesis_bootstrap_version() {
1914        let client = TestClient::with_epoch_height(10);
1915        let genesis = client.genesis().await;
1916        let db = SqlStorage::default().await.unwrap();
1917        let lc = LightClient::from_genesis(db.clone(), client.clone(), genesis.clone());
1918
1919        let epoch = genesis.first_epoch_with_dynamic_stake_table;
1920        let root_height = root_block_in_epoch(*epoch - 1, 10);
1921        client.set_upgrade(root_height, NEW_PROTOCOL_VERSION).await;
1922
1923        lc.quorum_for_epoch(epoch).await.unwrap();
1924
1925        let (cached_epoch, _, cached_version, _) =
1926            db.stake_table_lower_bound(epoch).await.unwrap().unwrap();
1927        assert_eq!(cached_epoch, epoch);
1928        assert_eq!(cached_version, DRB_AND_HEADER_UPGRADE_VERSION);
1929    }
1930
1931    /// Regression: iter 1 of DB-load catchup must seed from `next_stored_version`, not
1932    /// `stored_version`.
1933    ///
1934    /// - insert stake table for `lower_bound_epoch` with `stored_version` = V4 and
1935    ///   `next_stored_version` = CLIQUENET
1936    /// - activate the upgrade at `target_epoch_root`
1937    /// - request quorum for `target_epoch`
1938    /// - fix: seed = `next_stored_version` = CLIQUENET, filter drops all validators, errors
1939    /// - previous bug: seed = `stored_version` = V4, no filter, catchup wrongly succeeds
1940    ///
1941    /// Brittle: asserts that an error happens, not the filter version. Avoids x25519/p2p
1942    /// plumbing in the shared test client.
1943    #[tokio::test]
1944    #[test_log::test]
1945    async fn test_fetch_stake_table_catchup_db_load_advances_snapshot_version() {
1946        use crate::client::Client;
1947
1948        let client = TestClient::with_epoch_height(10);
1949        let genesis = client.genesis().await;
1950        let db = SqlStorage::default().await.unwrap();
1951        let lc = LightClient::from_genesis(db.clone(), client.clone(), genesis.clone());
1952
1953        let lower_bound_epoch = genesis.first_epoch_with_dynamic_stake_table;
1954        let target_epoch = lower_bound_epoch + 1;
1955        let target_epoch_root = root_block_in_epoch(*target_epoch - 2, 10);
1956
1957        let mut prev_state = StakeTableState::default();
1958        for event in client.stake_table_events(lower_bound_epoch).await.unwrap() {
1959            prev_state.apply_event(event).unwrap().unwrap();
1960        }
1961        db.insert_stake_table(
1962            lower_bound_epoch,
1963            &prev_state,
1964            DRB_AND_HEADER_UPGRADE_VERSION,
1965            NEW_PROTOCOL_VERSION,
1966        )
1967        .await
1968        .unwrap();
1969
1970        client
1971            .set_upgrade(target_epoch_root, NEW_PROTOCOL_VERSION)
1972            .await;
1973
1974        let err = lc.quorum_for_epoch(target_epoch).await.unwrap_err();
1975        assert!(
1976            err.to_string()
1977                .contains("No validators met the minimum criteria")
1978                || err.chain().any(|e| e
1979                    .to_string()
1980                    .contains("No validators met the minimum criteria")),
1981            "{err:#}"
1982        );
1983    }
1984
1985    /// Regression: catchup filtered the candidate active set at `root.version()` (root in
1986    /// epoch `e-1`) instead of the snapshot root's version (epoch `e-2`). When those versions
1987    /// differ across CLIQUENET, the LC diverges from the espresso node.
1988    #[tokio::test]
1989    #[test_log::test]
1990    async fn test_fetch_stake_table_catchup_uses_snapshot_root_version() {
1991        use crate::client::Client;
1992
1993        let client = TestClient::with_epoch_height(10);
1994        let genesis = client.genesis().await;
1995        let db = SqlStorage::default().await.unwrap();
1996        let lc = LightClient::from_genesis(db.clone(), client.clone(), genesis.clone());
1997
1998        let prev_epoch = genesis.first_epoch_with_dynamic_stake_table;
1999        let target_epoch = prev_epoch + 1;
2000        let root_height = root_block_in_epoch(*target_epoch - 1, 10);
2001
2002        let mut prev_state = StakeTableState::default();
2003        for event in client.stake_table_events(prev_epoch).await.unwrap() {
2004            prev_state.apply_event(event).unwrap().unwrap();
2005        }
2006        db.insert_stake_table(
2007            prev_epoch,
2008            &prev_state,
2009            DRB_AND_HEADER_UPGRADE_VERSION,
2010            DRB_AND_HEADER_UPGRADE_VERSION,
2011        )
2012        .await
2013        .unwrap();
2014
2015        client.set_upgrade(root_height, NEW_PROTOCOL_VERSION).await;
2016
2017        let expected = client.quorum_for_epoch(target_epoch).await.into();
2018        assert_eq!(*lc.quorum_for_epoch(target_epoch).await.unwrap(), expected);
2019    }
2020
2021    /// Regression: CLIQUENET active-set filter must run on the cache-hit path using the stored
2022    /// version, dropping validators missing x25519/p2p.
2023    #[tokio::test]
2024    #[test_log::test]
2025    async fn test_fetch_stake_table_cliquenet_filter() {
2026        let db = SqlStorage::default().await.unwrap();
2027        let client = TestClient::default();
2028        let genesis = client.genesis().await;
2029        let lc = LightClient::from_genesis(db.clone(), client.clone(), genesis.clone());
2030
2031        let mut complete: RegisteredValidator<PubKey> = random_validator().into();
2032        complete.x25519_key = Some(x25519::PublicKey::try_from([42u8; 32].as_slice()).unwrap());
2033        complete.p2p_addr = Some("127.0.0.1:9000".parse::<NetAddr>().unwrap());
2034
2035        let incomplete: RegisteredValidator<PubKey> = random_validator().into();
2036        assert!(incomplete.x25519_key.is_none());
2037        assert!(incomplete.p2p_addr.is_none());
2038
2039        let mut validators = RegisteredValidatorMap::default();
2040        validators.insert(complete.account, complete.clone());
2041        validators.insert(incomplete.account, incomplete.clone());
2042
2043        let complete_bls = complete.stake_table_key.expect("authenticated validator");
2044        let incomplete_bls = incomplete.stake_table_key.expect("authenticated validator");
2045        let mut used_bls = HashSet::default();
2046        used_bls.insert(complete_bls);
2047        used_bls.insert(incomplete_bls);
2048
2049        let mut used_schnorr = HashSet::default();
2050        used_schnorr.insert(
2051            complete
2052                .state_ver_key
2053                .clone()
2054                .expect("random_validator has valid schnorr key"),
2055        );
2056        used_schnorr.insert(
2057            incomplete
2058                .state_ver_key
2059                .clone()
2060                .expect("random_validator has valid schnorr key"),
2061        );
2062
2063        let mut used_x25519 = HashSet::default();
2064        used_x25519.insert(complete.x25519_key.unwrap());
2065
2066        let state = StakeTableState::new(
2067            validators,
2068            Default::default(),
2069            used_bls,
2070            used_schnorr,
2071            used_x25519,
2072        );
2073
2074        let epoch = genesis.first_epoch_with_dynamic_stake_table + 1;
2075        db.insert_stake_table(epoch, &state, NEW_PROTOCOL_VERSION, NEW_PROTOCOL_VERSION)
2076            .await
2077            .unwrap();
2078
2079        let quorum = lc.quorum_for_epoch(epoch).await.unwrap();
2080        let expected: StakeTable = vec![StakeTableEntry {
2081            stake_key: complete_bls,
2082            stake_amount: complete.stake,
2083        }]
2084        .into();
2085        assert_eq!(*quorum, expected);
2086
2087        // Cross-check: the same state under EPOCH_VERSION (pre-cliquenet) keeps both. This
2088        // guards against the filter being unconditionally on.
2089        let pre_cliquenet = ValidatorSet::from_state(&state, EPOCH_VERSION).unwrap();
2090        assert_eq!(pre_cliquenet.active_validators().len(), 2);
2091    }
2092
2093    /// Regression: catchup must thread the snapshot version end-to-end through reconstruction
2094    /// and cache it with each replayed epoch.
2095    #[tokio::test]
2096    #[test_log::test]
2097    async fn test_fetch_stake_table_cliquenet_filter_reconstruction() {
2098        let db = SqlStorage::default().await.unwrap();
2099        let client = TestClient::default();
2100        let genesis = client.genesis().await;
2101        let lc = LightClient::from_genesis(db.clone(), client.clone(), genesis.clone());
2102
2103        let epoch = genesis.first_epoch_with_dynamic_stake_table + 2;
2104        assert!(db.stake_table_lower_bound(epoch).await.unwrap().is_none());
2105
2106        let expected = client.quorum_for_epoch(epoch).await.into();
2107        assert_eq!(*lc.quorum_for_epoch(epoch).await.unwrap(), expected);
2108
2109        let (cached_epoch, _, cached_version, _) =
2110            db.stake_table_lower_bound(epoch).await.unwrap().unwrap();
2111        assert_eq!(cached_epoch, epoch);
2112        assert_eq!(cached_version, DRB_AND_HEADER_UPGRADE_VERSION);
2113    }
2114
2115    #[tokio::test]
2116    #[test_log::test]
2117    async fn test_fetch_payload() {
2118        let client = TestClient::default();
2119        let lc = LightClient::from_genesis(
2120            SqlStorage::default().await.unwrap(),
2121            client.clone(),
2122            client.genesis().await,
2123        );
2124
2125        let mut hashes = vec![];
2126        for i in 1..10 {
2127            let payload = client.payload(i).await;
2128            let res = lc.fetch_payload(BlockId::Number(i)).await.unwrap();
2129            assert_eq!(res.data(), &payload);
2130            assert_eq!(res.height(), i as u64);
2131            assert_eq!(res.block_hash(), client.leaf(i).await.block_hash());
2132            assert_eq!(res.hash(), client.leaf(i).await.payload_hash());
2133            hashes.push(res.hash());
2134        }
2135
2136        // Test ranged fetching.
2137        let blocks = lc.fetch_blocks_in_range(1, 10).await.unwrap();
2138        let vid = lc.fetch_vid_common_in_range(1, 10).await.unwrap();
2139        assert_eq!(blocks.len(), 9);
2140        assert_eq!(vid.len(), 9);
2141        for (i, (hash, block, vid)) in izip!(hashes, blocks, vid).enumerate() {
2142            assert_eq!(block.height() as usize, i + 1);
2143            assert_eq!(vid.height() as usize, i + 1);
2144            assert_eq!(block.payload_hash(), hash);
2145            assert_eq!(vid.payload_hash(), hash);
2146        }
2147    }
2148
2149    #[tokio::test]
2150    #[test_log::test]
2151    async fn test_fetch_payload_invalid() {
2152        let client = TestClient::default();
2153        let lc = LightClient::from_genesis(
2154            SqlStorage::default().await.unwrap(),
2155            client.clone(),
2156            client.genesis().await,
2157        );
2158
2159        client.return_invalid_payload(1).await;
2160        let err = lc.fetch_payload(BlockId::Number(1)).await.unwrap_err();
2161        assert!(
2162            err.to_string()
2163                .contains("commitment of payload does not match commitment in header"),
2164            "{err:#}"
2165        );
2166    }
2167
2168    // The leaf ranges request succeeds while the payload one fails, so this exercises the payload
2169    // fallback on its own.
2170    #[tokio::test]
2171    #[test_log::test]
2172    async fn test_fetch_blocks_for_ranges_without_ranges_endpoint() {
2173        let client = TestClient::default();
2174        let lc = LightClient::from_genesis(
2175            SqlStorage::default().await.unwrap(),
2176            client.clone(),
2177            client.genesis().await,
2178        );
2179        client.fail_payload_proof_ranges().await;
2180        client.payload(62).await;
2181
2182        let fetched = lc
2183            .fetch_blocks_and_vid_common_for_ranges(&[1..3, 60..63])
2184            .await
2185            .unwrap();
2186        assert_eq!(
2187            fetched
2188                .iter()
2189                .map(|(block, _)| block.height())
2190                .collect::<Vec<_>>(),
2191            [1, 2, 60, 61, 62]
2192        );
2193    }
2194
2195    #[tokio::test]
2196    #[test_log::test]
2197    async fn test_fetch_blocks_for_ranges_invalid_proof() {
2198        let client = TestClient::default();
2199        let lc = LightClient::from_genesis(
2200            SqlStorage::default().await.unwrap(),
2201            client.clone(),
2202            client.genesis().await,
2203        );
2204        client.payload(7).await;
2205
2206        client.return_invalid_payload(6).await;
2207        let err = lc
2208            .fetch_blocks_and_vid_common_for_ranges(&[1..3, 5..8])
2209            .await
2210            .unwrap_err();
2211        assert!(
2212            err.to_string()
2213                .contains("commitment of payload does not match commitment in header"),
2214            "{err:#}"
2215        );
2216    }
2217
2218    #[tokio::test]
2219    #[test_log::test]
2220    async fn test_fetch_namespace() {
2221        let client = TestClient::default();
2222        let lc = LightClient::from_genesis(
2223            SqlStorage::default().await.unwrap(),
2224            client.clone(),
2225            client.genesis().await,
2226        );
2227
2228        for i in 1..10 {
2229            let leaf = client.leaf(i).await;
2230            let payload = client.payload(i).await;
2231
2232            for id in [
2233                BlockId::Number(i),
2234                BlockId::Hash(leaf.block_hash()),
2235                BlockId::PayloadHash(leaf.payload_hash()),
2236            ] {
2237                // Request a non-empty namespace.
2238                let ns_index = NsIndex::from(0);
2239                let tx = payload
2240                    .transaction(&TransactionIndex {
2241                        ns_index,
2242                        position: 0,
2243                    })
2244                    .unwrap();
2245                let txs = lc.fetch_namespace(id, tx.namespace()).await.unwrap();
2246                assert_eq!(txs, std::slice::from_ref(&tx));
2247
2248                // Request an empty namespace.
2249                let txs = lc
2250                    .fetch_namespace(id, NamespaceId::from(u64::from(tx.namespace()) + 1))
2251                    .await
2252                    .unwrap();
2253                assert_eq!(txs, []);
2254            }
2255        }
2256
2257        // Fetch by range.
2258        let ns = client
2259            .payload(1)
2260            .await
2261            .transaction(&TransactionIndex {
2262                ns_index: 0.into(),
2263                position: 0,
2264            })
2265            .unwrap()
2266            .namespace();
2267        let namespaces = lc.fetch_namespaces_in_range(1, 10, ns).await.unwrap();
2268        assert_eq!(namespaces.len(), 9);
2269    }
2270
2271    #[tokio::test]
2272    #[test_log::test]
2273    async fn test_fetch_all_namespaces_in_range() {
2274        let client = TestClient::default();
2275        let lc = LightClient::from_genesis(
2276            SqlStorage::default().await.unwrap(),
2277            client.clone(),
2278            client.genesis().await,
2279        );
2280
2281        // Request the namespaces from the first few blocks (which may differ per block), plus one
2282        // that is absent everywhere.
2283        let mut requested = vec![];
2284        for height in 1..4 {
2285            requested.push(
2286                client
2287                    .payload(height)
2288                    .await
2289                    .transaction(&TransactionIndex {
2290                        ns_index: 0.into(),
2291                        position: 0,
2292                    })
2293                    .unwrap()
2294                    .namespace(),
2295            );
2296        }
2297        let absent =
2298            NamespaceId::from(requested.iter().map(|ns| u64::from(*ns)).max().unwrap() + 1);
2299        requested.push(absent);
2300
2301        let (start, end) = (1, 10);
2302        let blocks = lc
2303            .fetch_all_namespaces_in_range(start, end, &requested)
2304            .await
2305            .unwrap();
2306        assert_eq!(blocks.len(), end - start);
2307
2308        for (i, block) in blocks.iter().enumerate() {
2309            let height = i + 1;
2310            let header = client.leaf(height).await.header().clone();
2311
2312            // The map contains exactly the requested namespaces present in this block, and the
2313            // absent namespace is never included.
2314            assert!(!block.contains_key(&absent), "block {height}");
2315            for &ns in &requested {
2316                let expected = lc
2317                    .fetch_namespace(BlockId::Number(height), ns)
2318                    .await
2319                    .unwrap();
2320                if header.ns_table().find_ns_id(&ns).is_some() {
2321                    assert_eq!(block.get(&ns), Some(&expected), "block {height}, ns {ns}");
2322                } else {
2323                    assert!(!block.contains_key(&ns), "block {height}, ns {ns}");
2324                }
2325            }
2326        }
2327    }
2328
2329    #[tokio::test]
2330    #[test_log::test]
2331    async fn test_fetch_namespace_invalid() {
2332        let client = TestClient::default();
2333        let lc = LightClient::from_genesis(
2334            SqlStorage::default().await.unwrap(),
2335            client.clone(),
2336            client.genesis().await,
2337        );
2338
2339        let payload = client.payload(1).await;
2340        let ns_index = NsIndex::from(0);
2341        let tx = payload
2342            .transaction(&TransactionIndex {
2343                ns_index,
2344                position: 0,
2345            })
2346            .unwrap();
2347
2348        client.return_invalid_payload(1).await;
2349        let err = lc
2350            .fetch_namespace(BlockId::Number(1), tx.namespace())
2351            .await
2352            .unwrap_err();
2353        assert!(
2354            err.to_string().contains("invalid namespace proof"),
2355            "{err:#}"
2356        );
2357    }
2358
2359    #[tokio::test]
2360    #[test_log::test]
2361    async fn test_genesis_round_trip() {
2362        let client = TestClient::default();
2363        let genesis = client.genesis().await;
2364        let lc = LightClient::from_genesis(
2365            SqlStorage::default().await.unwrap(),
2366            client.clone(),
2367            genesis.clone(),
2368        );
2369        assert_eq!(lc.genesis(), genesis);
2370    }
2371}
2372
2373#[cfg(all(test, feature = "rlp"))]
2374mod rlp_test {
2375    use alloy::primitives::U256;
2376    use alloy_rlp::{Decodable, Encodable};
2377    use hotshot_types::traits::signature_key::SignatureKey;
2378
2379    use super::*;
2380
2381    #[test_log::test]
2382    fn rlp_genesis_round_trip() {
2383        let genesis = Genesis {
2384            epoch_height: 1000,
2385            first_epoch_with_dynamic_stake_table: EpochNumber::new(3),
2386            stake_table: vec![StakeTableEntry {
2387                stake_key: PubKey::generated_from_seed_indexed(
2388                    Default::default(),
2389                    Default::default(),
2390                )
2391                .0,
2392                stake_amount: U256::MAX,
2393            }],
2394            chain_id: DECAF_CHAIN_ID,
2395        };
2396
2397        let mut buf = vec![];
2398        genesis.encode(&mut buf);
2399
2400        let mut buf = buf.as_slice();
2401        assert_eq!(genesis, Genesis::decode(&mut buf).unwrap());
2402        assert!(buf.is_empty());
2403    }
2404}