Skip to main content

espresso_node/api/
light_client.rs

1use std::{cmp::Ordering, collections::HashMap, fmt::Display, sync::Arc, time::Duration};
2
3use anyhow::{Result, bail, ensure};
4use committable::{Commitment, Committable};
5use disco_types::status::StatusCode;
6use espresso_types::{BlockMerkleTree, Header, NsIndex, NsProof, SeqTypes};
7use futures::{TryStreamExt, future::try_join, stream::StreamExt};
8use hotshot_query_service::{
9    Error, QueryError,
10    availability::{AvailabilityDataSource, LeafQueryData, PayloadQueryData, VidCommonQueryData},
11    data_source::{VersionedDataSource, storage::NodeStorage},
12    merklized_state::{MerklizedStateDataSource, MerklizedStateHeightPersistence, Snapshot},
13    node::BlockId,
14    types::HeightIndexed,
15};
16use hotshot_types::simple_certificate::QuorumCertificate2;
17use itertools::izip;
18use jf_merkle_tree_compat::{
19    DigestAlgorithm, MerkleTreeScheme,
20    internal::MerkleNode,
21    prelude::{Sha3Digest, Sha3Node},
22};
23use light_client::{
24    client::NAMESPACES_PARAM_TAG,
25    consensus::{header::HeaderProof, leaf::LeafProof, namespace::NamespaceProof},
26};
27use tagged_base64::TaggedBase64;
28use tokio::spawn;
29use versions::NEW_PROTOCOL_VERSION;
30
31/// Construct a proof that the requested leaf is finalized.
32///
33/// The `finalized` hint is honored when it yields a bounded proof no longer than a direct finality
34/// proof, since the client can verify hint-based proofs without signature checks.
35pub(crate) async fn get_leaf_proof<State>(
36    state: &State,
37    requested_leaf: LeafQueryData<SeqTypes>,
38    finalized: Option<usize>,
39    fetch_timeout: Duration,
40    chain_limit: usize,
41) -> Result<LeafProof, Error>
42where
43    State: AvailabilityDataSource<SeqTypes> + VersionedDataSource + Clone + Send + Sync + 'static,
44    for<'a> State::ReadOnly<'a>: NodeStorage<SeqTypes>,
45{
46    let requested = requested_leaf.height() as usize;
47    let new_protocol = requested_leaf.header().version() >= NEW_PROTOCOL_VERSION;
48
49    let mut hint = finalized.filter(|finalized| finalized.saturating_sub(requested) <= chain_limit);
50
51    // New-protocol chains cannot terminate early, so a nearby cert2 may yield a shorter proof.
52    if let Some(finalized) = hint
53        && new_protocol
54        && let Some(cert2_height) = earliest_cert2_height(state, requested as u64).await
55        && cert2_height + 1 < finalized as u64
56    {
57        hint = None;
58    }
59
60    if let Some(finalized) = hint {
61        get_leaf_proof_with_finalized_assumption(
62            state,
63            requested_leaf,
64            finalized,
65            fetch_timeout,
66            chain_limit,
67        )
68        .await
69    } else if new_protocol {
70        get_leaf_proof_with_cert2(state, requested_leaf, fetch_timeout, chain_limit).await
71    } else {
72        get_leaf_proof_with_leaf_chain(state, requested_leaf, fetch_timeout, chain_limit).await
73    }
74}
75
76/// The earliest stored cert2 height at or after `height`. Errors are treated as no cert2: this
77/// only chooses between proof strategies, and the chosen path will surface any persistent error.
78async fn earliest_cert2_height<State>(state: &State, height: u64) -> Option<u64>
79where
80    State: VersionedDataSource,
81    for<'a> State::ReadOnly<'a>: NodeStorage<SeqTypes>,
82{
83    let cert2 = state
84        .read()
85        .await
86        .ok()?
87        .load_earliest_cert2(height)
88        .await
89        .ok()??;
90    Some(cert2.data.block_number)
91}
92
93/// Build a leaf proof for a legacy leaf from the justify QCs of the leaves that follow it.
94pub(crate) async fn get_leaf_proof_with_leaf_chain<State>(
95    state: &State,
96    requested_leaf: LeafQueryData<SeqTypes>,
97    fetch_timeout: Duration,
98    chain_limit: usize,
99) -> Result<LeafProof, Error>
100where
101    State: AvailabilityDataSource<SeqTypes> + VersionedDataSource + Clone + Send + Sync + 'static,
102    for<'a> State::ReadOnly<'a>: NodeStorage<SeqTypes>,
103{
104    let requested = requested_leaf.height() as usize;
105    let mut tx = state.read().await.map_err(internal)?;
106    let latest_height = NodeStorage::block_height(&mut tx).await.map_err(internal)?;
107    drop(tx);
108
109    let mut leaves = state.get_leaf_range(requested + 1..latest_height).await;
110    let mut proof = LeafProof::default();
111    let requested_leaf_qc = requested_leaf.qc().clone();
112    proof.push(requested_leaf);
113
114    let mut remaining = chain_limit;
115    while let Some(leaf) = leaves.next().await {
116        let leaf = leaf
117            .with_timeout(fetch_timeout)
118            .await
119            .ok_or_else(|| not_found("missing leaves"))?;
120
121        remaining = remaining
122            .checked_sub(1)
123            .ok_or_else(|| chain_too_long(requested, chain_limit))?;
124
125        // HotStuff commit rules cannot terminate a chain of new-protocol leaves; switch to cert2.
126        if leaf.header().version() >= NEW_PROTOCOL_VERSION {
127            complete_proof_with_cert2(
128                state,
129                &mut proof,
130                leaf,
131                requested_leaf_qc.clone(),
132                fetch_timeout,
133                remaining,
134            )
135            .await?;
136            return Ok(proof);
137        }
138
139        if proof.push(leaf) {
140            return Ok(proof);
141        }
142    }
143
144    // There is no stored legacy QC chain to fall back on: it could only close a chain whose tip was
145    // legacy, and every network is past the 0.6 cutover, so running out of leaves is a 404.
146    Err(not_found("not enough leaves to prove finality"))
147}
148
149/// Build a leaf proof for the new protocol using certificate2 finality.
150///
151/// Certificate2 directly commits the leaf at `cert2.data.block_number`. The indirect commit rule then
152/// commits all of that leaf's uncommitted ancestors. If cert2 directly commits the requested leaf,
153/// the proof contains only that leaf plus cert2. Otherwise, the proof includes the chain from the
154/// requested leaf through the directly committed descendant; the verifier checks parent commitments
155/// to prove the requested leaf is on that finalized chain.
156pub(crate) async fn get_leaf_proof_with_cert2<State>(
157    state: &State,
158    requested_leaf: LeafQueryData<SeqTypes>,
159    fetch_timeout: Duration,
160    chain_limit: usize,
161) -> Result<LeafProof, Error>
162where
163    State: AvailabilityDataSource<SeqTypes> + VersionedDataSource + Clone + Send + Sync + 'static,
164    for<'a> State::ReadOnly<'a>: NodeStorage<SeqTypes>,
165{
166    let mut proof = LeafProof::default();
167    let requested_leaf_qc = requested_leaf.qc().clone();
168    complete_proof_with_cert2(
169        state,
170        &mut proof,
171        requested_leaf,
172        requested_leaf_qc,
173        fetch_timeout,
174        chain_limit,
175    )
176    .await?;
177    Ok(proof)
178}
179
180/// Extend `proof` from `leaf` to the leaf directly committed by the earliest stored cert2, adding
181/// at most `chain_limit` leaves.
182async fn complete_proof_with_cert2<State>(
183    state: &State,
184    proof: &mut LeafProof,
185    leaf: LeafQueryData<SeqTypes>,
186    requested_leaf_qc: QuorumCertificate2<SeqTypes>,
187    fetch_timeout: Duration,
188    chain_limit: usize,
189) -> Result<(), Error>
190where
191    State: AvailabilityDataSource<SeqTypes> + VersionedDataSource + Clone + Send + Sync + 'static,
192    for<'a> State::ReadOnly<'a>: NodeStorage<SeqTypes>,
193{
194    let start_height = leaf.height();
195    let start_commit = leaf.leaf().commit();
196    let limit = chain_limit as u64;
197
198    // The new leaf may already complete a HotStuff chain begun before the protocol cutover.
199    if proof.push(leaf) {
200        return Ok(());
201    }
202
203    let stored = state
204        .read()
205        .await
206        .map_err(internal)?
207        .load_earliest_cert2(start_height)
208        .await
209        .map_err(internal)?;
210
211    let earliest = stored.as_ref().map(|cert2| cert2.data.block_number);
212    let Some(cert2) = stored.filter(|cert2| cert2.data.block_number <= start_height + limit) else {
213        // An indirectly finalized leaf has no cert2 of its own, so its descendants are asked, one
214        // at a time. A cert2 no peer has never resolves, hence the timeout per step.
215        let known_height = async {
216            let mut tx = state.read().await?;
217            anyhow::Ok(NodeStorage::block_height(&mut tx).await? as u64)
218        }
219        .await
220        // A miss either way, and a failing store should not be asked once more per height.
221        .inspect_err(|err| {
222            tracing::warn!(
223                %err,
224                height = start_height,
225                "failed to load block height, skipping cert2 fetch"
226            )
227        })
228        .ok();
229        if let Some(known_height) = known_height {
230            let end = (start_height + limit + 1).min(known_height);
231            let state = state.clone();
232            spawn(async move {
233                for height in start_height..end {
234                    if state
235                        .get_cert2(height)
236                        .await
237                        .with_timeout(fetch_timeout)
238                        .await
239                        .is_some()
240                    {
241                        break;
242                    }
243                }
244            });
245        }
246        return Err(not_found(match earliest {
247            None => format!("no cert2 finality proof available at or after height {start_height}"),
248            Some(height) => format!(
249                "earliest cert2 finality proof (height {height}) is more than {chain_limit} \
250                 leaves past height {start_height}"
251            ),
252        }));
253    };
254
255    let cert2_height = cert2.data.block_number;
256    if cert2_height < start_height {
257        return Err(not_found(
258            "cert2 finality proof is older than requested leaf",
259        ));
260    }
261
262    if cert2_height == start_height {
263        if start_commit != cert2.data.leaf_commit {
264            return Err(internal("stored cert2 does not finalize the expected leaf"));
265        }
266        proof.add_certificate(Arc::new(cert2), requested_leaf_qc);
267        return Ok(());
268    }
269
270    let mut leaves = state
271        .get_leaf_range(start_height as usize + 1..cert2_height as usize + 1)
272        .await;
273    // Extend the proof chain until we reach the leaf directly committed by cert2. Once that leaf is
274    // present and matches cert2's commitment, the requested leaf is finalized by the indirect
275    // commit rule.
276    while let Some(leaf) = leaves.next().await {
277        let leaf = leaf
278            .with_timeout(fetch_timeout)
279            .await
280            .ok_or_else(|| not_found("missing leaves"))?;
281
282        if leaf.height() == cert2_height {
283            if leaf.leaf().commit() != cert2.data.leaf_commit {
284                return Err(internal("stored cert2 does not finalize the expected leaf"));
285            }
286            if !proof.push(leaf) {
287                proof.add_certificate(Arc::new(cert2), requested_leaf_qc);
288            }
289            return Ok(());
290        }
291        if proof.push(leaf) {
292            return Ok(());
293        }
294    }
295
296    Err(not_found("missing cert2 leaf"))
297}
298
299pub(crate) async fn get_leaf_proof_with_finalized_assumption<State>(
300    state: &State,
301    requested_leaf: LeafQueryData<SeqTypes>,
302    finalized: usize,
303    fetch_timeout: Duration,
304    chain_limit: usize,
305) -> Result<LeafProof, Error>
306where
307    State: AvailabilityDataSource<SeqTypes>,
308{
309    let requested = requested_leaf.height() as usize;
310    // If we have a known-finalized block, we will not need a final 2-chain of QCs to prove
311    // the last leaf in the result finalized, since we will either terminate with a 3-chain
312    // of leaves or at the `finalized` leaf.
313    if finalized <= requested {
314        return Err(Error::Custom {
315            message: format!(
316                "finalized leaf height ({finalized}) must be greater than requested ({requested})"
317            ),
318            status: StatusCode::BAD_REQUEST,
319        });
320    }
321    if finalized - requested > chain_limit {
322        return Err(Error::Custom {
323            message: format!(
324                "finalized leaf height ({finalized}) is more than {chain_limit} blocks past the \
325                 requested leaf ({requested}); request a proof without the finalized parameter \
326                 instead"
327            ),
328            status: StatusCode::BAD_REQUEST,
329        });
330    }
331
332    let mut leaves = state.get_leaf_range(requested + 1..finalized).await;
333    let mut proof = LeafProof::default();
334    proof.push(requested_leaf);
335
336    while let Some(leaf) = leaves.next().await {
337        let leaf = leaf
338            .with_timeout(fetch_timeout)
339            .await
340            .ok_or_else(|| not_found("missing leaves"))?;
341
342        if proof.push(leaf) {
343            return Ok(proof);
344        }
345    }
346
347    Ok(proof)
348}
349
350pub(crate) async fn get_header_proof<State>(
351    state: &State,
352    root: u64,
353    requested: BlockId<SeqTypes>,
354    fetch_timeout: Duration,
355) -> Result<HeaderProof, Error>
356where
357    State: AvailabilityDataSource<SeqTypes>
358        + MerklizedStateDataSource<SeqTypes, BlockMerkleTree, { BlockMerkleTree::ARITY }>
359        + MerklizedStateHeightPersistence
360        + VersionedDataSource,
361{
362    let header = state
363        .get_header(requested)
364        .await
365        .with_timeout(fetch_timeout)
366        .await
367        .ok_or_else(|| not_found(format!("unknown header {requested}")))?;
368    if header.height() >= root {
369        return Err(Error::Custom {
370            message: format!(
371                "height ({}) must be less than root ({root})",
372                header.height()
373            ),
374            status: StatusCode::BAD_REQUEST,
375        });
376    }
377    let path = MerklizedStateDataSource::<SeqTypes, BlockMerkleTree, _>::get_path(
378        state,
379        Snapshot::Index(root),
380        header.height(),
381    )
382    .await;
383    let path = match path {
384        Ok(path) => path,
385        Err(err) if matches!(err.status(), StatusCode::NOT_FOUND | StatusCode::GONE) => {
386            // No usable snapshot at `root` (pruned or missing): rebuild from the newest one.
387            let head = state.get_last_state_height().await.map_err(merkle_error)? as u64;
388            if head <= root {
389                return Err(merkle_error(err));
390            }
391            return rebuild_header_path(state, root, header, head, fetch_timeout).await;
392        },
393        Err(err) => return Err(merkle_error(err)),
394    };
395
396    Ok(HeaderProof::new(header, path))
397}
398
399fn merkle_error(source: QueryError) -> Error {
400    Error::MerklizedState {
401        source: source.into(),
402    }
403}
404
405/// Prove `header` against `root` using the snapshot at `head`, when the state at `root` is pruned.
406///
407/// The result is verified against the commitment in header `root`.
408async fn rebuild_header_path<State>(
409    state: &State,
410    root: u64,
411    header: Header,
412    head: u64,
413    fetch_timeout: Duration,
414) -> Result<HeaderProof, Error>
415where
416    State: AvailabilityDataSource<SeqTypes>
417        + MerklizedStateDataSource<SeqTypes, BlockMerkleTree, { BlockMerkleTree::ARITY }>,
418{
419    let get_path = |height| {
420        MerklizedStateDataSource::<SeqTypes, BlockMerkleTree, _>::get_path(
421            state,
422            Snapshot::Index(head),
423            height,
424        )
425    };
426    let frontier = get_path(root - 1).await.map_err(merkle_error)?;
427    let target = get_path(header.height()).await.map_err(merkle_error)?;
428    let root_header = state
429        .get_header(BlockId::Number(root as usize))
430        .await
431        .with_timeout(fetch_timeout)
432        .await
433        .ok_or_else(|| not_found(format!("unknown header {root}")))?;
434
435    let proof = rebuild_block_proof(root, &frontier, &target).map_err(internal)?;
436    let proof = HeaderProof::new(header, proof);
437    proof
438        .verify_ref(root_header.block_merkle_tree_root())
439        .map_err(|err| {
440            internal(format!(
441                "rebuilt block merkle path does not match root {root}: {err:#}"
442            ))
443        })?;
444    Ok(proof)
445}
446
447type BlockMerkleProof = <BlockMerkleTree as MerkleTreeScheme>::MembershipProof;
448type BlockMerkleNode = MerkleNode<Commitment<Header>, u64, Sha3Node>;
449
450/// The path to `target.pos` in the block tree of the first `num_leaves` leaves.
451///
452/// Both proofs come from one snapshot with more than `num_leaves` leaves; `frontier` is for leaf
453/// `num_leaves - 1`. Subtrees left of the frontier are full and unchanged since, subtrees right of
454/// it are empty, and the frontier nodes are recomputed.
455pub(crate) fn rebuild_block_proof(
456    num_leaves: u64,
457    frontier: &BlockMerkleProof,
458    target: &BlockMerkleProof,
459) -> Result<BlockMerkleProof> {
460    ensure!(
461        frontier.pos + 1 == num_leaves,
462        "frontier proof is for leaf {}, expected {}",
463        frontier.pos,
464        num_leaves.wrapping_sub(1),
465    );
466    ensure!(target.pos < num_leaves, "target leaf is beyond the tree");
467    ensure!(
468        frontier.proof.len() == target.proof.len(),
469        "proofs have different heights"
470    );
471
472    // Value of the frontier node at each level, starting with the leaf.
473    let mut edge_values = vec![frontier.proof[0].value()];
474    for (level, node) in frontier.proof.iter().enumerate().skip(1) {
475        let edge = frontier.pos / ARITY.pow(level as u32 - 1);
476        let data = child_values(node, edge / ARITY, edge, edge_values[level - 1])?;
477        edge_values.push(<Sha3Digest as DigestAlgorithm<
478            Commitment<Header>,
479            u64,
480            _,
481        >>::digest(&data)?);
482    }
483
484    let mut proof = vec![target.proof[0].clone()];
485    for (level, node) in target.proof.iter().enumerate().skip(1) {
486        let divisor = ARITY.pow(level as u32 - 1);
487        let edge = frontier.pos / divisor;
488        let data = child_values(
489            node,
490            target.pos / divisor / ARITY,
491            edge,
492            edge_values[level - 1],
493        )?;
494        let children = data
495            .iter()
496            .enumerate()
497            .map(|(n, value)| {
498                let index = target.pos / divisor / ARITY * ARITY + n as u64;
499                Arc::new(match index.cmp(&edge) {
500                    Ordering::Greater => BlockMerkleNode::Empty,
501                    _ => BlockMerkleNode::ForgettenSubtree { value: *value },
502                })
503            })
504            .collect();
505        proof.push(BlockMerkleNode::Branch {
506            value: Sha3Node::default(),
507            children,
508        });
509    }
510    Ok(BlockMerkleProof {
511        pos: target.pos,
512        proof,
513    })
514}
515
516const ARITY: u64 = BlockMerkleTree::ARITY as u64;
517
518/// Child values of `node`, whose children have indices `parent * ARITY + n` at their level, in the
519/// tree where the rightmost non-empty node of that level is `edge` with value `edge_value`.
520fn child_values(
521    node: &BlockMerkleNode,
522    parent: u64,
523    edge: u64,
524    edge_value: Sha3Node,
525) -> Result<Vec<Sha3Node>> {
526    let BlockMerkleNode::Branch { children, .. } = node else {
527        bail!("expected a branch node in the proof");
528    };
529    Ok(children
530        .iter()
531        .enumerate()
532        .map(|(n, child)| match (parent * ARITY + n as u64).cmp(&edge) {
533            Ordering::Less => child.value(),
534            Ordering::Equal => edge_value,
535            Ordering::Greater => Sha3Node::default(),
536        })
537        .collect())
538}
539
540pub(crate) async fn fetch_block_data_range<State>(
541    state: &State,
542    start: usize,
543    end: usize,
544    fetch_timeout: Duration,
545    large_object_range_limit: usize,
546) -> Result<
547    Vec<(
548        Header,
549        PayloadQueryData<SeqTypes>,
550        VidCommonQueryData<SeqTypes>,
551    )>,
552    Error,
553>
554where
555    State: AvailabilityDataSource<SeqTypes>,
556{
557    if end <= start {
558        return Err(Error::Custom {
559            message: format!("requested empty interval [{start}, {end})"),
560            status: StatusCode::BAD_REQUEST,
561        });
562    }
563    if end - start > large_object_range_limit {
564        return Err(Error::Custom {
565            message: format!(
566                "requested range [{start}, {end}) exceeds maximum size {large_object_range_limit}"
567            ),
568            status: StatusCode::BAD_REQUEST,
569        });
570    }
571
572    let fetch_headers = async move {
573        state
574            .get_header_range(start..end)
575            .await
576            .enumerate()
577            .then(|(i, fetch)| async move {
578                fetch
579                    .with_timeout(fetch_timeout)
580                    .await
581                    .ok_or_else(|| Error::Custom {
582                        message: format!("missing header {}", start + i),
583                        status: StatusCode::NOT_FOUND,
584                    })
585            })
586            .try_collect::<Vec<_>>()
587            .await
588    };
589    let fetch_payloads = async move {
590        state
591            .get_payload_range(start..end)
592            .await
593            .enumerate()
594            .then(|(i, fetch)| async move {
595                fetch
596                    .with_timeout(fetch_timeout)
597                    .await
598                    .ok_or_else(|| Error::Custom {
599                        message: format!("missing payload {}", start + i),
600                        status: StatusCode::NOT_FOUND,
601                    })
602            })
603            .try_collect::<Vec<_>>()
604            .await
605    };
606    let fetch_vid_commons = async move {
607        state
608            .get_vid_common_range(start..end)
609            .await
610            .enumerate()
611            .then(|(i, fetch)| async move {
612                fetch
613                    .with_timeout(fetch_timeout)
614                    .await
615                    .ok_or_else(|| Error::Custom {
616                        message: format!("missing VID common {}", start + i),
617                        status: StatusCode::NOT_FOUND,
618                    })
619            })
620            .try_collect::<Vec<_>>()
621            .await
622    };
623    let (headers, (payloads, vid_commons)) =
624        try_join(fetch_headers, try_join(fetch_payloads, fetch_vid_commons)).await?;
625
626    Ok(izip!(headers, payloads, vid_commons).collect())
627}
628
629/// Single-namespace version of [`get_namespaces_proof_range`].
630pub(crate) async fn get_namespace_proof_range<State>(
631    state: &State,
632    start: usize,
633    end: usize,
634    namespace: u64,
635    fetch_timeout: Duration,
636    large_object_range_limit: usize,
637) -> Result<Vec<NamespaceProof>, Error>
638where
639    State: AvailabilityDataSource<SeqTypes>,
640{
641    let blocks = get_namespaces_proof_range(
642        state,
643        start,
644        end,
645        &[namespace],
646        fetch_timeout,
647        large_object_range_limit,
648    )
649    .await?;
650    Ok(blocks
651        .into_iter()
652        .map(|mut block| {
653            block
654                .remove(&namespace)
655                .unwrap_or_else(NamespaceProof::not_present)
656        })
657        .collect())
658}
659
660pub(crate) async fn get_namespaces_proof_range<State>(
661    state: &State,
662    start: usize,
663    end: usize,
664    namespaces: &[u64],
665    fetch_timeout: Duration,
666    large_object_range_limit: usize,
667) -> Result<Vec<HashMap<u64, NamespaceProof>>, Error>
668where
669    State: AvailabilityDataSource<SeqTypes>,
670{
671    fetch_block_data_range(state, start, end, fetch_timeout, large_object_range_limit)
672        .await?
673        .into_iter()
674        .map(|(header, payload, vid_common)| {
675            namespaces
676                .iter()
677                .filter_map(|&namespace| {
678                    let ns_index = header.ns_table().find_ns_id(&namespace.into())?;
679                    Some(
680                        build_namespace_proof(&payload, &ns_index, &vid_common)
681                            .map(|proof| (namespace, proof)),
682                    )
683                })
684                .collect::<Result<HashMap<_, _>, _>>()
685        })
686        .collect()
687}
688
689/// Decode the `namespaces` path segment: a `TaggedBase64` string tagged `NS` wrapping a
690/// JSON-encoded `Vec<u64>`.
691pub(crate) fn parse_namespaces_str(encoded: &str) -> anyhow::Result<Vec<u64>> {
692    let encoded: TaggedBase64 = encoded
693        .parse()
694        .map_err(|err| anyhow::anyhow!("invalid namespaces parameter: {err}"))?;
695    if encoded.tag() != NAMESPACES_PARAM_TAG {
696        anyhow::bail!(
697            "invalid namespaces parameter tag: expected {NAMESPACES_PARAM_TAG}, got {}",
698            encoded.tag()
699        );
700    }
701    serde_json::from_slice(&encoded.value())
702        .map_err(|err| anyhow::anyhow!("invalid namespaces parameter: {err}"))
703}
704
705/// Construct a [`NamespaceProof`] for the namespace at `ns_index` of the given block.
706fn build_namespace_proof(
707    payload: &PayloadQueryData<SeqTypes>,
708    ns_index: &NsIndex,
709    vid_common: &VidCommonQueryData<SeqTypes>,
710) -> Result<NamespaceProof, Error> {
711    let ns_proof =
712        NsProof::new(payload.data(), ns_index, vid_common.common()).ok_or_else(|| {
713            Error::Custom {
714                message: "failed to construct namespace proof".into(),
715                status: StatusCode::INTERNAL_SERVER_ERROR,
716            }
717        })?;
718    Ok(NamespaceProof::new(ns_proof, vid_common.common().clone()))
719}
720
721fn internal(err: impl Display) -> Error {
722    Error::Custom {
723        message: err.to_string(),
724        status: StatusCode::INTERNAL_SERVER_ERROR,
725    }
726}
727
728fn not_found(msg: impl Into<String>) -> Error {
729    Error::Custom {
730        message: msg.into(),
731        status: StatusCode::NOT_FOUND,
732    }
733}
734
735fn chain_too_long(requested: usize, chain_limit: usize) -> Error {
736    not_found(format!(
737        "no finality proof found within {chain_limit} leaves of requested leaf {requested}"
738    ))
739}
740
741#[cfg(test)]
742mod test {
743    use std::{marker::PhantomData, sync::Mutex};
744
745    use axum::{Router, extract::Path, http::HeaderMap, routing::get};
746    use committable::Committable;
747    use disco_types::error::Error;
748    use espresso_types::BLOCK_MERKLE_TREE_HEIGHT;
749    use futures::future::join_all;
750    use hotshot_query_service::{
751        availability::{BlockQueryData, TransactionIndex, VidCommonQueryData},
752        data_source::{
753            Transaction,
754            storage::{AvailabilityStorage, UpdateAvailabilityStorage, pruning::PrunerCfg},
755        },
756        fetching::provider::TrustedQueryServiceProvider,
757        merklized_state::{MerklizedState, UpdateStateData},
758    };
759    use hotshot_types::{data::ViewNumber, simple_vote::Vote2Data};
760    use http_wire::{ServerError, respond};
761    use itertools::Itertools;
762    use jf_merkle_tree_compat::{
763        AppendableMerkleTreeScheme, ToTraversalPath, prelude::SHA3MerkleTree,
764    };
765    use light_client::{
766        client::{Client as _, FallbackClient, QueryServiceClient},
767        consensus::leaf::{FinalityProof, LeafProofHint},
768        testing::{
769            AlwaysTrueQuorum, LEAF_CHAIN_EPOCH_HEIGHT, TestClient, VersionCheckQuorum,
770            custom_leaf_chain_with_upgrade, custom_leaf_chain_with_upgrades, leaf_chain,
771            leaf_chain_with_upgrade,
772        },
773    };
774    use rstest_reuse::apply;
775    use tokio::{
776        net::TcpListener,
777        time::{sleep, timeout},
778    };
779    use vbs::version::Version;
780    use versions::{
781        DRB_AND_HEADER_UPGRADE_VERSION, EPOCH_REWARD_VERSION, EPOCH_VERSION, FEE_VERSION,
782        NEW_PROTOCOL_VERSION, Upgrade,
783    };
784
785    use super::*;
786    use crate::api::{
787        data_source::{Provider, testing::TestableSqlDataSource},
788        test::sql_data_sources,
789    };
790
791    const CHAIN_LIMIT: usize = 500;
792
793    fn cert2_for_leaf(leaf: &LeafQueryData<SeqTypes>) -> espresso_types::Certificate2<SeqTypes> {
794        let data = Vote2Data {
795            leaf_commit: leaf.leaf().commit(),
796            epoch: leaf.qc().data.epoch.unwrap(),
797            block_number: leaf.height(),
798        };
799        espresso_types::Certificate2::new(
800            data.clone(),
801            data.commit(),
802            leaf.leaf().view_number(),
803            None,
804            PhantomData,
805        )
806    }
807
808    #[apply(sql_data_sources)]
809    async fn test_two_chain<D: TestableSqlDataSource>(_d: PhantomData<D>) {
810        let storage = D::create_storage().await;
811        let ds = D::create(D::persistence_options(&storage), Default::default(), false)
812            .await
813            .unwrap();
814
815        // Insert some leaves, forming a chain.
816        let leaves = leaf_chain(1..=3, EPOCH_VERSION).await;
817        {
818            let mut tx = ds.write().await.unwrap();
819            tx.insert_leaf(&leaves[0]).await.unwrap();
820            tx.insert_leaf(&leaves[1]).await.unwrap();
821            tx.insert_leaf(&leaves[2]).await.unwrap();
822            tx.commit().await.unwrap();
823        }
824
825        // Ask for the first leaf; it is proved finalized by the chain formed along with the second.
826        let proof =
827            get_leaf_proof_with_leaf_chain(&ds, leaves[0].clone(), Duration::MAX, CHAIN_LIMIT)
828                .await
829                .unwrap();
830        assert_eq!(
831            proof
832                .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum::new(
833                    LEAF_CHAIN_EPOCH_HEIGHT
834                )))
835                .await
836                .unwrap(),
837            leaves[0]
838        );
839    }
840
841    #[apply(sql_data_sources)]
842    async fn test_finalized<D: TestableSqlDataSource>(_d: PhantomData<D>) {
843        let storage = D::create_storage().await;
844        let ds = D::create(D::persistence_options(&storage), Default::default(), false)
845            .await
846            .unwrap();
847
848        // Insert a single leaf. We will not be able to provide proofs ending in a leaf chain, but
849        // we can return a leaf if the leaf after it is already known to be finalized.
850        let leaves = leaf_chain(1..=2, EPOCH_VERSION).await;
851        {
852            let mut tx = ds.write().await.unwrap();
853            tx.insert_leaf(&leaves[0]).await.unwrap();
854            tx.commit().await.unwrap();
855        }
856
857        let proof = get_leaf_proof_with_finalized_assumption(
858            &ds,
859            leaves[0].clone(),
860            2,
861            Duration::MAX,
862            CHAIN_LIMIT,
863        )
864        .await
865        .unwrap();
866        assert_eq!(
867            proof
868                .verify(LeafProofHint::assumption(leaves[1].leaf()))
869                .await
870                .unwrap(),
871            leaves[0]
872        );
873    }
874
875    #[apply(sql_data_sources)]
876    async fn test_new_protocol_finalized_assumption<D: TestableSqlDataSource>(_d: PhantomData<D>) {
877        let storage = D::create_storage().await;
878        let ds = D::create(D::persistence_options(&storage), Default::default(), false)
879            .await
880            .unwrap();
881
882        let leaves = leaf_chain(1..=2, NEW_PROTOCOL_VERSION).await;
883        {
884            let mut tx = ds.write().await.unwrap();
885            tx.insert_leaf(&leaves[0]).await.unwrap();
886            tx.commit().await.unwrap();
887        }
888
889        let proof = get_leaf_proof_with_finalized_assumption(
890            &ds,
891            leaves[0].clone(),
892            2,
893            Duration::MAX,
894            CHAIN_LIMIT,
895        )
896        .await
897        .unwrap();
898        assert!(matches!(proof.proof(), FinalityProof::Assumption));
899        assert_eq!(
900            proof
901                .verify(LeafProofHint::assumption(leaves[1].leaf()))
902                .await
903                .unwrap(),
904            leaves[0]
905        );
906    }
907
908    #[apply(sql_data_sources)]
909    async fn test_new_protocol_cert2<D: TestableSqlDataSource>(_d: PhantomData<D>) {
910        let storage = D::create_storage().await;
911        let ds = D::create(D::persistence_options(&storage), Default::default(), false)
912            .await
913            .unwrap();
914
915        let leaves = leaf_chain(1..=2, NEW_PROTOCOL_VERSION).await;
916        let cert2_leaf = &leaves[1];
917        let cert2 = cert2_for_leaf(cert2_leaf);
918
919        {
920            let mut tx = ds.write().await.unwrap();
921            tx.insert_leaf(&leaves[0]).await.unwrap();
922            tx.insert_leaf(cert2_leaf).await.unwrap();
923            tx.insert_cert2(cert2_leaf.height(), cert2).await.unwrap();
924            tx.commit().await.unwrap();
925        }
926
927        let proof = get_leaf_proof_with_cert2(&ds, leaves[0].clone(), Duration::MAX, CHAIN_LIMIT)
928            .await
929            .unwrap();
930        assert!(matches!(proof.proof(), FinalityProof::NewProtocol { .. }));
931        assert_eq!(
932            proof
933                .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum::new(
934                    LEAF_CHAIN_EPOCH_HEIGHT
935                )))
936                .await
937                .unwrap(),
938            leaves[0]
939        );
940    }
941
942    #[apply(sql_data_sources)]
943    async fn test_new_protocol_cert2_chain_limit<D: TestableSqlDataSource>(_d: PhantomData<D>) {
944        let storage = D::create_storage().await;
945        let ds = D::create(D::persistence_options(&storage), Default::default(), false)
946            .await
947            .unwrap();
948
949        // A proof for the first leaf must extend to the nearest cert2, three leaves away.
950        let leaves = leaf_chain(1..=4, NEW_PROTOCOL_VERSION).await;
951        let cert2_leaf = &leaves[3];
952        let cert2 = cert2_for_leaf(cert2_leaf);
953
954        {
955            let mut tx = ds.write().await.unwrap();
956            for leaf in &leaves {
957                tx.insert_leaf(leaf).await.unwrap();
958            }
959            tx.insert_cert2(cert2_leaf.height(), cert2).await.unwrap();
960            tx.commit().await.unwrap();
961        }
962
963        let err = get_leaf_proof_with_cert2(&ds, leaves[0].clone(), Duration::MAX, 2)
964            .await
965            .unwrap_err();
966        assert_eq!(err.status(), StatusCode::NOT_FOUND);
967
968        let proof = get_leaf_proof_with_cert2(&ds, leaves[0].clone(), Duration::MAX, 3)
969            .await
970            .unwrap();
971        assert!(matches!(proof.proof(), FinalityProof::NewProtocol { .. }));
972        assert_eq!(
973            proof
974                .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum::new(
975                    LEAF_CHAIN_EPOCH_HEIGHT
976                )))
977                .await
978                .unwrap(),
979            leaves[0]
980        );
981    }
982
983    #[apply(sql_data_sources)]
984    async fn test_new_protocol_hint_vs_cert2<D: TestableSqlDataSource>(_d: PhantomData<D>) {
985        let storage = D::create_storage().await;
986        let ds = D::create(D::persistence_options(&storage), Default::default(), false)
987            .await
988            .unwrap();
989
990        let leaves = leaf_chain(1..=5, NEW_PROTOCOL_VERSION).await;
991        let cert2_leaf = &leaves[1];
992        let cert2 = cert2_for_leaf(cert2_leaf);
993
994        {
995            let mut tx = ds.write().await.unwrap();
996            for leaf in &leaves {
997                tx.insert_leaf(leaf).await.unwrap();
998            }
999            tx.insert_cert2(cert2_leaf.height(), cert2).await.unwrap();
1000            tx.commit().await.unwrap();
1001        }
1002
1003        // The cert2 at height 2 beats the hint at height 5, so the hint is ignored.
1004        let proof = get_leaf_proof(&ds, leaves[0].clone(), Some(5), Duration::MAX, CHAIN_LIMIT)
1005            .await
1006            .unwrap();
1007        assert!(matches!(proof.proof(), FinalityProof::NewProtocol { .. }));
1008        assert_eq!(
1009            proof
1010                .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum::new(
1011                    LEAF_CHAIN_EPOCH_HEIGHT
1012                )))
1013                .await
1014                .unwrap(),
1015            leaves[0]
1016        );
1017
1018        // A hint at height 3 ties the cert2 proof length, so the hint wins.
1019        let proof = get_leaf_proof(&ds, leaves[0].clone(), Some(3), Duration::MAX, CHAIN_LIMIT)
1020            .await
1021            .unwrap();
1022        assert!(matches!(proof.proof(), FinalityProof::Assumption));
1023        assert_eq!(
1024            proof
1025                .verify(LeafProofHint::assumption(leaves[2].leaf()))
1026                .await
1027                .unwrap(),
1028            leaves[0]
1029        );
1030
1031        // With no cert2 at or after the requested height, the hint is honored.
1032        let proof = get_leaf_proof(&ds, leaves[2].clone(), Some(5), Duration::MAX, CHAIN_LIMIT)
1033            .await
1034            .unwrap();
1035        assert!(matches!(proof.proof(), FinalityProof::Assumption));
1036        assert_eq!(
1037            proof
1038                .verify(LeafProofHint::assumption(leaves[4].leaf()))
1039                .await
1040                .unwrap(),
1041            leaves[2]
1042        );
1043    }
1044
1045    #[apply(sql_data_sources)]
1046    async fn test_distant_hint_falls_through<D: TestableSqlDataSource>(_d: PhantomData<D>) {
1047        let storage = D::create_storage().await;
1048        let ds = D::create(D::persistence_options(&storage), Default::default(), false)
1049            .await
1050            .unwrap();
1051
1052        // Legacy leaves 1-3 upgrade to the new protocol at height 4, with a cert2 at height 5, so
1053        // both fall-through paths have a bounded proof available.
1054        let leaves = leaf_chain_with_upgrade(
1055            1..=5,
1056            4,
1057            Upgrade::new(DRB_AND_HEADER_UPGRADE_VERSION, NEW_PROTOCOL_VERSION),
1058        )
1059        .await;
1060        let cert2_leaf = &leaves[4];
1061        let cert2 = cert2_for_leaf(cert2_leaf);
1062
1063        {
1064            let mut tx = ds.write().await.unwrap();
1065            for leaf in &leaves {
1066                tx.insert_leaf(leaf).await.unwrap();
1067            }
1068            tx.insert_cert2(cert2_leaf.height(), cert2).await.unwrap();
1069            tx.commit().await.unwrap();
1070        }
1071
1072        // A hint more than `chain_limit` past a legacy leaf is ignored in favor of a QC chain.
1073        let proof = get_leaf_proof(
1074            &ds,
1075            leaves[0].clone(),
1076            Some(1000),
1077            Duration::MAX,
1078            CHAIN_LIMIT,
1079        )
1080        .await
1081        .unwrap();
1082        assert!(matches!(proof.proof(), FinalityProof::HotStuff2 { .. }));
1083        assert_eq!(
1084            proof
1085                .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum::new(
1086                    LEAF_CHAIN_EPOCH_HEIGHT
1087                )))
1088                .await
1089                .unwrap(),
1090            leaves[0]
1091        );
1092
1093        // A hint more than `chain_limit` past a new-protocol leaf is ignored in favor of cert2.
1094        let proof = get_leaf_proof(
1095            &ds,
1096            leaves[3].clone(),
1097            Some(1000),
1098            Duration::MAX,
1099            CHAIN_LIMIT,
1100        )
1101        .await
1102        .unwrap();
1103        assert!(matches!(proof.proof(), FinalityProof::NewProtocol { .. }));
1104        assert_eq!(
1105            proof
1106                .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum::new(
1107                    LEAF_CHAIN_EPOCH_HEIGHT
1108                )))
1109                .await
1110                .unwrap(),
1111            leaves[3]
1112        );
1113    }
1114
1115    #[apply(sql_data_sources)]
1116    async fn test_qc_chain_chain_limit<D: TestableSqlDataSource>(_d: PhantomData<D>) {
1117        let storage = D::create_storage().await;
1118        let ds = D::create(D::persistence_options(&storage), Default::default(), false)
1119            .await
1120            .unwrap();
1121
1122        // Proving the first leaf finalized requires walking the two subsequent leaves.
1123        let leaves = leaf_chain(1..=3, EPOCH_VERSION).await;
1124        {
1125            let mut tx = ds.write().await.unwrap();
1126            for leaf in &leaves {
1127                tx.insert_leaf(leaf).await.unwrap();
1128            }
1129            tx.commit().await.unwrap();
1130        }
1131
1132        let err = get_leaf_proof_with_leaf_chain(&ds, leaves[0].clone(), Duration::MAX, 1)
1133            .await
1134            .unwrap_err();
1135        assert_eq!(err.status(), StatusCode::NOT_FOUND);
1136
1137        let proof = get_leaf_proof_with_leaf_chain(&ds, leaves[0].clone(), Duration::MAX, 2)
1138            .await
1139            .unwrap();
1140        assert_eq!(
1141            proof
1142                .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum::new(
1143                    LEAF_CHAIN_EPOCH_HEIGHT
1144                )))
1145                .await
1146                .unwrap(),
1147            leaves[0]
1148        );
1149    }
1150
1151    #[apply(sql_data_sources)]
1152    async fn test_finalized_hint_too_far<D: TestableSqlDataSource>(_d: PhantomData<D>) {
1153        let storage = D::create_storage().await;
1154        let ds = D::create(D::persistence_options(&storage), Default::default(), false)
1155            .await
1156            .unwrap();
1157
1158        let leaves = leaf_chain(1..=2, EPOCH_VERSION).await;
1159        {
1160            let mut tx = ds.write().await.unwrap();
1161            tx.insert_leaf(&leaves[0]).await.unwrap();
1162            tx.commit().await.unwrap();
1163        }
1164
1165        let err = get_leaf_proof_with_finalized_assumption(
1166            &ds,
1167            leaves[0].clone(),
1168            1000,
1169            Duration::MAX,
1170            10,
1171        )
1172        .await
1173        .unwrap_err();
1174        assert_eq!(err.status(), StatusCode::BAD_REQUEST);
1175    }
1176
1177    #[apply(sql_data_sources)]
1178    async fn test_qc_chain_new_protocol_cutover<D: TestableSqlDataSource>(_d: PhantomData<D>) {
1179        let storage = D::create_storage().await;
1180        let ds = D::create(D::persistence_options(&storage), Default::default(), false)
1181            .await
1182            .unwrap();
1183
1184        // Upgrade mid-chain and skew view numbers so no HotStuff 2-chain ever forms; the proof
1185        // for the first (pre-upgrade) leaf must fall through to cert2 finality.
1186        let leaves = custom_leaf_chain_with_upgrade(
1187            1..=4,
1188            2,
1189            Upgrade::new(DRB_AND_HEADER_UPGRADE_VERSION, NEW_PROTOCOL_VERSION),
1190            |proposal| {
1191                proposal.view_number = ViewNumber::new(proposal.block_header.height() * 2);
1192            },
1193        )
1194        .await;
1195        assert_eq!(leaves[0].header().version(), DRB_AND_HEADER_UPGRADE_VERSION);
1196        assert_eq!(leaves[1].header().version(), NEW_PROTOCOL_VERSION);
1197        let cert2_leaf = &leaves[3];
1198        let cert2 = cert2_for_leaf(cert2_leaf);
1199
1200        {
1201            let mut tx = ds.write().await.unwrap();
1202            for leaf in &leaves {
1203                tx.insert_leaf(leaf).await.unwrap();
1204            }
1205            tx.insert_cert2(cert2_leaf.height(), cert2).await.unwrap();
1206            tx.commit().await.unwrap();
1207        }
1208
1209        let proof =
1210            get_leaf_proof_with_leaf_chain(&ds, leaves[0].clone(), Duration::MAX, CHAIN_LIMIT)
1211                .await
1212                .unwrap();
1213        assert!(matches!(proof.proof(), FinalityProof::NewProtocol { .. }));
1214        assert_eq!(
1215            proof
1216                .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum::new(
1217                    LEAF_CHAIN_EPOCH_HEIGHT
1218                )))
1219                .await
1220                .unwrap(),
1221            leaves[0]
1222        );
1223    }
1224
1225    #[apply(sql_data_sources)]
1226    async fn test_qc_chain_new_protocol_cutover_two_chain<D: TestableSqlDataSource>(
1227        _d: PhantomData<D>,
1228    ) {
1229        let storage = D::create_storage().await;
1230        let ds = D::create(D::persistence_options(&storage), Default::default(), false)
1231            .await
1232            .unwrap();
1233
1234        // Upgrade at height 3: the HotStuff2 chain begun before the cutover completes without
1235        // needing a cert2 (none is stored).
1236        let leaves = leaf_chain_with_upgrade(
1237            1..=3,
1238            3,
1239            Upgrade::new(DRB_AND_HEADER_UPGRADE_VERSION, NEW_PROTOCOL_VERSION),
1240        )
1241        .await;
1242        assert_eq!(leaves[1].header().version(), DRB_AND_HEADER_UPGRADE_VERSION);
1243        assert_eq!(leaves[2].header().version(), NEW_PROTOCOL_VERSION);
1244
1245        {
1246            let mut tx = ds.write().await.unwrap();
1247            for leaf in &leaves {
1248                tx.insert_leaf(leaf).await.unwrap();
1249            }
1250            tx.commit().await.unwrap();
1251        }
1252
1253        let proof =
1254            get_leaf_proof_with_leaf_chain(&ds, leaves[0].clone(), Duration::MAX, CHAIN_LIMIT)
1255                .await
1256                .unwrap();
1257        assert!(matches!(proof.proof(), FinalityProof::HotStuff2 { .. }));
1258        assert_eq!(
1259            proof
1260                .verify(LeafProofHint::Quorum(&VersionCheckQuorum::new(
1261                    leaves.iter().map(|leaf| leaf.leaf().clone()),
1262                    LEAF_CHAIN_EPOCH_HEIGHT
1263                )))
1264                .await
1265                .unwrap(),
1266            leaves[0]
1267        );
1268    }
1269
1270    #[apply(sql_data_sources)]
1271    async fn test_qc_chain_last_legacy_leaf_uses_cert2<D: TestableSqlDataSource>(
1272        _d: PhantomData<D>,
1273    ) {
1274        let storage = D::create_storage().await;
1275        let ds = D::create(D::persistence_options(&storage), Default::default(), false)
1276            .await
1277            .unwrap();
1278
1279        // Upgrade at height 2: any 2-chain proving the last legacy leaf spans the cutover, so the
1280        // prover must fall through to the cert2 at height 3 instead.
1281        let leaves = leaf_chain_with_upgrade(
1282            1..=4,
1283            2,
1284            Upgrade::new(DRB_AND_HEADER_UPGRADE_VERSION, NEW_PROTOCOL_VERSION),
1285        )
1286        .await;
1287        assert_eq!(leaves[0].header().version(), DRB_AND_HEADER_UPGRADE_VERSION);
1288        assert_eq!(leaves[1].header().version(), NEW_PROTOCOL_VERSION);
1289
1290        let cert2_leaf = &leaves[2];
1291        let cert2 = cert2_for_leaf(cert2_leaf);
1292
1293        {
1294            let mut tx = ds.write().await.unwrap();
1295            for leaf in &leaves {
1296                tx.insert_leaf(leaf).await.unwrap();
1297            }
1298            tx.insert_cert2(cert2_leaf.height(), cert2).await.unwrap();
1299            tx.commit().await.unwrap();
1300        }
1301
1302        let proof =
1303            get_leaf_proof_with_leaf_chain(&ds, leaves[0].clone(), Duration::MAX, CHAIN_LIMIT)
1304                .await
1305                .unwrap();
1306        assert!(matches!(proof.proof(), FinalityProof::NewProtocol { .. }));
1307        assert_eq!(
1308            proof
1309                .verify(LeafProofHint::Quorum(&VersionCheckQuorum::new(
1310                    leaves.iter().map(|leaf| leaf.leaf().clone()),
1311                    LEAF_CHAIN_EPOCH_HEIGHT
1312                )))
1313                .await
1314                .unwrap(),
1315            leaves[0]
1316        );
1317    }
1318
1319    /// With a gapless cutover, the leaf two before the cutover (`upgrade_height
1320    /// - 2`) is finalized by the old protocol: its HotStuff2 2-chain completes
1321    /// within pre-cutover leaves (deciding QC belongs to `upgrade_height - 1`,
1322    /// still V5), so no cert2 is needed.
1323    #[apply(sql_data_sources)]
1324    async fn test_leaf_before_cutover_uses_hotstuff2<D: TestableSqlDataSource>(_d: PhantomData<D>) {
1325        let storage = D::create_storage().await;
1326        let ds = D::create(D::persistence_options(&storage), Default::default(), false)
1327            .await
1328            .unwrap();
1329
1330        // Cutover at height 4, so `upgrade_height - 2` is height 2 (leaves[1]).
1331        let leaves = leaf_chain_with_upgrade(
1332            1..=5,
1333            4,
1334            Upgrade::new(DRB_AND_HEADER_UPGRADE_VERSION, NEW_PROTOCOL_VERSION),
1335        )
1336        .await;
1337        assert!(leaves[1].header().version() < NEW_PROTOCOL_VERSION);
1338        assert!(leaves[3].header().version() >= NEW_PROTOCOL_VERSION);
1339
1340        {
1341            let mut tx = ds.write().await.unwrap();
1342            for leaf in &leaves {
1343                tx.insert_leaf(leaf).await.unwrap();
1344            }
1345            tx.commit().await.unwrap();
1346        }
1347
1348        let proof =
1349            get_leaf_proof_with_leaf_chain(&ds, leaves[1].clone(), Duration::MAX, CHAIN_LIMIT)
1350                .await
1351                .unwrap();
1352        assert!(matches!(proof.proof(), FinalityProof::HotStuff2 { .. }));
1353        assert_eq!(
1354            proof
1355                .verify(LeafProofHint::Quorum(&VersionCheckQuorum::new(
1356                    leaves.iter().map(|leaf| leaf.leaf().clone()),
1357                    LEAF_CHAIN_EPOCH_HEIGHT
1358                )))
1359                .await
1360                .unwrap(),
1361            leaves[1]
1362        );
1363    }
1364
1365    /// When a view gap at the cutover prevents a HotStuff2 2-chain from
1366    /// completing within pre-cutover leaves, the leaf two before the cutover
1367    /// (`upgrade_height - 2`) instead falls through to cert2 finality. Skewing
1368    /// the view numbers breaks the consecutive-view requirement, so no 2-chain
1369    /// forms and the proof terminates at the first cert2.
1370    #[apply(sql_data_sources)]
1371    async fn test_leaf_before_cutover_uses_cert2<D: TestableSqlDataSource>(_d: PhantomData<D>) {
1372        let storage = D::create_storage().await;
1373        let ds = D::create(D::persistence_options(&storage), Default::default(), false)
1374            .await
1375            .unwrap();
1376
1377        // Cutover at height 4, so `upgrade_height - 2` is height 2 (leaves[1]).
1378        // Skewed views mean no HotStuff2 2-chain ever forms.
1379        let leaves = custom_leaf_chain_with_upgrade(
1380            1..=5,
1381            4,
1382            Upgrade::new(DRB_AND_HEADER_UPGRADE_VERSION, NEW_PROTOCOL_VERSION),
1383            |proposal| {
1384                proposal.view_number = ViewNumber::new(proposal.block_header.height() * 2);
1385            },
1386        )
1387        .await;
1388        assert!(leaves[1].header().version() < NEW_PROTOCOL_VERSION);
1389        assert!(leaves[3].header().version() >= NEW_PROTOCOL_VERSION);
1390        let cert2_leaf = &leaves[3];
1391        let cert2 = cert2_for_leaf(cert2_leaf);
1392
1393        {
1394            let mut tx = ds.write().await.unwrap();
1395            for leaf in &leaves {
1396                tx.insert_leaf(leaf).await.unwrap();
1397            }
1398            tx.insert_cert2(cert2_leaf.height(), cert2).await.unwrap();
1399            tx.commit().await.unwrap();
1400        }
1401
1402        let proof =
1403            get_leaf_proof_with_leaf_chain(&ds, leaves[1].clone(), Duration::MAX, CHAIN_LIMIT)
1404                .await
1405                .unwrap();
1406        assert!(matches!(proof.proof(), FinalityProof::NewProtocol { .. }));
1407        assert_eq!(
1408            proof
1409                .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum::new(
1410                    LEAF_CHAIN_EPOCH_HEIGHT
1411                )))
1412                .await
1413                .unwrap(),
1414            leaves[1]
1415        );
1416    }
1417
1418    /// Every leaf of a history whose tip is past the 0.6 cutover is proved from stored leaves and
1419    /// cert2s alone, with no legacy QC chain stored.
1420    #[apply(sql_data_sources)]
1421    async fn test_leaf_proofs_across_history<D: TestableSqlDataSource>(_d: PhantomData<D>) {
1422        // Every protocol version from mainnet's 0.2 genesis on, with the height it took effect.
1423        const UPGRADES: [(u64, Version); 4] = [
1424            (6, EPOCH_VERSION),
1425            (11, DRB_AND_HEADER_UPGRADE_VERSION),
1426            (16, EPOCH_REWARD_VERSION),
1427            (21, NEW_PROTOCOL_VERSION),
1428        ];
1429        // A view fails just before each of these heights, under HotStuff and under HotStuff2.
1430        const FAILED_VIEWS_BEFORE: [u64; 2] = [3, 13];
1431        const CERT2_HEIGHTS: [u64; 3] = [22, 24, 25];
1432
1433        let view_of = |height: u64| {
1434            height + FAILED_VIEWS_BEFORE.iter().filter(|&&h| h <= height).count() as u64
1435        };
1436        let leaves = custom_leaf_chain_with_upgrades(1..=25, FEE_VERSION, &UPGRADES, |proposal| {
1437            proposal.view_number = ViewNumber::new(view_of(proposal.block_header.height()));
1438            // Upgrades take effect by view, so failed views move where each one starts.
1439            if let Some(cert) = &mut proposal.upgrade_certificate {
1440                let (height, _) = UPGRADES
1441                    .into_iter()
1442                    .find(|(_, version)| *version == cert.data.new_version)
1443                    .unwrap();
1444                let first_view = ViewNumber::new(view_of(height));
1445                cert.data.old_version_last_view = first_view - 1;
1446                cert.data.new_version_first_view = first_view;
1447                cert.data.decide_by = first_view;
1448            }
1449        })
1450        .await;
1451        let leaf_at = |height: u64| &leaves[height as usize - 1];
1452
1453        let storage = D::create_storage().await;
1454        let ds = D::create(D::persistence_options(&storage), Default::default(), false)
1455            .await
1456            .unwrap();
1457        {
1458            let mut tx = ds.write().await.unwrap();
1459            for leaf in &leaves {
1460                tx.insert_leaf(leaf).await.unwrap();
1461            }
1462            for height in CERT2_HEIGHTS {
1463                tx.insert_cert2(height, cert2_for_leaf(leaf_at(height)))
1464                    .await
1465                    .unwrap();
1466            }
1467            tx.commit().await.unwrap();
1468        }
1469
1470        let quorum = VersionCheckQuorum::new(
1471            leaves.iter().map(|leaf| leaf.leaf().clone()),
1472            LEAF_CHAIN_EPOCH_HEIGHT,
1473        );
1474        for leaf in &leaves {
1475            let height = leaf.height();
1476            let proof = get_leaf_proof(&ds, leaf.clone(), None, Duration::MAX, CHAIN_LIMIT)
1477                .await
1478                .unwrap_or_else(|err| panic!("no proof for height {height}: {err:#}"));
1479            match height {
1480                // A 3-chain, which past a failed view closes later on.
1481                1..=4 => assert!(matches!(proof.proof(), FinalityProof::HotStuff { .. })),
1482                // From 5, the first 2-chain closes before the 3-chain does.
1483                5..=19 => assert!(matches!(proof.proof(), FinalityProof::HotStuff2 { .. })),
1484                // The last legacy leaf's 2-chain would span the cutover.
1485                _ => assert!(matches!(proof.proof(), FinalityProof::NewProtocol { .. })),
1486            }
1487            assert_eq!(
1488                proof.verify(LeafProofHint::Quorum(&quorum)).await.unwrap(),
1489                *leaf,
1490                "height {height}"
1491            );
1492        }
1493
1494        // The client may instead name a leaf it already trusts, here the next one.
1495        for (leaf, finalized) in leaves.iter().tuple_windows() {
1496            let height = leaf.height();
1497            let proof = get_leaf_proof(
1498                &ds,
1499                leaf.clone(),
1500                Some(finalized.height() as usize),
1501                Duration::MAX,
1502                CHAIN_LIMIT,
1503            )
1504            .await
1505            .unwrap_or_else(|err| panic!("no proof for height {height}: {err:#}"));
1506            assert!(
1507                matches!(proof.proof(), FinalityProof::Assumption),
1508                "height {height}"
1509            );
1510            assert_eq!(
1511                proof
1512                    .verify(LeafProofHint::<AlwaysTrueQuorum>::Assumption(
1513                        finalized.leaf()
1514                    ))
1515                    .await
1516                    .unwrap(),
1517                *leaf,
1518                "height {height}"
1519            );
1520        }
1521    }
1522
1523    #[apply(sql_data_sources)]
1524    async fn test_bad_finalized<D: TestableSqlDataSource>(_d: PhantomData<D>) {
1525        let storage = D::create_storage().await;
1526        let ds = D::create(D::persistence_options(&storage), Default::default(), false)
1527            .await
1528            .unwrap();
1529
1530        // Insert a single leaf. If we request this leaf but provide a finalized leaf which is
1531        // earlier, we should fail.
1532        let leaves = leaf_chain(1..2, EPOCH_VERSION).await;
1533        {
1534            let mut tx = ds.write().await.unwrap();
1535            tx.insert_leaf(&leaves[0]).await.unwrap();
1536            tx.commit().await.unwrap();
1537        }
1538
1539        let err = get_leaf_proof_with_finalized_assumption(
1540            &ds,
1541            leaves[0].clone(),
1542            0,
1543            Duration::MAX,
1544            CHAIN_LIMIT,
1545        )
1546        .await
1547        .unwrap_err();
1548        assert_eq!(err.status(), StatusCode::BAD_REQUEST);
1549    }
1550
1551    #[apply(sql_data_sources)]
1552    async fn test_no_chain<D: TestableSqlDataSource>(_d: PhantomData<D>) {
1553        let storage = D::create_storage().await;
1554        let ds = D::create(D::persistence_options(&storage), Default::default(), false)
1555            .await
1556            .unwrap();
1557
1558        // Insert multiple leaves that don't chain. We will not be able to prove these are
1559        // finalized.
1560        let leaves = leaf_chain(1..=4, EPOCH_VERSION).await;
1561        {
1562            let mut tx = ds.write().await.unwrap();
1563            tx.insert_leaf(&leaves[0]).await.unwrap();
1564            tx.insert_leaf(&leaves[2]).await.unwrap();
1565            tx.insert_leaf(&leaves[3]).await.unwrap();
1566            tx.commit().await.unwrap();
1567        }
1568
1569        let err = get_leaf_proof_with_leaf_chain(
1570            &ds,
1571            leaves[0].clone(),
1572            Duration::from_secs(1),
1573            CHAIN_LIMIT,
1574        )
1575        .await
1576        .unwrap_err();
1577        assert_eq!(err.status(), StatusCode::NOT_FOUND);
1578
1579        // Even if we start from a finalized leave that extends one of the leaves we do have (4,
1580        // extends 3) we fail to generate a proof because we can't generate a chain from the
1581        // requested leaf (1) to the finalized leaf (4), since leaf 2 is missing.
1582        let err = get_leaf_proof_with_finalized_assumption(
1583            &ds,
1584            leaves[0].clone(),
1585            4,
1586            Duration::from_secs(1),
1587            CHAIN_LIMIT,
1588        )
1589        .await
1590        .unwrap_err();
1591        assert_eq!(err.status(), StatusCode::NOT_FOUND);
1592    }
1593
1594    #[apply(sql_data_sources)]
1595    async fn test_chain_ends_before_finality<D: TestableSqlDataSource>(_d: PhantomData<D>) {
1596        let storage = D::create_storage().await;
1597        let ds = D::create(D::persistence_options(&storage), Default::default(), false)
1598            .await
1599            .unwrap();
1600
1601        // A legacy tip, as every replayed pre-0.6 chain has: the next leaf chains onto the
1602        // requested one, but no deciding QC has arrived and no cert2 ever will.
1603        let leaves = leaf_chain(1..=3, EPOCH_REWARD_VERSION).await;
1604        {
1605            let mut tx = ds.write().await.unwrap();
1606            tx.insert_leaf(&leaves[0]).await.unwrap();
1607            tx.insert_leaf(&leaves[1]).await.unwrap();
1608            tx.commit().await.unwrap();
1609        }
1610
1611        let err = get_leaf_proof_with_leaf_chain(
1612            &ds,
1613            leaves[0].clone(),
1614            Duration::from_secs(1),
1615            CHAIN_LIMIT,
1616        )
1617        .await
1618        .unwrap_err();
1619        assert_eq!(err.status(), StatusCode::NOT_FOUND);
1620        assert!(
1621            matches!(&err, hotshot_query_service::Error::Custom { message, .. } if message.contains("not enough leaves")),
1622            "{err}"
1623        );
1624
1625        // Once the leaf carrying the deciding QC is stored, the same request succeeds.
1626        {
1627            let mut tx = ds.write().await.unwrap();
1628            tx.insert_leaf(&leaves[2]).await.unwrap();
1629            tx.commit().await.unwrap();
1630        }
1631        let proof =
1632            get_leaf_proof_with_leaf_chain(&ds, leaves[0].clone(), Duration::MAX, CHAIN_LIMIT)
1633                .await
1634                .unwrap();
1635        assert_eq!(
1636            proof
1637                .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum::new(
1638                    LEAF_CHAIN_EPOCH_HEIGHT
1639                )))
1640                .await
1641                .unwrap(),
1642            leaves[0]
1643        );
1644    }
1645
1646    #[apply(sql_data_sources)]
1647    async fn test_header_proof<D: TestableSqlDataSource>(_d: PhantomData<D>) {
1648        let storage = D::create_storage().await;
1649        let ds = D::create(D::persistence_options(&storage), Default::default(), false)
1650            .await
1651            .unwrap();
1652
1653        // Construct a chain of leaves, plus the corresponding block Merkle tree at each leaf.
1654        let leaves = leaf_chain(0..=2, EPOCH_VERSION).await;
1655        let mts = leaves
1656            .iter()
1657            .scan(
1658                BlockMerkleTree::new(BLOCK_MERKLE_TREE_HEIGHT),
1659                |mt, leaf| {
1660                    assert_eq!(mt.commitment(), leaf.header().block_merkle_tree_root());
1661                    let item = mt.clone();
1662                    mt.push(leaf.block_hash()).unwrap();
1663                    Some(item)
1664                },
1665            )
1666            .collect::<Vec<_>>();
1667
1668        // Save all those objects in the DB.
1669        {
1670            let mut tx = ds.write().await.unwrap();
1671            for (leaf, mt) in leaves.iter().zip(&mts) {
1672                tx.insert_leaf(leaf).await.unwrap();
1673
1674                if leaf.height() > 0 {
1675                    let merkle_path = mt.lookup(leaf.height() - 1).expect_ok().unwrap().1;
1676                    UpdateStateData::<SeqTypes, BlockMerkleTree, _>::insert_merkle_nodes(
1677                        &mut tx,
1678                        merkle_path,
1679                        ToTraversalPath::<{ BlockMerkleTree::ARITY }>::to_traversal_path(
1680                            &(leaf.height() - 1),
1681                            BLOCK_MERKLE_TREE_HEIGHT,
1682                        ),
1683                        leaf.height(),
1684                    )
1685                    .await
1686                    .unwrap();
1687                    UpdateStateData::<SeqTypes, BlockMerkleTree, _>::set_last_state_height(
1688                        &mut tx,
1689                        leaf.height() as usize,
1690                    )
1691                    .await
1692                    .unwrap();
1693                }
1694            }
1695            tx.commit().await.unwrap();
1696        }
1697
1698        // Test happy path.
1699        for (root, mt) in mts.iter().enumerate().skip(1) {
1700            for (height, leaf) in leaves.iter().enumerate().take(root) {
1701                tracing::info!(root, height, "test happy path");
1702                let proof =
1703                    get_header_proof(&ds, root as u64, BlockId::Number(height), Duration::MAX)
1704                        .await
1705                        .unwrap();
1706                assert_eq!(proof.verify_ref(mt.commitment()).unwrap(), leaf.header());
1707            }
1708        }
1709
1710        // Test unknown leaf.
1711        let err = get_header_proof(&ds, 5, BlockId::Number(4), Duration::from_secs(1))
1712            .await
1713            .unwrap_err();
1714        assert_eq!(err.status(), StatusCode::NOT_FOUND);
1715
1716        // Test height >= root.
1717        let err = get_header_proof(&ds, 1, BlockId::Number(1), Duration::MAX)
1718            .await
1719            .unwrap_err();
1720        assert_eq!(err.status(), StatusCode::BAD_REQUEST);
1721    }
1722
1723    // The light-weight block tree forgets all but its last leaf.
1724    type FullTree = SHA3MerkleTree<Commitment<Header>>;
1725
1726    fn fake_tree(num_leaves: u64) -> FullTree {
1727        FullTree::from_elems(
1728            Some(BLOCK_MERKLE_TREE_HEIGHT),
1729            (0..num_leaves).map(|i| Commitment::<Header>::from_raw([i as u8 + 1; 32])),
1730        )
1731        .unwrap()
1732    }
1733
1734    fn lookup(tree: &FullTree, pos: u64) -> BlockMerkleProof {
1735        tree.lookup(pos).expect_ok().unwrap().1
1736    }
1737
1738    /// Rebuilding from `head_leaves` leaves matches the tree of `r` leaves for every `r` in `rs`.
1739    fn check_rebuild(head_leaves: u64, rs: impl IntoIterator<Item = u64>) {
1740        let head = fake_tree(head_leaves);
1741        for r in rs {
1742            let tree = fake_tree(r);
1743            let frontier = lookup(&head, r - 1);
1744            for i in 0..r {
1745                let proof = rebuild_block_proof(r, &frontier, &lookup(&head, i)).unwrap();
1746                assert_eq!(proof, lookup(&tree, i), "r={r} i={i}");
1747                assert!(
1748                    BlockMerkleTree::verify(tree.commitment(), i, &proof)
1749                        .unwrap()
1750                        .is_ok()
1751                );
1752            }
1753        }
1754    }
1755
1756    #[test]
1757    fn test_rebuild_block_proof_matches_snapshot() {
1758        check_rebuild(100, [2, 4, 5, 10, 26, 28, 50, 99]);
1759    }
1760
1761    #[test]
1762    fn test_rebuild_block_proof_last_leaf() {
1763        let head = fake_tree(40);
1764        let tree = fake_tree(13);
1765        let proof = rebuild_block_proof(13, &lookup(&head, 12), &lookup(&head, 12)).unwrap();
1766        assert_eq!(proof, lookup(&tree, 12));
1767    }
1768
1769    #[test]
1770    fn test_rebuild_block_proof_full_subtree() {
1771        check_rebuild(100, [3, 9, 27, 81]);
1772    }
1773
1774    #[test]
1775    fn test_rebuild_block_proof_single_leaf() {
1776        check_rebuild(5, [1]);
1777    }
1778
1779    #[test]
1780    fn test_rebuild_block_proof_wrong_frontier() {
1781        let head = fake_tree(40);
1782        let err = rebuild_block_proof(13, &lookup(&head, 11), &lookup(&head, 3)).unwrap_err();
1783        assert!(err.to_string().contains("frontier"), "{err:#}");
1784    }
1785
1786    /// A data source with `num_leaves` headers, state at every height, and state below `pruned`
1787    /// collected. Returns the data source and the block tree at each height. The storage guard
1788    /// keeps the database alive.
1789    async fn pruned_data_source<D: TestableSqlDataSource>(
1790        num_leaves: usize,
1791        pruned: u64,
1792    ) -> (impl Sized, D, Vec<BlockMerkleTree>) {
1793        let storage = D::create_storage().await;
1794        let ds = D::create(D::persistence_options(&storage), Default::default(), false)
1795            .await
1796            .unwrap();
1797
1798        let leaves = leaf_chain(0..num_leaves as u64, EPOCH_VERSION).await;
1799        let mts = leaves
1800            .iter()
1801            .scan(
1802                BlockMerkleTree::new(BLOCK_MERKLE_TREE_HEIGHT),
1803                |mt, leaf| {
1804                    let item = mt.clone();
1805                    mt.push(leaf.block_hash()).unwrap();
1806                    Some(item)
1807                },
1808            )
1809            .collect::<Vec<_>>();
1810
1811        let mut tx = ds.write().await.unwrap();
1812        for (leaf, mt) in leaves.iter().zip(&mts) {
1813            tx.insert_leaf(leaf).await.unwrap();
1814            if leaf.height() > 0 {
1815                let merkle_path = mt.lookup(leaf.height() - 1).expect_ok().unwrap().1;
1816                UpdateStateData::<SeqTypes, BlockMerkleTree, _>::insert_merkle_nodes(
1817                    &mut tx,
1818                    merkle_path,
1819                    ToTraversalPath::<{ BlockMerkleTree::ARITY }>::to_traversal_path(
1820                        &(leaf.height() - 1),
1821                        BLOCK_MERKLE_TREE_HEIGHT,
1822                    ),
1823                    leaf.height(),
1824                )
1825                .await
1826                .unwrap();
1827                UpdateStateData::<SeqTypes, BlockMerkleTree, _>::set_last_state_height(
1828                    &mut tx,
1829                    leaf.height() as usize,
1830                )
1831                .await
1832                .unwrap();
1833            }
1834        }
1835        tx.commit().await.unwrap();
1836
1837        let cfg =
1838            PrunerCfg::default().with_state_tables(vec![BlockMerkleTree::state_type().into()]);
1839        D::prune_state_below(&ds, pruned, &cfg).await.unwrap();
1840        (storage, ds, mts)
1841    }
1842
1843    #[apply(sql_data_sources)]
1844    async fn test_header_proof_pruned_root<D: TestableSqlDataSource>(_d: PhantomData<D>) {
1845        let num_leaves = 40;
1846        let (_storage, ds, mts) = pruned_data_source::<D>(num_leaves, 30).await;
1847
1848        // Roots below the pruned height are rebuilt from the newest snapshot.
1849        for root in [1, 2, 4, 10, 13, 27, 28] {
1850            for height in 0..root {
1851                let proof = get_header_proof(
1852                    &ds,
1853                    root as u64,
1854                    BlockId::Number(height),
1855                    Duration::from_secs(10),
1856                )
1857                .await
1858                .unwrap();
1859                proof.verify(mts[root].commitment()).unwrap();
1860            }
1861        }
1862    }
1863
1864    #[apply(sql_data_sources)]
1865    async fn test_header_proof_root_beyond_head_fails<D: TestableSqlDataSource>(
1866        _d: PhantomData<D>,
1867    ) {
1868        let (_storage, ds, _) = pruned_data_source::<D>(10, 5).await;
1869        // The state at the head height is stored; beyond it there is no snapshot to rebuild from.
1870        let err = get_header_proof(&ds, 20, BlockId::Number(3), Duration::from_secs(10))
1871            .await
1872            .unwrap_err();
1873        assert_eq!(err.status(), StatusCode::NOT_FOUND);
1874    }
1875
1876    #[apply(sql_data_sources)]
1877    async fn test_namespace_proof<D: TestableSqlDataSource>(_d: PhantomData<D>) {
1878        let storage = D::create_storage().await;
1879        let ds = D::create(D::persistence_options(&storage), Default::default(), false)
1880            .await
1881            .unwrap();
1882
1883        // Construct a chain of blocks.
1884        let client = TestClient::default();
1885        let leaves = join_all((0..=2).map(|i| client.leaf(i))).await;
1886        let payloads = join_all((0..=2).map(|i| client.payload(i))).await;
1887        let vid_commons = join_all((0..=2).map(|i| client.vid_common(i))).await;
1888
1889        // Save all those objects in the DB.
1890        {
1891            let mut tx = ds.write().await.unwrap();
1892            for (leaf, payload, vid_common) in izip!(&leaves, &payloads, &vid_commons) {
1893                tx.insert_leaf(leaf).await.unwrap();
1894                tx.insert_block(&BlockQueryData::<SeqTypes>::new(
1895                    leaf.header().clone(),
1896                    payload.clone(),
1897                ))
1898                .await
1899                .unwrap();
1900                tx.insert_vid(
1901                    &VidCommonQueryData::<SeqTypes>::new(leaf.header().clone(), vid_common.clone()),
1902                    None,
1903                )
1904                .await
1905                .unwrap();
1906            }
1907            tx.commit().await.unwrap();
1908        }
1909
1910        // Test happy path: all blocks.
1911        let ns = payloads[0]
1912            .transaction(&TransactionIndex {
1913                ns_index: 0.into(),
1914                position: 0,
1915            })
1916            .unwrap()
1917            .namespace();
1918        let proofs = get_namespace_proof_range(&ds, 0, 3, ns.into(), Duration::MAX, 100)
1919            .await
1920            .unwrap();
1921        assert_eq!(proofs.len(), 3);
1922        for (leaf, proof) in leaves.iter().zip(proofs) {
1923            proof.verify(leaf.header(), ns).unwrap();
1924        }
1925
1926        // Test happy path: subset.
1927        let tx = payloads[1]
1928            .transaction(&TransactionIndex {
1929                ns_index: 0.into(),
1930                position: 0,
1931            })
1932            .unwrap();
1933        let ns = tx.namespace();
1934        let proofs = get_namespace_proof_range(&ds, 1, 2, ns.into(), Duration::MAX, 100)
1935            .await
1936            .unwrap();
1937        assert_eq!(proofs.len(), 1);
1938        assert_eq!(proofs[0].verify(leaves[1].header(), ns).unwrap(), [tx]);
1939
1940        // Test missing data in range.
1941        let err = get_namespace_proof_range(&ds, 0, 4, ns.into(), Duration::from_secs(1), 100)
1942            .await
1943            .unwrap_err();
1944        assert_eq!(err.status(), StatusCode::NOT_FOUND);
1945
1946        // Test invalid range.
1947        let err = get_namespace_proof_range(&ds, 1, 0, ns.into(), Duration::from_secs(1), 100)
1948            .await
1949            .unwrap_err();
1950        assert_eq!(err.status(), StatusCode::BAD_REQUEST);
1951        assert!(
1952            err.to_string().contains("requested empty interval"),
1953            "{err:#}"
1954        );
1955
1956        // Test large range.
1957        let err = get_namespace_proof_range(&ds, 0, 10_000, ns.into(), Duration::from_secs(1), 100)
1958            .await
1959            .unwrap_err();
1960        assert_eq!(err.status(), StatusCode::BAD_REQUEST);
1961        assert!(err.to_string().contains("exceeds maximum size"), "{err:#}");
1962    }
1963
1964    /// A peer with one cert2, 404ing every other height as a node does for a leaf finalized
1965    /// indirectly. Records the heights it is asked for.
1966    async fn serve_cert2(
1967        cert2: espresso_types::Certificate2<SeqTypes>,
1968    ) -> (TrustedQueryServiceProvider, Arc<Mutex<Vec<u64>>>) {
1969        let requested = Arc::new(Mutex::new(Vec::new()));
1970        let seen = requested.clone();
1971        let router = Router::new().route(
1972            "/availability/cert2/{height}",
1973            get(move |headers: HeaderMap, Path(height): Path<u64>| {
1974                seen.lock().unwrap().push(height);
1975                let served = (height == cert2.data.block_number).then(|| cert2.clone());
1976                async move {
1977                    respond(
1978                        &headers,
1979                        served.ok_or_else(|| ServerError {
1980                            status: axum::http::StatusCode::NOT_FOUND,
1981                            message: format!("no cert2 at height {height}"),
1982                        }),
1983                    )
1984                }
1985            }),
1986        );
1987        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1988        let addr = listener.local_addr().unwrap();
1989        tokio::spawn(async move { axum::serve(listener, router).await.unwrap() });
1990        let provider = TrustedQueryServiceProvider::new(format!("http://{addr}").parse().unwrap());
1991        (provider, requested)
1992    }
1993
1994    /// A peer serving `cert2` over the catchup route at every height, or 404ing when it has none.
1995    /// Records the heights it is asked for.
1996    async fn serve_catchup_cert2(
1997        cert2: Option<espresso_types::Certificate2<SeqTypes>>,
1998    ) -> (QueryServiceClient, Arc<Mutex<Vec<u64>>>) {
1999        let requested = Arc::new(Mutex::new(Vec::new()));
2000        let seen = requested.clone();
2001        let router = Router::new().route(
2002            "/catchup/{height}/cert2",
2003            get(move |headers: HeaderMap, Path(height): Path<u64>| {
2004                seen.lock().unwrap().push(height);
2005                let served = cert2.clone();
2006                async move {
2007                    respond(
2008                        &headers,
2009                        served.ok_or_else(|| ServerError {
2010                            status: axum::http::StatusCode::NOT_FOUND,
2011                            message: format!("no cert2 available for height {height}"),
2012                        }),
2013                    )
2014                }
2015            }),
2016        );
2017        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2018        let addr = listener.local_addr().unwrap();
2019        tokio::spawn(async move { axum::serve(listener, router).await.unwrap() });
2020        let client = QueryServiceClient::new(format!("http://{addr}").parse().unwrap());
2021        (client, requested)
2022    }
2023
2024    /// A proof no stored cert2 can complete asks the peers for one. The peer 404s the requested
2025    /// height itself, so recovery has to reach a descendant.
2026    #[apply(sql_data_sources)]
2027    async fn test_cert2_recovery_from_peer<D: TestableSqlDataSource>(_d: PhantomData<D>) {
2028        let leaves = leaf_chain(1..=4, NEW_PROTOCOL_VERSION).await;
2029        let (peer, requested) = serve_cert2(cert2_for_leaf(&leaves[1])).await;
2030
2031        // Once with no cert2 stored, once with one stored too far past the requested leaf.
2032        for stored_out_of_limit in [false, true] {
2033            requested.lock().unwrap().clear();
2034            let storage = D::create_storage().await;
2035            let ds = D::on_request_data_source(
2036                &storage,
2037                Provider::default().with_cert2_provider(peer.clone()),
2038            )
2039            .await
2040            .unwrap();
2041            {
2042                let mut tx = ds.write().await.unwrap();
2043                for leaf in &leaves {
2044                    tx.insert_leaf(leaf).await.unwrap();
2045                }
2046                if stored_out_of_limit {
2047                    tx.insert_cert2(leaves[3].height(), cert2_for_leaf(&leaves[3]))
2048                        .await
2049                        .unwrap();
2050                }
2051                tx.commit().await.unwrap();
2052            }
2053
2054            let err = get_leaf_proof_with_cert2(&ds, leaves[0].clone(), Duration::from_secs(1), 2)
2055                .await
2056                .unwrap_err();
2057            assert_eq!(err.status(), StatusCode::NOT_FOUND);
2058
2059            timeout(Duration::from_secs(30), async {
2060                loop {
2061                    let mut tx = ds.read().await.unwrap();
2062                    let stored: Option<espresso_types::Certificate2<SeqTypes>> =
2063                        tx.load_cert2(leaves[1].height()).await.unwrap();
2064                    if stored.is_some() {
2065                        break;
2066                    }
2067                    drop(tx);
2068                    sleep(Duration::from_millis(100)).await;
2069                }
2070            })
2071            .await
2072            .expect("descendant cert2 was not recovered from the peer");
2073
2074            let asked = requested.lock().unwrap().clone();
2075            assert_eq!(
2076                asked,
2077                [1, 2],
2078                "the walk did not stop at the first cert2 it found"
2079            );
2080
2081            let proof =
2082                get_leaf_proof_with_cert2(&ds, leaves[0].clone(), Duration::from_secs(1), 2)
2083                    .await
2084                    .unwrap();
2085            assert_eq!(
2086                proof
2087                    .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum::new(
2088                        LEAF_CHAIN_EPOCH_HEIGHT
2089                    )))
2090                    .await
2091                    .unwrap(),
2092                leaves[0]
2093            );
2094        }
2095    }
2096
2097    /// Recovery stops at `chain_limit` past the requested leaf, even with a cert2 just beyond.
2098    #[apply(sql_data_sources)]
2099    async fn test_cert2_recovery_stops_at_the_window<D: TestableSqlDataSource>(_d: PhantomData<D>) {
2100        let leaves = leaf_chain(1..=4, NEW_PROTOCOL_VERSION).await;
2101        // The only cert2 the peer has is one leaf beyond what a `chain_limit` of 2 may reach.
2102        let (peer, requested) = serve_cert2(cert2_for_leaf(&leaves[3])).await;
2103
2104        let storage = D::create_storage().await;
2105        let ds = D::on_request_data_source(&storage, Provider::default().with_cert2_provider(peer))
2106            .await
2107            .unwrap();
2108        {
2109            let mut tx = ds.write().await.unwrap();
2110            for leaf in &leaves {
2111                tx.insert_leaf(leaf).await.unwrap();
2112            }
2113            tx.commit().await.unwrap();
2114        }
2115
2116        let err = get_leaf_proof_with_cert2(&ds, leaves[0].clone(), Duration::from_secs(1), 2)
2117            .await
2118            .unwrap_err();
2119        assert_eq!(err.status(), StatusCode::NOT_FOUND);
2120
2121        // Nothing answers, so the walk runs the whole window and then stops.
2122        timeout(Duration::from_secs(30), async {
2123            while !(1..=3).all(|h| requested.lock().unwrap().contains(&h)) {
2124                sleep(Duration::from_millis(100)).await;
2125            }
2126        })
2127        .await
2128        .expect("recovery did not reach every height within the limit");
2129        sleep(Duration::from_secs(1)).await;
2130        let asked = requested.lock().unwrap().clone();
2131        assert!(
2132            asked.iter().all(|h| (1..=3).contains(h)),
2133            "recovery reached past the limit: {asked:?}"
2134        );
2135    }
2136
2137    #[tokio::test]
2138    async fn test_fallback_client_asks_every_peer_for_cert2() {
2139        let leaves = leaf_chain(1..=2, NEW_PROTOCOL_VERSION).await;
2140        let cert2 = cert2_for_leaf(&leaves[1]);
2141        let height = leaves[1].height();
2142
2143        let mut peers = Vec::new();
2144        let mut requests = Vec::new();
2145        for holds_cert2 in [false, false, false, false, true] {
2146            let (peer, requested) = serve_catchup_cert2(holds_cert2.then(|| cert2.clone())).await;
2147            peers.push(peer);
2148            requests.push(requested);
2149        }
2150        let client = FallbackClient::new(peers).unwrap();
2151
2152        assert_eq!(client.cert2(height).await.unwrap(), Some(cert2));
2153        for (i, requested) in requests.iter().enumerate() {
2154            assert_eq!(
2155                *requested.lock().unwrap(),
2156                [height],
2157                "peer {i} was not asked exactly once"
2158            );
2159        }
2160    }
2161}