Skip to main content

espresso_node/api/
light_client.rs

1use std::{collections::HashMap, fmt::Display, sync::Arc, time::Duration};
2
3use anyhow::Result;
4use committable::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,
10    availability::{AvailabilityDataSource, LeafQueryData, PayloadQueryData, VidCommonQueryData},
11    data_source::{VersionedDataSource, storage::NodeStorage},
12    merklized_state::{MerklizedStateDataSource, Snapshot},
13    node::BlockId,
14    types::HeightIndexed,
15};
16use hotshot_types::simple_certificate::QuorumCertificate2;
17use itertools::izip;
18use jf_merkle_tree_compat::MerkleTreeScheme;
19use light_client::{
20    client::NAMESPACES_PARAM_TAG,
21    consensus::{header::HeaderProof, leaf::LeafProof, namespace::NamespaceProof},
22};
23use tagged_base64::TaggedBase64;
24use versions::NEW_PROTOCOL_VERSION;
25
26/// Construct a proof that the requested leaf is finalized.
27///
28/// The `finalized` hint is honored when it yields a bounded proof no longer than a direct finality
29/// proof, since the client can verify hint-based proofs without signature checks.
30pub(crate) async fn get_leaf_proof<State>(
31    state: &State,
32    requested_leaf: LeafQueryData<SeqTypes>,
33    finalized: Option<usize>,
34    fetch_timeout: Duration,
35    chain_limit: usize,
36) -> Result<LeafProof, Error>
37where
38    State: AvailabilityDataSource<SeqTypes> + VersionedDataSource,
39    for<'a> State::ReadOnly<'a>: NodeStorage<SeqTypes>,
40{
41    let requested = requested_leaf.height() as usize;
42    let new_protocol = requested_leaf.header().version() >= NEW_PROTOCOL_VERSION;
43
44    let mut hint = finalized.filter(|finalized| finalized.saturating_sub(requested) <= chain_limit);
45
46    // New-protocol chains cannot terminate early, so a nearby cert2 may yield a shorter proof.
47    if let Some(finalized) = hint
48        && new_protocol
49        && let Some(cert2_height) = earliest_cert2_height(state, requested as u64).await
50        && cert2_height + 1 < finalized as u64
51    {
52        hint = None;
53    }
54
55    if let Some(finalized) = hint {
56        get_leaf_proof_with_finalized_assumption(
57            state,
58            requested_leaf,
59            finalized,
60            fetch_timeout,
61            chain_limit,
62        )
63        .await
64    } else if new_protocol {
65        get_leaf_proof_with_cert2(state, requested_leaf, fetch_timeout, chain_limit).await
66    } else {
67        get_leaf_proof_with_qc_chain(state, requested_leaf, fetch_timeout, chain_limit).await
68    }
69}
70
71/// The earliest stored cert2 height at or after `height`. Errors are treated as no cert2: this
72/// only chooses between proof strategies, and the chosen path will surface any persistent error.
73async fn earliest_cert2_height<State>(state: &State, height: u64) -> Option<u64>
74where
75    State: VersionedDataSource,
76    for<'a> State::ReadOnly<'a>: NodeStorage<SeqTypes>,
77{
78    let cert2 = state
79        .read()
80        .await
81        .ok()?
82        .load_earliest_cert2(height)
83        .await
84        .ok()??;
85    Some(cert2.data.block_number)
86}
87
88pub(crate) async fn get_leaf_proof_with_qc_chain<State>(
89    state: &State,
90    requested_leaf: LeafQueryData<SeqTypes>,
91    fetch_timeout: Duration,
92    chain_limit: usize,
93) -> Result<LeafProof, Error>
94where
95    State: AvailabilityDataSource<SeqTypes> + VersionedDataSource,
96    for<'a> State::ReadOnly<'a>: NodeStorage<SeqTypes>,
97{
98    let requested = requested_leaf.height() as usize;
99    // Grab the endpoint and the final QC chain in the same transaction, to ensure that
100    // the QC chain actually corresponds to the endpoint block (and is not subject to
101    // concurrent updates).
102    let mut tx = state.read().await.map_err(internal)?;
103    let latest_height = NodeStorage::block_height(&mut tx).await.map_err(internal)?;
104    let qc_chain = tx.latest_qc_chain().await.map_err(internal)?;
105    drop(tx);
106
107    let mut leaves = state.get_leaf_range(requested + 1..latest_height).await;
108    let mut proof = LeafProof::default();
109    let requested_leaf_qc = requested_leaf.qc().clone();
110    proof.push(requested_leaf);
111
112    let mut remaining = chain_limit;
113    while let Some(leaf) = leaves.next().await {
114        let leaf = leaf
115            .with_timeout(fetch_timeout)
116            .await
117            .ok_or_else(|| not_found("missing leaves"))?;
118
119        remaining = remaining
120            .checked_sub(1)
121            .ok_or_else(|| chain_too_long(requested, chain_limit))?;
122
123        // HotStuff commit rules cannot terminate a chain of new-protocol leaves; switch to cert2.
124        if leaf.header().version() >= NEW_PROTOCOL_VERSION {
125            complete_proof_with_cert2(
126                state,
127                &mut proof,
128                leaf,
129                requested_leaf_qc.clone(),
130                fetch_timeout,
131                remaining,
132            )
133            .await?;
134            return Ok(proof);
135        }
136
137        if proof.push(leaf) {
138            return Ok(proof);
139        }
140    }
141
142    // We reached the end of the range of interest without encountering a 3-chain. Thus, if the last
143    // leaf in the chain is not already assumed finalized by the client, we must prove it finalized
144    // by appending two more QCs.
145    let Some([committing_qc, deciding_qc]) = qc_chain else {
146        return Err(not_found("missing QC 2-chain to prove finality"));
147    };
148    proof.add_qc_chain(Arc::new(committing_qc), Arc::new(deciding_qc));
149
150    Ok(proof)
151}
152
153/// Build a leaf proof for the new protocol using certificate2 finality.
154///
155/// Certificate2 directly commits the leaf at `cert2.data.block_number`. The indirect commit rule then
156/// commits all of that leaf's uncommitted ancestors. If cert2 directly commits the requested leaf,
157/// the proof contains only that leaf plus cert2. Otherwise, the proof includes the chain from the
158/// requested leaf through the directly committed descendant; the verifier checks parent commitments
159/// to prove the requested leaf is on that finalized chain.
160pub(crate) async fn get_leaf_proof_with_cert2<State>(
161    state: &State,
162    requested_leaf: LeafQueryData<SeqTypes>,
163    fetch_timeout: Duration,
164    chain_limit: usize,
165) -> Result<LeafProof, Error>
166where
167    State: AvailabilityDataSource<SeqTypes> + VersionedDataSource,
168    for<'a> State::ReadOnly<'a>: NodeStorage<SeqTypes>,
169{
170    let mut proof = LeafProof::default();
171    let requested_leaf_qc = requested_leaf.qc().clone();
172    complete_proof_with_cert2(
173        state,
174        &mut proof,
175        requested_leaf,
176        requested_leaf_qc,
177        fetch_timeout,
178        chain_limit,
179    )
180    .await?;
181    Ok(proof)
182}
183
184/// Extend `proof` from `leaf` to the leaf directly committed by the earliest stored cert2, adding
185/// at most `chain_limit` leaves.
186async fn complete_proof_with_cert2<State>(
187    state: &State,
188    proof: &mut LeafProof,
189    leaf: LeafQueryData<SeqTypes>,
190    requested_leaf_qc: QuorumCertificate2<SeqTypes>,
191    fetch_timeout: Duration,
192    chain_limit: usize,
193) -> Result<(), Error>
194where
195    State: AvailabilityDataSource<SeqTypes> + VersionedDataSource,
196    for<'a> State::ReadOnly<'a>: NodeStorage<SeqTypes>,
197{
198    let start_height = leaf.height();
199    let start_commit = leaf.leaf().commit();
200
201    // The new leaf may already complete a HotStuff chain begun before the protocol cutover.
202    if proof.push(leaf) {
203        return Ok(());
204    }
205
206    let cert2 = state
207        .read()
208        .await
209        .map_err(internal)?
210        .load_earliest_cert2(start_height)
211        .await
212        .map_err(internal)?
213        .ok_or_else(|| {
214            not_found(format!(
215                "no cert2 finality proof available at or after height {start_height}"
216            ))
217        })?;
218
219    let cert2_height = cert2.data.block_number;
220    if cert2_height < start_height {
221        return Err(not_found(
222            "cert2 finality proof is older than requested leaf",
223        ));
224    }
225    if cert2_height - start_height > chain_limit as u64 {
226        return Err(not_found(format!(
227            "earliest cert2 finality proof (height {cert2_height}) is more than {chain_limit} \
228             leaves past height {start_height}"
229        )));
230    }
231
232    if cert2_height == start_height {
233        if start_commit != cert2.data.leaf_commit {
234            return Err(internal("stored cert2 does not finalize the expected leaf"));
235        }
236        proof.add_certificate(Arc::new(cert2), requested_leaf_qc);
237        return Ok(());
238    }
239
240    let mut leaves = state
241        .get_leaf_range(start_height as usize + 1..cert2_height as usize + 1)
242        .await;
243    // Extend the proof chain until we reach the leaf directly committed by cert2. Once that leaf is
244    // present and matches cert2's commitment, the requested leaf is finalized by the indirect
245    // commit rule.
246    while let Some(leaf) = leaves.next().await {
247        let leaf = leaf
248            .with_timeout(fetch_timeout)
249            .await
250            .ok_or_else(|| not_found("missing leaves"))?;
251
252        if leaf.height() == cert2_height {
253            if leaf.leaf().commit() != cert2.data.leaf_commit {
254                return Err(internal("stored cert2 does not finalize the expected leaf"));
255            }
256            if !proof.push(leaf) {
257                proof.add_certificate(Arc::new(cert2), requested_leaf_qc);
258            }
259            return Ok(());
260        }
261        if proof.push(leaf) {
262            return Ok(());
263        }
264    }
265
266    Err(not_found("missing cert2 leaf"))
267}
268
269pub(crate) async fn get_leaf_proof_with_finalized_assumption<State>(
270    state: &State,
271    requested_leaf: LeafQueryData<SeqTypes>,
272    finalized: usize,
273    fetch_timeout: Duration,
274    chain_limit: usize,
275) -> Result<LeafProof, Error>
276where
277    State: AvailabilityDataSource<SeqTypes>,
278{
279    let requested = requested_leaf.height() as usize;
280    // If we have a known-finalized block, we will not need a final 2-chain of QCs to prove
281    // the last leaf in the result finalized, since we will either terminate with a 3-chain
282    // of leaves or at the `finalized` leaf.
283    if finalized <= requested {
284        return Err(Error::Custom {
285            message: format!(
286                "finalized leaf height ({finalized}) must be greater than requested ({requested})"
287            ),
288            status: StatusCode::BAD_REQUEST,
289        });
290    }
291    if finalized - requested > chain_limit {
292        return Err(Error::Custom {
293            message: format!(
294                "finalized leaf height ({finalized}) is more than {chain_limit} blocks past the \
295                 requested leaf ({requested}); request a proof without the finalized parameter \
296                 instead"
297            ),
298            status: StatusCode::BAD_REQUEST,
299        });
300    }
301
302    let mut leaves = state.get_leaf_range(requested + 1..finalized).await;
303    let mut proof = LeafProof::default();
304    proof.push(requested_leaf);
305
306    while let Some(leaf) = leaves.next().await {
307        let leaf = leaf
308            .with_timeout(fetch_timeout)
309            .await
310            .ok_or_else(|| not_found("missing leaves"))?;
311
312        if proof.push(leaf) {
313            return Ok(proof);
314        }
315    }
316
317    Ok(proof)
318}
319
320pub(crate) async fn get_header_proof<State>(
321    state: &State,
322    root: u64,
323    requested: BlockId<SeqTypes>,
324    fetch_timeout: Duration,
325) -> Result<HeaderProof, Error>
326where
327    State: AvailabilityDataSource<SeqTypes>
328        + MerklizedStateDataSource<SeqTypes, BlockMerkleTree, { BlockMerkleTree::ARITY }>
329        + VersionedDataSource,
330{
331    let header = state
332        .get_header(requested)
333        .await
334        .with_timeout(fetch_timeout)
335        .await
336        .ok_or_else(|| not_found(format!("unknown header {requested}")))?;
337    if header.height() >= root {
338        return Err(Error::Custom {
339            message: format!(
340                "height ({}) must be less than root ({root})",
341                header.height()
342            ),
343            status: StatusCode::BAD_REQUEST,
344        });
345    }
346    let path = MerklizedStateDataSource::<SeqTypes, BlockMerkleTree, _>::get_path(
347        state,
348        Snapshot::Index(root),
349        header.height(),
350    )
351    .await
352    .map_err(|source| Error::MerklizedState {
353        source: source.into(),
354    })?;
355
356    Ok(HeaderProof::new(header, path))
357}
358
359pub(crate) async fn fetch_block_data_range<State>(
360    state: &State,
361    start: usize,
362    end: usize,
363    fetch_timeout: Duration,
364    large_object_range_limit: usize,
365) -> Result<
366    Vec<(
367        Header,
368        PayloadQueryData<SeqTypes>,
369        VidCommonQueryData<SeqTypes>,
370    )>,
371    Error,
372>
373where
374    State: AvailabilityDataSource<SeqTypes>,
375{
376    if end <= start {
377        return Err(Error::Custom {
378            message: format!("requested empty interval [{start}, {end})"),
379            status: StatusCode::BAD_REQUEST,
380        });
381    }
382    if end - start > large_object_range_limit {
383        return Err(Error::Custom {
384            message: format!(
385                "requested range [{start}, {end}) exceeds maximum size {large_object_range_limit}"
386            ),
387            status: StatusCode::BAD_REQUEST,
388        });
389    }
390
391    let fetch_headers = async move {
392        state
393            .get_header_range(start..end)
394            .await
395            .enumerate()
396            .then(|(i, fetch)| async move {
397                fetch
398                    .with_timeout(fetch_timeout)
399                    .await
400                    .ok_or_else(|| Error::Custom {
401                        message: format!("missing header {}", start + i),
402                        status: StatusCode::NOT_FOUND,
403                    })
404            })
405            .try_collect::<Vec<_>>()
406            .await
407    };
408    let fetch_payloads = async move {
409        state
410            .get_payload_range(start..end)
411            .await
412            .enumerate()
413            .then(|(i, fetch)| async move {
414                fetch
415                    .with_timeout(fetch_timeout)
416                    .await
417                    .ok_or_else(|| Error::Custom {
418                        message: format!("missing payload {}", start + i),
419                        status: StatusCode::NOT_FOUND,
420                    })
421            })
422            .try_collect::<Vec<_>>()
423            .await
424    };
425    let fetch_vid_commons = async move {
426        state
427            .get_vid_common_range(start..end)
428            .await
429            .enumerate()
430            .then(|(i, fetch)| async move {
431                fetch
432                    .with_timeout(fetch_timeout)
433                    .await
434                    .ok_or_else(|| Error::Custom {
435                        message: format!("missing VID common {}", start + i),
436                        status: StatusCode::NOT_FOUND,
437                    })
438            })
439            .try_collect::<Vec<_>>()
440            .await
441    };
442    let (headers, (payloads, vid_commons)) =
443        try_join(fetch_headers, try_join(fetch_payloads, fetch_vid_commons)).await?;
444
445    Ok(izip!(headers, payloads, vid_commons).collect())
446}
447
448/// Single-namespace version of [`get_namespaces_proof_range`].
449pub(crate) async fn get_namespace_proof_range<State>(
450    state: &State,
451    start: usize,
452    end: usize,
453    namespace: u64,
454    fetch_timeout: Duration,
455    large_object_range_limit: usize,
456) -> Result<Vec<NamespaceProof>, Error>
457where
458    State: AvailabilityDataSource<SeqTypes>,
459{
460    let blocks = get_namespaces_proof_range(
461        state,
462        start,
463        end,
464        &[namespace],
465        fetch_timeout,
466        large_object_range_limit,
467    )
468    .await?;
469    Ok(blocks
470        .into_iter()
471        .map(|mut block| {
472            block
473                .remove(&namespace)
474                .unwrap_or_else(NamespaceProof::not_present)
475        })
476        .collect())
477}
478
479pub(crate) async fn get_namespaces_proof_range<State>(
480    state: &State,
481    start: usize,
482    end: usize,
483    namespaces: &[u64],
484    fetch_timeout: Duration,
485    large_object_range_limit: usize,
486) -> Result<Vec<HashMap<u64, NamespaceProof>>, Error>
487where
488    State: AvailabilityDataSource<SeqTypes>,
489{
490    fetch_block_data_range(state, start, end, fetch_timeout, large_object_range_limit)
491        .await?
492        .into_iter()
493        .map(|(header, payload, vid_common)| {
494            namespaces
495                .iter()
496                .filter_map(|&namespace| {
497                    let ns_index = header.ns_table().find_ns_id(&namespace.into())?;
498                    Some(
499                        build_namespace_proof(&payload, &ns_index, &vid_common)
500                            .map(|proof| (namespace, proof)),
501                    )
502                })
503                .collect::<Result<HashMap<_, _>, _>>()
504        })
505        .collect()
506}
507
508/// Decode the `namespaces` path segment: a `TaggedBase64` string tagged `NS` wrapping a
509/// JSON-encoded `Vec<u64>`.
510pub(crate) fn parse_namespaces_str(encoded: &str) -> anyhow::Result<Vec<u64>> {
511    let encoded: TaggedBase64 = encoded
512        .parse()
513        .map_err(|err| anyhow::anyhow!("invalid namespaces parameter: {err}"))?;
514    if encoded.tag() != NAMESPACES_PARAM_TAG {
515        anyhow::bail!(
516            "invalid namespaces parameter tag: expected {NAMESPACES_PARAM_TAG}, got {}",
517            encoded.tag()
518        );
519    }
520    serde_json::from_slice(&encoded.value())
521        .map_err(|err| anyhow::anyhow!("invalid namespaces parameter: {err}"))
522}
523
524/// Construct a [`NamespaceProof`] for the namespace at `ns_index` of the given block.
525fn build_namespace_proof(
526    payload: &PayloadQueryData<SeqTypes>,
527    ns_index: &NsIndex,
528    vid_common: &VidCommonQueryData<SeqTypes>,
529) -> Result<NamespaceProof, Error> {
530    let ns_proof =
531        NsProof::new(payload.data(), ns_index, vid_common.common()).ok_or_else(|| {
532            Error::Custom {
533                message: "failed to construct namespace proof".into(),
534                status: StatusCode::INTERNAL_SERVER_ERROR,
535            }
536        })?;
537    Ok(NamespaceProof::new(ns_proof, vid_common.common().clone()))
538}
539
540fn internal(err: impl Display) -> Error {
541    Error::Custom {
542        message: err.to_string(),
543        status: StatusCode::INTERNAL_SERVER_ERROR,
544    }
545}
546
547fn not_found(msg: impl Into<String>) -> Error {
548    Error::Custom {
549        message: msg.into(),
550        status: StatusCode::NOT_FOUND,
551    }
552}
553
554fn chain_too_long(requested: usize, chain_limit: usize) -> Error {
555    not_found(format!(
556        "no finality proof found within {chain_limit} leaves of requested leaf {requested}"
557    ))
558}
559
560#[cfg(test)]
561mod test {
562    use std::marker::PhantomData;
563
564    use committable::Committable;
565    use disco_types::error::Error;
566    use espresso_types::BLOCK_MERKLE_TREE_HEIGHT;
567    use futures::future::join_all;
568    use hotshot_query_service::{
569        availability::{BlockQueryData, TransactionIndex, VidCommonQueryData},
570        data_source::{Transaction, storage::UpdateAvailabilityStorage},
571        merklized_state::UpdateStateData,
572    };
573    use hotshot_types::{
574        data::ViewNumber, simple_certificate::CertificatePair, simple_vote::Vote2Data,
575    };
576    use jf_merkle_tree_compat::{AppendableMerkleTreeScheme, ToTraversalPath};
577    use light_client::{
578        consensus::leaf::{FinalityProof, LeafProofHint},
579        testing::{
580            AlwaysTrueQuorum, ENABLE_EPOCHS, LEGACY_VERSION, TestClient, VersionCheckQuorum,
581            custom_leaf_chain_with_upgrade, leaf_chain, leaf_chain_with_upgrade,
582        },
583    };
584    use versions::{DRB_AND_HEADER_UPGRADE_VERSION, EPOCH_VERSION, NEW_PROTOCOL_VERSION, Upgrade};
585
586    use super::*;
587    use crate::api::{
588        data_source::{SequencerDataSource, testing::TestableSequencerDataSource},
589        sql::DataSource,
590    };
591
592    const CHAIN_LIMIT: usize = 500;
593
594    fn cert2_for_leaf(leaf: &LeafQueryData<SeqTypes>) -> espresso_types::Certificate2<SeqTypes> {
595        let data = Vote2Data {
596            leaf_commit: leaf.leaf().commit(),
597            epoch: leaf.qc().data.epoch.unwrap(),
598            block_number: leaf.height(),
599        };
600        espresso_types::Certificate2::new(
601            data.clone(),
602            data.commit(),
603            leaf.leaf().view_number(),
604            None,
605            PhantomData,
606        )
607    }
608
609    #[test_log::test(tokio::test(flavor = "multi_thread"))]
610    async fn test_two_chain() {
611        let storage = <DataSource as TestableSequencerDataSource>::create_storage().await;
612        let ds = DataSource::create(
613            DataSource::persistence_options(&storage),
614            Default::default(),
615            false,
616        )
617        .await
618        .unwrap();
619
620        // Insert some leaves, forming a chain.
621        let leaves = leaf_chain(1..=3, EPOCH_VERSION).await;
622        {
623            let mut tx = ds.write().await.unwrap();
624            tx.insert_leaf(&leaves[0]).await.unwrap();
625            tx.insert_leaf(&leaves[1]).await.unwrap();
626            tx.insert_leaf(&leaves[2]).await.unwrap();
627            tx.commit().await.unwrap();
628        }
629
630        // Ask for the first leaf; it is proved finalized by the chain formed along with the second.
631        let proof =
632            get_leaf_proof_with_qc_chain(&ds, leaves[0].clone(), Duration::MAX, CHAIN_LIMIT)
633                .await
634                .unwrap();
635        assert_eq!(
636            proof
637                .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum))
638                .await
639                .unwrap(),
640            leaves[0]
641        );
642    }
643
644    #[test_log::test(tokio::test(flavor = "multi_thread"))]
645    async fn test_finalized() {
646        let storage = <DataSource as TestableSequencerDataSource>::create_storage().await;
647        let ds = DataSource::create(
648            DataSource::persistence_options(&storage),
649            Default::default(),
650            false,
651        )
652        .await
653        .unwrap();
654
655        // Insert a single leaf. We will not be able to provide proofs ending in a leaf chain, but
656        // we can return a leaf if the leaf after it is already known to be finalized.
657        let leaves = leaf_chain(1..=2, EPOCH_VERSION).await;
658        {
659            let mut tx = ds.write().await.unwrap();
660            tx.insert_leaf(&leaves[0]).await.unwrap();
661            tx.commit().await.unwrap();
662        }
663
664        let proof = get_leaf_proof_with_finalized_assumption(
665            &ds,
666            leaves[0].clone(),
667            2,
668            Duration::MAX,
669            CHAIN_LIMIT,
670        )
671        .await
672        .unwrap();
673        assert_eq!(
674            proof
675                .verify(LeafProofHint::assumption(leaves[1].leaf()))
676                .await
677                .unwrap(),
678            leaves[0]
679        );
680    }
681
682    #[test_log::test(tokio::test(flavor = "multi_thread"))]
683    async fn test_new_protocol_finalized_assumption() {
684        let storage = <DataSource as TestableSequencerDataSource>::create_storage().await;
685        let ds = DataSource::create(
686            DataSource::persistence_options(&storage),
687            Default::default(),
688            false,
689        )
690        .await
691        .unwrap();
692
693        let leaves = leaf_chain(1..=2, NEW_PROTOCOL_VERSION).await;
694        {
695            let mut tx = ds.write().await.unwrap();
696            tx.insert_leaf(&leaves[0]).await.unwrap();
697            tx.commit().await.unwrap();
698        }
699
700        let proof = get_leaf_proof_with_finalized_assumption(
701            &ds,
702            leaves[0].clone(),
703            2,
704            Duration::MAX,
705            CHAIN_LIMIT,
706        )
707        .await
708        .unwrap();
709        assert!(matches!(proof.proof(), FinalityProof::Assumption));
710        assert_eq!(
711            proof
712                .verify(LeafProofHint::assumption(leaves[1].leaf()))
713                .await
714                .unwrap(),
715            leaves[0]
716        );
717    }
718
719    #[test_log::test(tokio::test(flavor = "multi_thread"))]
720    async fn test_new_protocol_cert2() {
721        let storage = <DataSource as TestableSequencerDataSource>::create_storage().await;
722        let ds = DataSource::create(
723            DataSource::persistence_options(&storage),
724            Default::default(),
725            false,
726        )
727        .await
728        .unwrap();
729
730        let leaves = leaf_chain(1..=2, NEW_PROTOCOL_VERSION).await;
731        let cert2_leaf = &leaves[1];
732        let cert2 = cert2_for_leaf(cert2_leaf);
733
734        {
735            let mut tx = ds.write().await.unwrap();
736            tx.insert_leaf(&leaves[0]).await.unwrap();
737            tx.insert_leaf(cert2_leaf).await.unwrap();
738            tx.insert_cert2(cert2_leaf.height(), cert2).await.unwrap();
739            tx.commit().await.unwrap();
740        }
741
742        let proof = get_leaf_proof_with_cert2(&ds, leaves[0].clone(), Duration::MAX, CHAIN_LIMIT)
743            .await
744            .unwrap();
745        assert!(matches!(proof.proof(), FinalityProof::NewProtocol { .. }));
746        assert_eq!(
747            proof
748                .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum))
749                .await
750                .unwrap(),
751            leaves[0]
752        );
753    }
754
755    #[test_log::test(tokio::test(flavor = "multi_thread"))]
756    async fn test_new_protocol_cert2_chain_limit() {
757        let storage = <DataSource as TestableSequencerDataSource>::create_storage().await;
758        let ds = DataSource::create(
759            DataSource::persistence_options(&storage),
760            Default::default(),
761            false,
762        )
763        .await
764        .unwrap();
765
766        // A proof for the first leaf must extend to the nearest cert2, three leaves away.
767        let leaves = leaf_chain(1..=4, NEW_PROTOCOL_VERSION).await;
768        let cert2_leaf = &leaves[3];
769        let cert2 = cert2_for_leaf(cert2_leaf);
770
771        {
772            let mut tx = ds.write().await.unwrap();
773            for leaf in &leaves {
774                tx.insert_leaf(leaf).await.unwrap();
775            }
776            tx.insert_cert2(cert2_leaf.height(), cert2).await.unwrap();
777            tx.commit().await.unwrap();
778        }
779
780        let err = get_leaf_proof_with_cert2(&ds, leaves[0].clone(), Duration::MAX, 2)
781            .await
782            .unwrap_err();
783        assert_eq!(err.status(), StatusCode::NOT_FOUND);
784
785        let proof = get_leaf_proof_with_cert2(&ds, leaves[0].clone(), Duration::MAX, 3)
786            .await
787            .unwrap();
788        assert!(matches!(proof.proof(), FinalityProof::NewProtocol { .. }));
789        assert_eq!(
790            proof
791                .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum))
792                .await
793                .unwrap(),
794            leaves[0]
795        );
796    }
797
798    #[test_log::test(tokio::test(flavor = "multi_thread"))]
799    async fn test_new_protocol_hint_vs_cert2() {
800        let storage = <DataSource as TestableSequencerDataSource>::create_storage().await;
801        let ds = DataSource::create(
802            DataSource::persistence_options(&storage),
803            Default::default(),
804            false,
805        )
806        .await
807        .unwrap();
808
809        let leaves = leaf_chain(1..=5, NEW_PROTOCOL_VERSION).await;
810        let cert2_leaf = &leaves[1];
811        let cert2 = cert2_for_leaf(cert2_leaf);
812
813        {
814            let mut tx = ds.write().await.unwrap();
815            for leaf in &leaves {
816                tx.insert_leaf(leaf).await.unwrap();
817            }
818            tx.insert_cert2(cert2_leaf.height(), cert2).await.unwrap();
819            tx.commit().await.unwrap();
820        }
821
822        // The cert2 at height 2 beats the hint at height 5, so the hint is ignored.
823        let proof = get_leaf_proof(&ds, leaves[0].clone(), Some(5), Duration::MAX, CHAIN_LIMIT)
824            .await
825            .unwrap();
826        assert!(matches!(proof.proof(), FinalityProof::NewProtocol { .. }));
827        assert_eq!(
828            proof
829                .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum))
830                .await
831                .unwrap(),
832            leaves[0]
833        );
834
835        // A hint at height 3 ties the cert2 proof length, so the hint wins.
836        let proof = get_leaf_proof(&ds, leaves[0].clone(), Some(3), Duration::MAX, CHAIN_LIMIT)
837            .await
838            .unwrap();
839        assert!(matches!(proof.proof(), FinalityProof::Assumption));
840        assert_eq!(
841            proof
842                .verify(LeafProofHint::assumption(leaves[2].leaf()))
843                .await
844                .unwrap(),
845            leaves[0]
846        );
847
848        // With no cert2 at or after the requested height, the hint is honored.
849        let proof = get_leaf_proof(&ds, leaves[2].clone(), Some(5), Duration::MAX, CHAIN_LIMIT)
850            .await
851            .unwrap();
852        assert!(matches!(proof.proof(), FinalityProof::Assumption));
853        assert_eq!(
854            proof
855                .verify(LeafProofHint::assumption(leaves[4].leaf()))
856                .await
857                .unwrap(),
858            leaves[2]
859        );
860    }
861
862    #[test_log::test(tokio::test(flavor = "multi_thread"))]
863    async fn test_distant_hint_falls_through() {
864        let storage = <DataSource as TestableSequencerDataSource>::create_storage().await;
865        let ds = DataSource::create(
866            DataSource::persistence_options(&storage),
867            Default::default(),
868            false,
869        )
870        .await
871        .unwrap();
872
873        // Legacy leaves 1-3 upgrade to the new protocol at height 4, with a cert2 at height 5, so
874        // both fall-through paths have a bounded proof available.
875        let leaves = leaf_chain_with_upgrade(
876            1..=5,
877            4,
878            Upgrade::new(DRB_AND_HEADER_UPGRADE_VERSION, NEW_PROTOCOL_VERSION),
879        )
880        .await;
881        let cert2_leaf = &leaves[4];
882        let cert2 = cert2_for_leaf(cert2_leaf);
883
884        {
885            let mut tx = ds.write().await.unwrap();
886            for leaf in &leaves {
887                tx.insert_leaf(leaf).await.unwrap();
888            }
889            tx.insert_cert2(cert2_leaf.height(), cert2).await.unwrap();
890            tx.commit().await.unwrap();
891        }
892
893        // A hint more than `chain_limit` past a legacy leaf is ignored in favor of a QC chain.
894        let proof = get_leaf_proof(
895            &ds,
896            leaves[0].clone(),
897            Some(1000),
898            Duration::MAX,
899            CHAIN_LIMIT,
900        )
901        .await
902        .unwrap();
903        assert!(matches!(proof.proof(), FinalityProof::HotStuff2 { .. }));
904        assert_eq!(
905            proof
906                .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum))
907                .await
908                .unwrap(),
909            leaves[0]
910        );
911
912        // A hint more than `chain_limit` past a new-protocol leaf is ignored in favor of cert2.
913        let proof = get_leaf_proof(
914            &ds,
915            leaves[3].clone(),
916            Some(1000),
917            Duration::MAX,
918            CHAIN_LIMIT,
919        )
920        .await
921        .unwrap();
922        assert!(matches!(proof.proof(), FinalityProof::NewProtocol { .. }));
923        assert_eq!(
924            proof
925                .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum))
926                .await
927                .unwrap(),
928            leaves[3]
929        );
930    }
931
932    #[test_log::test(tokio::test(flavor = "multi_thread"))]
933    async fn test_qc_chain_chain_limit() {
934        let storage = <DataSource as TestableSequencerDataSource>::create_storage().await;
935        let ds = DataSource::create(
936            DataSource::persistence_options(&storage),
937            Default::default(),
938            false,
939        )
940        .await
941        .unwrap();
942
943        // Proving the first leaf finalized requires walking the two subsequent leaves.
944        let leaves = leaf_chain(1..=3, EPOCH_VERSION).await;
945        {
946            let mut tx = ds.write().await.unwrap();
947            for leaf in &leaves {
948                tx.insert_leaf(leaf).await.unwrap();
949            }
950            tx.commit().await.unwrap();
951        }
952
953        let err = get_leaf_proof_with_qc_chain(&ds, leaves[0].clone(), Duration::MAX, 1)
954            .await
955            .unwrap_err();
956        assert_eq!(err.status(), StatusCode::NOT_FOUND);
957
958        let proof = get_leaf_proof_with_qc_chain(&ds, leaves[0].clone(), Duration::MAX, 2)
959            .await
960            .unwrap();
961        assert_eq!(
962            proof
963                .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum))
964                .await
965                .unwrap(),
966            leaves[0]
967        );
968    }
969
970    #[test_log::test(tokio::test(flavor = "multi_thread"))]
971    async fn test_finalized_hint_too_far() {
972        let storage = <DataSource as TestableSequencerDataSource>::create_storage().await;
973        let ds = DataSource::create(
974            DataSource::persistence_options(&storage),
975            Default::default(),
976            false,
977        )
978        .await
979        .unwrap();
980
981        let leaves = leaf_chain(1..=2, EPOCH_VERSION).await;
982        {
983            let mut tx = ds.write().await.unwrap();
984            tx.insert_leaf(&leaves[0]).await.unwrap();
985            tx.commit().await.unwrap();
986        }
987
988        let err = get_leaf_proof_with_finalized_assumption(
989            &ds,
990            leaves[0].clone(),
991            1000,
992            Duration::MAX,
993            10,
994        )
995        .await
996        .unwrap_err();
997        assert_eq!(err.status(), StatusCode::BAD_REQUEST);
998    }
999
1000    #[test_log::test(tokio::test(flavor = "multi_thread"))]
1001    async fn test_qc_chain_new_protocol_cutover() {
1002        let storage = <DataSource as TestableSequencerDataSource>::create_storage().await;
1003        let ds = DataSource::create(
1004            DataSource::persistence_options(&storage),
1005            Default::default(),
1006            false,
1007        )
1008        .await
1009        .unwrap();
1010
1011        // Upgrade mid-chain and skew view numbers so no HotStuff 2-chain ever forms; the proof
1012        // for the first (pre-upgrade) leaf must fall through to cert2 finality.
1013        let leaves = custom_leaf_chain_with_upgrade(
1014            1..=4,
1015            2,
1016            Upgrade::new(DRB_AND_HEADER_UPGRADE_VERSION, NEW_PROTOCOL_VERSION),
1017            |proposal| {
1018                proposal.view_number = ViewNumber::new(proposal.block_header.height() * 2);
1019            },
1020        )
1021        .await;
1022        assert_eq!(leaves[0].header().version(), DRB_AND_HEADER_UPGRADE_VERSION);
1023        assert_eq!(leaves[1].header().version(), NEW_PROTOCOL_VERSION);
1024        let cert2_leaf = &leaves[3];
1025        let cert2 = cert2_for_leaf(cert2_leaf);
1026
1027        {
1028            let mut tx = ds.write().await.unwrap();
1029            for leaf in &leaves {
1030                tx.insert_leaf(leaf).await.unwrap();
1031            }
1032            tx.insert_cert2(cert2_leaf.height(), cert2).await.unwrap();
1033            tx.commit().await.unwrap();
1034        }
1035
1036        let proof =
1037            get_leaf_proof_with_qc_chain(&ds, leaves[0].clone(), Duration::MAX, CHAIN_LIMIT)
1038                .await
1039                .unwrap();
1040        assert!(matches!(proof.proof(), FinalityProof::NewProtocol { .. }));
1041        assert_eq!(
1042            proof
1043                .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum))
1044                .await
1045                .unwrap(),
1046            leaves[0]
1047        );
1048    }
1049
1050    #[test_log::test(tokio::test(flavor = "multi_thread"))]
1051    async fn test_qc_chain_new_protocol_cutover_two_chain() {
1052        let storage = <DataSource as TestableSequencerDataSource>::create_storage().await;
1053        let ds = DataSource::create(
1054            DataSource::persistence_options(&storage),
1055            Default::default(),
1056            false,
1057        )
1058        .await
1059        .unwrap();
1060
1061        // Upgrade at height 3: the HotStuff2 chain begun before the cutover completes without
1062        // needing a cert2 (none is stored).
1063        let leaves = leaf_chain_with_upgrade(
1064            1..=3,
1065            3,
1066            Upgrade::new(DRB_AND_HEADER_UPGRADE_VERSION, NEW_PROTOCOL_VERSION),
1067        )
1068        .await;
1069        assert_eq!(leaves[1].header().version(), DRB_AND_HEADER_UPGRADE_VERSION);
1070        assert_eq!(leaves[2].header().version(), NEW_PROTOCOL_VERSION);
1071
1072        {
1073            let mut tx = ds.write().await.unwrap();
1074            for leaf in &leaves {
1075                tx.insert_leaf(leaf).await.unwrap();
1076            }
1077            tx.commit().await.unwrap();
1078        }
1079
1080        let proof =
1081            get_leaf_proof_with_qc_chain(&ds, leaves[0].clone(), Duration::MAX, CHAIN_LIMIT)
1082                .await
1083                .unwrap();
1084        assert!(matches!(proof.proof(), FinalityProof::HotStuff2 { .. }));
1085        assert_eq!(
1086            proof
1087                .verify(LeafProofHint::Quorum(&VersionCheckQuorum::new(
1088                    leaves.iter().map(|leaf| leaf.leaf().clone())
1089                )))
1090                .await
1091                .unwrap(),
1092            leaves[0]
1093        );
1094    }
1095
1096    #[test_log::test(tokio::test(flavor = "multi_thread"))]
1097    async fn test_qc_chain_last_legacy_leaf_uses_cert2() {
1098        let storage = <DataSource as TestableSequencerDataSource>::create_storage().await;
1099        let ds = DataSource::create(
1100            DataSource::persistence_options(&storage),
1101            Default::default(),
1102            false,
1103        )
1104        .await
1105        .unwrap();
1106
1107        // Upgrade at height 2: any 2-chain proving the last legacy leaf spans the cutover, so the
1108        // prover must fall through to the cert2 at height 3 instead.
1109        let leaves = leaf_chain_with_upgrade(
1110            1..=4,
1111            2,
1112            Upgrade::new(DRB_AND_HEADER_UPGRADE_VERSION, NEW_PROTOCOL_VERSION),
1113        )
1114        .await;
1115        assert_eq!(leaves[0].header().version(), DRB_AND_HEADER_UPGRADE_VERSION);
1116        assert_eq!(leaves[1].header().version(), NEW_PROTOCOL_VERSION);
1117
1118        let cert2_leaf = &leaves[2];
1119        let cert2 = cert2_for_leaf(cert2_leaf);
1120
1121        {
1122            let mut tx = ds.write().await.unwrap();
1123            for leaf in &leaves {
1124                tx.insert_leaf(leaf).await.unwrap();
1125            }
1126            tx.insert_cert2(cert2_leaf.height(), cert2).await.unwrap();
1127            tx.commit().await.unwrap();
1128        }
1129
1130        let proof =
1131            get_leaf_proof_with_qc_chain(&ds, leaves[0].clone(), Duration::MAX, CHAIN_LIMIT)
1132                .await
1133                .unwrap();
1134        assert!(matches!(proof.proof(), FinalityProof::NewProtocol { .. }));
1135        assert_eq!(
1136            proof
1137                .verify(LeafProofHint::Quorum(&VersionCheckQuorum::new(
1138                    leaves.iter().map(|leaf| leaf.leaf().clone())
1139                )))
1140                .await
1141                .unwrap(),
1142            leaves[0]
1143        );
1144    }
1145
1146    /// With a gapless cutover, the leaf two before the cutover (`upgrade_height
1147    /// - 2`) is finalized by the old protocol: its HotStuff2 2-chain completes
1148    /// within pre-cutover leaves (deciding QC belongs to `upgrade_height - 1`,
1149    /// still V5), so no cert2 is needed.
1150    #[test_log::test(tokio::test(flavor = "multi_thread"))]
1151    async fn test_leaf_before_cutover_uses_hotstuff2() {
1152        let storage = <DataSource as TestableSequencerDataSource>::create_storage().await;
1153        let ds = DataSource::create(
1154            DataSource::persistence_options(&storage),
1155            Default::default(),
1156            false,
1157        )
1158        .await
1159        .unwrap();
1160
1161        // Cutover at height 4, so `upgrade_height - 2` is height 2 (leaves[1]).
1162        let leaves = leaf_chain_with_upgrade(
1163            1..=5,
1164            4,
1165            Upgrade::new(DRB_AND_HEADER_UPGRADE_VERSION, NEW_PROTOCOL_VERSION),
1166        )
1167        .await;
1168        assert!(leaves[1].header().version() < NEW_PROTOCOL_VERSION);
1169        assert!(leaves[3].header().version() >= NEW_PROTOCOL_VERSION);
1170
1171        {
1172            let mut tx = ds.write().await.unwrap();
1173            for leaf in &leaves {
1174                tx.insert_leaf(leaf).await.unwrap();
1175            }
1176            tx.commit().await.unwrap();
1177        }
1178
1179        let proof =
1180            get_leaf_proof_with_qc_chain(&ds, leaves[1].clone(), Duration::MAX, CHAIN_LIMIT)
1181                .await
1182                .unwrap();
1183        assert!(matches!(proof.proof(), FinalityProof::HotStuff2 { .. }));
1184        assert_eq!(
1185            proof
1186                .verify(LeafProofHint::Quorum(&VersionCheckQuorum::new(
1187                    leaves.iter().map(|leaf| leaf.leaf().clone())
1188                )))
1189                .await
1190                .unwrap(),
1191            leaves[1]
1192        );
1193    }
1194
1195    /// When a view gap at the cutover prevents a HotStuff2 2-chain from
1196    /// completing within pre-cutover leaves, the leaf two before the cutover
1197    /// (`upgrade_height - 2`) instead falls through to cert2 finality. Skewing
1198    /// the view numbers breaks the consecutive-view requirement, so no 2-chain
1199    /// forms and the proof terminates at the first cert2.
1200    #[test_log::test(tokio::test(flavor = "multi_thread"))]
1201    async fn test_leaf_before_cutover_uses_cert2() {
1202        let storage = <DataSource as TestableSequencerDataSource>::create_storage().await;
1203        let ds = DataSource::create(
1204            DataSource::persistence_options(&storage),
1205            Default::default(),
1206            false,
1207        )
1208        .await
1209        .unwrap();
1210
1211        // Cutover at height 4, so `upgrade_height - 2` is height 2 (leaves[1]).
1212        // Skewed views mean no HotStuff2 2-chain ever forms.
1213        let leaves = custom_leaf_chain_with_upgrade(
1214            1..=5,
1215            4,
1216            Upgrade::new(DRB_AND_HEADER_UPGRADE_VERSION, NEW_PROTOCOL_VERSION),
1217            |proposal| {
1218                proposal.view_number = ViewNumber::new(proposal.block_header.height() * 2);
1219            },
1220        )
1221        .await;
1222        assert!(leaves[1].header().version() < NEW_PROTOCOL_VERSION);
1223        assert!(leaves[3].header().version() >= NEW_PROTOCOL_VERSION);
1224        let cert2_leaf = &leaves[3];
1225        let cert2 = cert2_for_leaf(cert2_leaf);
1226
1227        {
1228            let mut tx = ds.write().await.unwrap();
1229            for leaf in &leaves {
1230                tx.insert_leaf(leaf).await.unwrap();
1231            }
1232            tx.insert_cert2(cert2_leaf.height(), cert2).await.unwrap();
1233            tx.commit().await.unwrap();
1234        }
1235
1236        let proof =
1237            get_leaf_proof_with_qc_chain(&ds, leaves[1].clone(), Duration::MAX, CHAIN_LIMIT)
1238                .await
1239                .unwrap();
1240        assert!(matches!(proof.proof(), FinalityProof::NewProtocol { .. }));
1241        assert_eq!(
1242            proof
1243                .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum))
1244                .await
1245                .unwrap(),
1246            leaves[1]
1247        );
1248    }
1249
1250    #[test_log::test(tokio::test(flavor = "multi_thread"))]
1251    async fn test_bad_finalized() {
1252        let storage = <DataSource as TestableSequencerDataSource>::create_storage().await;
1253        let ds = DataSource::create(
1254            DataSource::persistence_options(&storage),
1255            Default::default(),
1256            false,
1257        )
1258        .await
1259        .unwrap();
1260
1261        // Insert a single leaf. If we request this leaf but provide a finalized leaf which is
1262        // earlier, we should fail.
1263        let leaves = leaf_chain(1..2, EPOCH_VERSION).await;
1264        {
1265            let mut tx = ds.write().await.unwrap();
1266            tx.insert_leaf(&leaves[0]).await.unwrap();
1267            tx.commit().await.unwrap();
1268        }
1269
1270        let err = get_leaf_proof_with_finalized_assumption(
1271            &ds,
1272            leaves[0].clone(),
1273            0,
1274            Duration::MAX,
1275            CHAIN_LIMIT,
1276        )
1277        .await
1278        .unwrap_err();
1279        assert_eq!(err.status(), StatusCode::BAD_REQUEST);
1280    }
1281
1282    #[test_log::test(tokio::test(flavor = "multi_thread"))]
1283    async fn test_no_chain() {
1284        let storage = <DataSource as TestableSequencerDataSource>::create_storage().await;
1285        let ds = DataSource::create(
1286            DataSource::persistence_options(&storage),
1287            Default::default(),
1288            false,
1289        )
1290        .await
1291        .unwrap();
1292
1293        // Insert multiple leaves that don't chain. We will not be able to prove these are
1294        // finalized.
1295        let leaves = leaf_chain(1..=4, EPOCH_VERSION).await;
1296        {
1297            let mut tx = ds.write().await.unwrap();
1298            tx.insert_leaf(&leaves[0]).await.unwrap();
1299            tx.insert_leaf(&leaves[2]).await.unwrap();
1300            tx.insert_leaf(&leaves[3]).await.unwrap();
1301            tx.commit().await.unwrap();
1302        }
1303
1304        let err = get_leaf_proof_with_qc_chain(
1305            &ds,
1306            leaves[0].clone(),
1307            Duration::from_secs(1),
1308            CHAIN_LIMIT,
1309        )
1310        .await
1311        .unwrap_err();
1312        assert_eq!(err.status(), StatusCode::NOT_FOUND);
1313
1314        // Even if we start from a finalized leave that extends one of the leaves we do have (4,
1315        // extends 3) we fail to generate a proof because we can't generate a chain from the
1316        // requested leaf (1) to the finalized leaf (4), since leaf 2 is missing.
1317        let err = get_leaf_proof_with_finalized_assumption(
1318            &ds,
1319            leaves[0].clone(),
1320            4,
1321            Duration::from_secs(1),
1322            CHAIN_LIMIT,
1323        )
1324        .await
1325        .unwrap_err();
1326        assert_eq!(err.status(), StatusCode::NOT_FOUND);
1327    }
1328
1329    #[test_log::test(tokio::test(flavor = "multi_thread"))]
1330    async fn test_final_qcs() {
1331        let storage = <DataSource as TestableSequencerDataSource>::create_storage().await;
1332        let ds = DataSource::create(
1333            DataSource::persistence_options(&storage),
1334            Default::default(),
1335            false,
1336        )
1337        .await
1338        .unwrap();
1339
1340        // Insert a single leaf, plus an extra QC chain proving it finalized.
1341        let leaves = leaf_chain(1..=3, EPOCH_VERSION).await;
1342        let qcs = [
1343            CertificatePair::for_parent(leaves[1].leaf()),
1344            CertificatePair::for_parent(leaves[2].leaf()),
1345        ];
1346        {
1347            let mut tx = ds.write().await.unwrap();
1348            tx.insert_leaf_with_qc_chain(&leaves[0], Some(qcs.clone()))
1349                .await
1350                .unwrap();
1351            tx.commit().await.unwrap();
1352        }
1353
1354        let proof =
1355            get_leaf_proof_with_qc_chain(&ds, leaves[0].clone(), Duration::MAX, CHAIN_LIMIT)
1356                .await
1357                .unwrap();
1358        assert_eq!(
1359            proof
1360                .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum))
1361                .await
1362                .unwrap(),
1363            leaves[0]
1364        );
1365    }
1366
1367    #[test_log::test(tokio::test(flavor = "multi_thread"))]
1368    async fn test_upgrade_to_epochs() {
1369        let storage = <DataSource as TestableSequencerDataSource>::create_storage().await;
1370        let ds = DataSource::create(
1371            DataSource::persistence_options(&storage),
1372            Default::default(),
1373            false,
1374        )
1375        .await
1376        .unwrap();
1377
1378        // Upgrade to epochs (and enabling HotStuff2) in the middle of a leaf chain, so that the
1379        // last leaf in the chain only requires 2 QCs to verify, even though at the start of the
1380        // chain we would have required 3.
1381        let leaves = leaf_chain_with_upgrade(1..=4, 2, ENABLE_EPOCHS).await;
1382        assert_eq!(leaves[0].header().version(), LEGACY_VERSION);
1383        assert_eq!(leaves[1].header().version(), DRB_AND_HEADER_UPGRADE_VERSION);
1384        let qcs = [
1385            CertificatePair::for_parent(leaves[2].leaf()),
1386            CertificatePair::for_parent(leaves[3].leaf()),
1387        ];
1388        {
1389            let mut tx = ds.write().await.unwrap();
1390            tx.insert_leaf(&leaves[0]).await.unwrap();
1391            tx.insert_leaf_with_qc_chain(&leaves[1], Some(qcs.clone()))
1392                .await
1393                .unwrap();
1394            tx.commit().await.unwrap();
1395        }
1396
1397        let proof =
1398            get_leaf_proof_with_qc_chain(&ds, leaves[0].clone(), Duration::MAX, CHAIN_LIMIT)
1399                .await
1400                .unwrap();
1401        assert_eq!(
1402            proof
1403                .verify(LeafProofHint::Quorum(&VersionCheckQuorum::new(
1404                    leaves.iter().map(|leaf| leaf.leaf().clone())
1405                )))
1406                .await
1407                .unwrap(),
1408            leaves[0]
1409        );
1410        assert!(matches!(proof.proof(), FinalityProof::HotStuff2 { .. }))
1411    }
1412
1413    #[tokio::test]
1414    #[test_log::test]
1415    async fn test_header_proof() {
1416        let storage = <DataSource as TestableSequencerDataSource>::create_storage().await;
1417        let ds = DataSource::create(
1418            DataSource::persistence_options(&storage),
1419            Default::default(),
1420            false,
1421        )
1422        .await
1423        .unwrap();
1424
1425        // Construct a chain of leaves, plus the corresponding block Merkle tree at each leaf.
1426        let leaves = leaf_chain(0..=2, EPOCH_VERSION).await;
1427        let mts = leaves
1428            .iter()
1429            .scan(
1430                BlockMerkleTree::new(BLOCK_MERKLE_TREE_HEIGHT),
1431                |mt, leaf| {
1432                    assert_eq!(mt.commitment(), leaf.header().block_merkle_tree_root());
1433                    let item = mt.clone();
1434                    mt.push(leaf.block_hash()).unwrap();
1435                    Some(item)
1436                },
1437            )
1438            .collect::<Vec<_>>();
1439
1440        // Save all those objects in the DB.
1441        {
1442            let mut tx = ds.write().await.unwrap();
1443            for (leaf, mt) in leaves.iter().zip(&mts) {
1444                tx.insert_leaf(leaf).await.unwrap();
1445
1446                if leaf.height() > 0 {
1447                    let merkle_path = mt.lookup(leaf.height() - 1).expect_ok().unwrap().1;
1448                    UpdateStateData::<SeqTypes, BlockMerkleTree, _>::insert_merkle_nodes(
1449                        &mut tx,
1450                        merkle_path,
1451                        ToTraversalPath::<{ BlockMerkleTree::ARITY }>::to_traversal_path(
1452                            &(leaf.height() - 1),
1453                            BLOCK_MERKLE_TREE_HEIGHT,
1454                        ),
1455                        leaf.height(),
1456                    )
1457                    .await
1458                    .unwrap();
1459                    UpdateStateData::<SeqTypes, BlockMerkleTree, _>::set_last_state_height(
1460                        &mut tx,
1461                        leaf.height() as usize,
1462                    )
1463                    .await
1464                    .unwrap();
1465                }
1466            }
1467            tx.commit().await.unwrap();
1468        }
1469
1470        // Test happy path.
1471        for (root, mt) in mts.iter().enumerate().skip(1) {
1472            for (height, leaf) in leaves.iter().enumerate().take(root) {
1473                tracing::info!(root, height, "test happy path");
1474                let proof =
1475                    get_header_proof(&ds, root as u64, BlockId::Number(height), Duration::MAX)
1476                        .await
1477                        .unwrap();
1478                assert_eq!(proof.verify_ref(mt.commitment()).unwrap(), leaf.header());
1479            }
1480        }
1481
1482        // Test unknown leaf.
1483        let err = get_header_proof(&ds, 5, BlockId::Number(4), Duration::from_secs(1))
1484            .await
1485            .unwrap_err();
1486        assert_eq!(err.status(), StatusCode::NOT_FOUND);
1487
1488        // Test height >= root.
1489        let err = get_header_proof(&ds, 1, BlockId::Number(1), Duration::MAX)
1490            .await
1491            .unwrap_err();
1492        assert_eq!(err.status(), StatusCode::BAD_REQUEST);
1493    }
1494
1495    #[tokio::test]
1496    #[test_log::test]
1497    async fn test_namespace_proof() {
1498        let storage = <DataSource as TestableSequencerDataSource>::create_storage().await;
1499        let ds = DataSource::create(
1500            DataSource::persistence_options(&storage),
1501            Default::default(),
1502            false,
1503        )
1504        .await
1505        .unwrap();
1506
1507        // Construct a chain of blocks.
1508        let client = TestClient::default();
1509        let leaves = join_all((0..=2).map(|i| client.leaf(i))).await;
1510        let payloads = join_all((0..=2).map(|i| client.payload(i))).await;
1511        let vid_commons = join_all((0..=2).map(|i| client.vid_common(i))).await;
1512
1513        // Save all those objects in the DB.
1514        {
1515            let mut tx = ds.write().await.unwrap();
1516            for (leaf, payload, vid_common) in izip!(&leaves, &payloads, &vid_commons) {
1517                tx.insert_leaf(leaf).await.unwrap();
1518                tx.insert_block(&BlockQueryData::<SeqTypes>::new(
1519                    leaf.header().clone(),
1520                    payload.clone(),
1521                ))
1522                .await
1523                .unwrap();
1524                tx.insert_vid(
1525                    &VidCommonQueryData::<SeqTypes>::new(leaf.header().clone(), vid_common.clone()),
1526                    None,
1527                )
1528                .await
1529                .unwrap();
1530            }
1531            tx.commit().await.unwrap();
1532        }
1533
1534        // Test happy path: all blocks.
1535        let ns = payloads[0]
1536            .transaction(&TransactionIndex {
1537                ns_index: 0.into(),
1538                position: 0,
1539            })
1540            .unwrap()
1541            .namespace();
1542        let proofs = get_namespace_proof_range(&ds, 0, 3, ns.into(), Duration::MAX, 100)
1543            .await
1544            .unwrap();
1545        assert_eq!(proofs.len(), 3);
1546        for (leaf, proof) in leaves.iter().zip(proofs) {
1547            proof.verify(leaf.header(), ns).unwrap();
1548        }
1549
1550        // Test happy path: subset.
1551        let tx = payloads[1]
1552            .transaction(&TransactionIndex {
1553                ns_index: 0.into(),
1554                position: 0,
1555            })
1556            .unwrap();
1557        let ns = tx.namespace();
1558        let proofs = get_namespace_proof_range(&ds, 1, 2, ns.into(), Duration::MAX, 100)
1559            .await
1560            .unwrap();
1561        assert_eq!(proofs.len(), 1);
1562        assert_eq!(proofs[0].verify(leaves[1].header(), ns).unwrap(), [tx]);
1563
1564        // Test missing data in range.
1565        let err = get_namespace_proof_range(&ds, 0, 4, ns.into(), Duration::from_secs(1), 100)
1566            .await
1567            .unwrap_err();
1568        assert_eq!(err.status(), StatusCode::NOT_FOUND);
1569
1570        // Test invalid range.
1571        let err = get_namespace_proof_range(&ds, 1, 0, ns.into(), Duration::from_secs(1), 100)
1572            .await
1573            .unwrap_err();
1574        assert_eq!(err.status(), StatusCode::BAD_REQUEST);
1575        assert!(
1576            err.to_string().contains("requested empty interval"),
1577            "{err:#}"
1578        );
1579
1580        // Test large range.
1581        let err = get_namespace_proof_range(&ds, 0, 10_000, ns.into(), Duration::from_secs(1), 100)
1582            .await
1583            .unwrap_err();
1584        assert_eq!(err.status(), StatusCode::BAD_REQUEST);
1585        assert!(err.to_string().contains("exceeds maximum size"), "{err:#}");
1586    }
1587}