Skip to main content

light_client/
client.rs

1use std::{collections::HashMap, future::Future, ops::Range};
2#[cfg(feature = "client")]
3use std::{fmt::Debug, pin::pin, time::Duration};
4
5use anyhow::Result;
6#[cfg(feature = "client")]
7use anyhow::{Context, anyhow};
8#[cfg(feature = "client")]
9use derive_builder::Builder;
10use espresso_types::{Certificate2, NamespaceId, SeqTypes, v0_3::StakeTableEvent};
11#[cfg(feature = "client")]
12use futures::{
13    FutureExt, TryFuture, TryFutureExt,
14    future::{BoxFuture, Either, select, select_ok},
15};
16#[cfg(feature = "client")]
17use hotshot_query_service_types::availability::LeafId;
18use hotshot_query_service_types::{availability::LeafQueryData, node::BlockId};
19use hotshot_types::data::EpochNumber;
20#[cfg(feature = "client")]
21use http_client::{StatusCode, Url, error::ClientError as _};
22#[cfg(feature = "client")]
23use serde::de::DeserializeOwned;
24#[cfg(feature = "client")]
25use tagged_base64::TaggedBase64;
26#[cfg(feature = "client")]
27use tokio::time::sleep;
28#[cfg(feature = "client")]
29use vbs::version::StaticVersion;
30
31use crate::{
32    consensus::{
33        header::HeaderProof, leaf::LeafProof, namespace::NamespaceProof, payload::PayloadProof,
34    },
35    storage::LeafRequest,
36};
37
38/// `TaggedBase64` tag for the namespace set encoded into the `namespaces` range endpoint URL.
39///
40/// Shared between the client (which encodes the parameter) and the server (which validates the tag).
41pub const NAMESPACES_PARAM_TAG: &str = "NS";
42
43/// Interface to a query server providing the light client API.
44pub trait Client: Send + Sync + 'static {
45    /// Get the number of blocks known to be in the chain.
46    ///
47    /// This is equivalent to one more than the block number of the latest known block.
48    fn block_height(&self) -> impl Send + Future<Output = Result<u64>>;
49
50    /// Get a finality proof for the requested leaf.
51    ///
52    /// Optionally, the client may specify the height of a known-finalized leaf. In this case, the
53    /// server _may_ terminate the proof in a leaf chain ending at this height, rather than a QC
54    /// chain.
55    fn leaf_proof(
56        &self,
57        id: impl Into<LeafRequest> + Send,
58        finalized: Option<u64>,
59    ) -> impl Send + Future<Output = Result<LeafProof>>;
60
61    /// Get an inclusion proof for the requested header relative to the Merkle tree at height `root`.
62    fn header_proof(
63        &self,
64        root: u64,
65        id: BlockId<SeqTypes>,
66    ) -> impl Send + Future<Output = Result<HeaderProof>>;
67
68    /// Get all leaves in the given range `[start, end)`.
69    fn get_leaves_in_range(
70        &self,
71        start: usize,
72        end: usize,
73    ) -> impl Send + Future<Output = Result<Vec<LeafQueryData<SeqTypes>>>>;
74
75    /// Get the leaves the server has in the given ranges, in one request.
76    ///
77    /// The ranges must be ascending and disjoint. The leaves come without proofs, exactly as
78    /// [`get_leaves_in_range`](Self::get_leaves_in_range) does, so the caller must still verify
79    /// them.
80    fn get_leaves_for_ranges(
81        &self,
82        ranges: &[Range<u64>],
83    ) -> impl Send + Future<Output = Result<Vec<LeafQueryData<SeqTypes>>>>;
84
85    /// Get a proof for the requested payload.
86    ///
87    /// This method accepts only a `height`, not the more flexible [`BlockId`] type, because a
88    /// [`Header`](espresso_types::Header) is needed to verify the resulting proof, so the height
89    /// must already be known anyways.
90    fn payload_proof(&self, height: u64) -> impl Send + Future<Output = Result<PayloadProof>>;
91
92    /// Get proofs for the payloads in `[start, end)`.
93    fn payload_proofs_in_range(
94        &self,
95        start: u64,
96        end: u64,
97    ) -> impl Send + Future<Output = Result<Vec<PayloadProof>>>;
98
99    /// Get payload proofs for a set of height ranges, in one request.
100    ///
101    /// The ranges must be ascending and disjoint, so the proofs come back height-ascending, one
102    /// per requested height; callers pair them with leaves positionally.
103    fn payload_proofs_for_ranges(
104        &self,
105        ranges: &[Range<u64>],
106    ) -> impl Send + Future<Output = Result<Vec<PayloadProof>>>;
107
108    /// Get a proof for the requested namespace.
109    ///
110    /// This method accepts only a `height`, not the more flexible [`BlockId`] type, because a
111    /// [`Header`](espresso_types::Header) is needed to verify the resulting proof, so the height
112    /// must already be known anyways.
113    fn namespace_proof(
114        &self,
115        height: u64,
116        namespace: NamespaceId,
117    ) -> impl Send + Future<Output = Result<NamespaceProof>>;
118
119    /// Get proofs for the requested namespace for each block in `[start, end)`.
120    fn namespace_proofs_in_range(
121        &self,
122        start: u64,
123        end: u64,
124        namespace: NamespaceId,
125    ) -> impl Send + Future<Output = Result<Vec<NamespaceProof>>>;
126
127    /// Get proofs for the requested namespaces for each block in `[start, end)`.
128    fn namespaces_proofs_in_range(
129        &self,
130        start: u64,
131        end: u64,
132        namespaces: &[NamespaceId],
133    ) -> impl Send + Future<Output = Result<Vec<HashMap<NamespaceId, NamespaceProof>>>>;
134
135    /// Get stake table events for the given epoch.
136    ///
137    /// This returns the list of events that must be applied to transform the stake table from
138    /// `epoch - 1` into the stake table for `epoch`.
139    fn stake_table_events(
140        &self,
141        epoch: EpochNumber,
142    ) -> impl Send + Future<Output = Result<Vec<StakeTableEvent>>>;
143
144    /// Get the finality certificate at the given height.
145    ///
146    /// The returned certificate is unverified. Callers must check the threshold signature
147    /// against the appropriate stake table and that it commits to the expected leaf.
148    fn cert2(
149        &self,
150        height: u64,
151    ) -> impl Send + Future<Output = Result<Option<Certificate2<SeqTypes>>>>;
152}
153
154#[cfg(feature = "client")]
155type HttpClient = http_client::Client<hotshot_query_service_types::Error, StaticVersion<0, 1>>;
156
157/// A [`Client`] connected to the HotShot query service.
158#[cfg(feature = "client")]
159#[derive(Clone, Debug)]
160pub struct QueryServiceClient {
161    client: HttpClient,
162}
163
164#[cfg(feature = "client")]
165impl QueryServiceClient {
166    /// Connect to a HotShot query service at the given base URL.
167    pub fn new(url: Url) -> Self {
168        Self {
169            client: http_client::Client::builder(url)
170                .set_timeout(Some(Duration::from_secs(10)))
171                .build(),
172        }
173    }
174
175    /// GET `path`, deserializing the response
176    async fn fetch<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
177        self.client
178            .get(path)
179            .send()
180            .await
181            .with_context(|| format!("fetching {path}"))
182    }
183}
184
185#[cfg(feature = "client")]
186fn is_not_found(err: &anyhow::Error) -> bool {
187    err.chain().any(|cause| {
188        cause
189            .downcast_ref::<hotshot_query_service_types::Error>()
190            .is_some_and(|e| e.status() == StatusCode::NOT_FOUND)
191    })
192}
193
194#[cfg(feature = "client")]
195impl Client for QueryServiceClient {
196    async fn block_height(&self) -> Result<u64> {
197        self.fetch("/node/block-height").await
198    }
199
200    async fn leaf_proof(
201        &self,
202        id: impl Into<LeafRequest> + Send,
203        finalized: Option<u64>,
204    ) -> Result<LeafProof> {
205        let path = "/light-client/leaf";
206        let path = match id.into() {
207            LeafRequest::Leaf(LeafId::Number(n)) | LeafRequest::Header(BlockId::Number(n)) => {
208                format!("{path}/{n}")
209            },
210            LeafRequest::Leaf(LeafId::Hash(h)) => format!("{path}/hash/{h}"),
211            LeafRequest::Header(BlockId::Hash(h)) => format!("{path}/block-hash/{h}"),
212            LeafRequest::Header(BlockId::PayloadHash(h)) => format!("{path}/payload-hash/{h}"),
213        };
214        let path = match finalized {
215            Some(finalized) => format!("{path}/{finalized}"),
216            None => path,
217        };
218        self.fetch(&path).await
219    }
220
221    /// Get all leaves in the given range `[start, end)`.
222    async fn get_leaves_in_range(
223        &self,
224        start: usize,
225        end: usize,
226    ) -> Result<Vec<LeafQueryData<SeqTypes>>> {
227        self.fetch(&format!("/availability/leaf/{start}/{end}"))
228            .await
229    }
230
231    async fn get_leaves_for_ranges(
232        &self,
233        ranges: &[Range<u64>],
234    ) -> Result<Vec<LeafQueryData<SeqTypes>>> {
235        self.client
236            .post("/availability/leaf/ranges")
237            .body_binary(&ranges)?
238            .send()
239            .await
240            .context("fetching leaf ranges")
241    }
242
243    async fn header_proof(&self, root: u64, id: BlockId<SeqTypes>) -> Result<HeaderProof> {
244        self.fetch(&format!("/light-client/header/{root}/{}", fmt_block_id(id)))
245            .await
246    }
247
248    async fn payload_proof(&self, height: u64) -> Result<PayloadProof> {
249        self.fetch(&format!("/light-client/payload/{height}")).await
250    }
251
252    async fn payload_proofs_for_ranges(&self, ranges: &[Range<u64>]) -> Result<Vec<PayloadProof>> {
253        self.client
254            .post("/light-client/payload/ranges")
255            .body_binary(&ranges)?
256            .send()
257            .await
258            .context("fetching payload proof ranges")
259    }
260
261    async fn payload_proofs_in_range(&self, start: u64, end: u64) -> Result<Vec<PayloadProof>> {
262        self.fetch(&format!("/light-client/payload/{start}/{end}"))
263            .await
264    }
265
266    async fn namespace_proof(&self, height: u64, namespace: NamespaceId) -> Result<NamespaceProof> {
267        self.fetch(&format!("/light-client/namespace/{height}/{namespace}"))
268            .await
269    }
270
271    async fn namespace_proofs_in_range(
272        &self,
273        start: u64,
274        end: u64,
275        namespace: NamespaceId,
276    ) -> Result<Vec<NamespaceProof>> {
277        self.fetch(&format!(
278            "/light-client/namespace/{start}/{end}/{namespace}"
279        ))
280        .await
281    }
282
283    async fn namespaces_proofs_in_range(
284        &self,
285        start: u64,
286        end: u64,
287        namespaces: &[NamespaceId],
288    ) -> Result<Vec<HashMap<NamespaceId, NamespaceProof>>> {
289        let encoded = TaggedBase64::new(NAMESPACES_PARAM_TAG, &serde_json::to_vec(namespaces)?)?;
290        self.fetch(&format!("/light-client/namespaces/{start}/{end}/{encoded}"))
291            .await
292    }
293
294    async fn stake_table_events(&self, epoch: EpochNumber) -> Result<Vec<StakeTableEvent>> {
295        self.fetch(&format!("/light-client/stake-table/{epoch}"))
296            .await
297    }
298
299    async fn cert2(&self, height: u64) -> Result<Option<Certificate2<SeqTypes>>> {
300        // A peer that lacks the cert2 responds 404. We surface that as an error rather than
301        // `Ok(None)` so `FallbackClient` keeps trying other peers instead of stopping at the first
302        // one that lacks it; it resolves absent only once every peer has 404'd.
303        //
304        // Catchup reads only the peer's storage. `/availability/cert2` would start a fetch on a
305        // peer that lacks the cert2, and query nodes peered at each other then keep re-triggering
306        // one another's fetches for heights that never had a cert2.
307        self.fetch::<Certificate2<SeqTypes>>(&format!("/catchup/{height}/cert2"))
308            .await
309            .map(Some)
310    }
311}
312
313#[cfg(feature = "client")]
314fn fmt_block_id(id: BlockId<SeqTypes>) -> String {
315    match id {
316        BlockId::Number(n) => format!("{n}"),
317        BlockId::Hash(h) => format!("hash/{h}"),
318        BlockId::PayloadHash(h) => format!("payload-hash/{h}"),
319    }
320}
321
322#[cfg(feature = "client")]
323#[derive(Clone, Debug, Builder)]
324#[builder(pattern = "owned")]
325pub struct FallbackClient<T> {
326    /// Duration to wait on one client before starting a fallback request on the next.
327    ///
328    /// When a fallback request is started, the original request will _not_ be cancelled, and so the
329    /// overall operation can complete if the original request completes shortly after the fallback
330    /// request is spawned. However, in the event that the original request takes a very long time
331    /// or does not complete at all, the fallback allows the overall request to complete without
332    /// much delay.
333    #[builder(default = Duration::from_millis(300))]
334    fallback_delay: Duration,
335
336    /// Ordered list of clients.
337    ///
338    /// Requests to the [`FallbackClient`] will be tried on each client successively, falling back
339    /// down the list until success.
340    clients: Vec<T>,
341}
342
343#[cfg(feature = "client")]
344impl<T> FallbackClient<T> {
345    pub fn new(clients: Vec<T>) -> Result<Self, FallbackClientBuilderError> {
346        Self::builder().clients(clients).build()
347    }
348
349    pub fn builder() -> FallbackClientBuilder<T> {
350        Default::default()
351    }
352}
353
354#[cfg(feature = "client")]
355impl<T> Client for FallbackClient<T>
356where
357    T: Client,
358{
359    async fn block_height(&self) -> Result<u64> {
360        self.get_any(&self.clients, T::block_height).await
361    }
362
363    async fn get_leaves_in_range(
364        &self,
365        start: usize,
366        end: usize,
367    ) -> Result<Vec<LeafQueryData<SeqTypes>>> {
368        self.get_any(&self.clients, |client| {
369            client.get_leaves_in_range(start, end)
370        })
371        .await
372    }
373
374    async fn get_leaves_for_ranges(
375        &self,
376        ranges: &[Range<u64>],
377    ) -> Result<Vec<LeafQueryData<SeqTypes>>> {
378        self.get_any(&self.clients, |client| client.get_leaves_for_ranges(ranges))
379            .await
380    }
381
382    async fn header_proof(&self, root: u64, id: BlockId<SeqTypes>) -> Result<HeaderProof> {
383        self.get_any(&self.clients, |client| client.header_proof(root, id))
384            .await
385    }
386
387    async fn leaf_proof(
388        &self,
389        id: impl Into<LeafRequest> + Send,
390        finalized: Option<u64>,
391    ) -> Result<LeafProof> {
392        let id = id.into();
393        self.get_any(&self.clients, |client| client.leaf_proof(id, finalized))
394            .await
395    }
396
397    async fn namespace_proof(&self, height: u64, namespace: NamespaceId) -> Result<NamespaceProof> {
398        self.get_any(&self.clients, |client: &T| {
399            client.namespace_proof(height, namespace)
400        })
401        .await
402    }
403
404    async fn namespace_proofs_in_range(
405        &self,
406        start: u64,
407        end: u64,
408        namespace: NamespaceId,
409    ) -> Result<Vec<NamespaceProof>> {
410        self.get_any(&self.clients, |client| {
411            client.namespace_proofs_in_range(start, end, namespace)
412        })
413        .await
414    }
415
416    async fn namespaces_proofs_in_range(
417        &self,
418        start: u64,
419        end: u64,
420        namespaces: &[NamespaceId],
421    ) -> Result<Vec<HashMap<NamespaceId, NamespaceProof>>> {
422        self.get_any(&self.clients, |client| {
423            client.namespaces_proofs_in_range(start, end, namespaces)
424        })
425        .await
426    }
427
428    async fn payload_proof(&self, height: u64) -> Result<PayloadProof> {
429        self.get_any(&self.clients, |client| client.payload_proof(height))
430            .await
431    }
432
433    async fn payload_proofs_in_range(&self, start: u64, end: u64) -> Result<Vec<PayloadProof>> {
434        self.get_any(&self.clients, |client| {
435            client.payload_proofs_in_range(start, end)
436        })
437        .await
438    }
439
440    async fn payload_proofs_for_ranges(&self, ranges: &[Range<u64>]) -> Result<Vec<PayloadProof>> {
441        self.get_any(&self.clients, |client| {
442            client.payload_proofs_for_ranges(ranges)
443        })
444        .await
445    }
446
447    async fn stake_table_events(&self, epoch: EpochNumber) -> Result<Vec<StakeTableEvent>> {
448        self.get_any(&self.clients, |client| client.stake_table_events(epoch))
449            .await
450    }
451
452    async fn cert2(&self, height: u64) -> Result<Option<Certificate2<SeqTypes>>> {
453        // Each peer 404s (an error) when it lacks the cert2, so `get_any` keeps trying peers until
454        // one has it. Once every peer has 404'd, `get_any` returns that not-found error, which we
455        // map back to `Ok(None)` (absent). Any other error propagates so the fetch is retried.
456        match self
457            .get_any(&self.clients, |client| client.cert2(height))
458            .await
459        {
460            Ok(cert2) => Ok(cert2),
461            Err(err) if is_not_found(&err) => Ok(None),
462            Err(err) => Err(err),
463        }
464    }
465}
466
467#[cfg(feature = "client")]
468impl<T> FallbackClient<T> {
469    async fn get_any<C, F>(
470        &self,
471        clients: impl IntoIterator<Item = C, IntoIter: Send>,
472        get: impl Fn(C) -> F + Send,
473    ) -> Result<F::Ok>
474    where
475        C: Send + Sync,
476        F: TryFuture<Ok: Send, Error = anyhow::Error> + Send,
477    {
478        Self::get_any_recursive(0, self.fallback_delay, clients.into_iter(), get, None)
479            .await
480            .map_err(|err| {
481                tracing::warn!(%err, "failed to fetch on all clients");
482                err.context("failed to fetch on all clients")
483            })
484    }
485
486    fn get_any_recursive<'a, C, F>(
487        i: usize,
488        timeout: Duration,
489        mut clients: impl Iterator<Item = C> + Send + 'a,
490        get: impl Fn(C) -> F + Send + 'a,
491        prev_err: Option<anyhow::Error>,
492    ) -> BoxFuture<'a, Result<F::Ok>>
493    where
494        C: Send + Sync + 'a,
495        F: TryFuture<Ok: Send, Error = anyhow::Error> + Send + 'a,
496    {
497        async move {
498            let Some(client) = clients.next() else {
499                return Err(prev_err.unwrap_or_else(|| anyhow!("no clients available")));
500            };
501            match select(
502                pin!(TryFutureExt::into_future(get(client))),
503                pin!(sleep(timeout)),
504            )
505            .await
506            {
507                Either::Left((Ok(res), _)) => {
508                    tracing::debug!("fetch succeeded on client {i}");
509                    Ok(res)
510                },
511                Either::Left((Err(err), _)) => {
512                    // If the pending future fails, abandon it and immediately start the fallback
513                    // even if we haven't hit the timeout.
514                    tracing::info!(i, %err, "failed to fetch on client");
515                    Self::get_any_recursive(i + 1, timeout, clients, get, Some(err)).await
516                },
517                Either::Right((_, fut)) => {
518                    // The timeout has elapsed. Start the request on the next client, but also keep
519                    // polling the in-progress future from the first request, since there is still a
520                    // chance that it will complete before the fallback request.
521                    tracing::info!(
522                        "fetch on client {i} took longer than {timeout:?}, starting fallback \
523                         request"
524                    );
525                    Ok(select_ok([
526                        fut.boxed(),
527                        Self::get_any_recursive(i + 1, timeout, clients, get, prev_err),
528                    ])
529                    .await?
530                    .0)
531                },
532            }
533        }
534        .boxed()
535    }
536}
537
538#[cfg(test)]
539mod test {
540    use std::{marker::PhantomData, time::Duration};
541
542    use committable::Committable;
543    use espresso_node::{
544        api::{
545            Options,
546            data_source::testing::TestableSequencerDataSource,
547            sql,
548            test_helpers::{TestNetwork, TestNetworkConfigBuilder},
549        },
550        testing::{TestConfigBuilder, wait_for_decide_on_handle},
551    };
552    use espresso_types::{Header, TEST_UPGRADE, Transaction};
553    use futures::{TryStreamExt, stream::StreamExt};
554    use hotshot_query_service_types::{
555        Resolvable,
556        availability::{BlockQueryData, LeafQueryData},
557    };
558    use pretty_assertions::assert_eq;
559    use rand::RngCore;
560    use rstest_reuse::apply;
561    use test_utils;
562    use tokio::time::sleep;
563
564    use super::*;
565    use crate::{
566        consensus::leaf::{FinalityProof, LeafProofHint},
567        testing::AlwaysTrueQuorum,
568    };
569
570    #[rstest_reuse::template]
571    #[rstest::rstest]
572    #[case::postgres(PhantomData::<sql::postgres::DataSource>)]
573    #[case::sqlite(PhantomData::<sql::sqlite::DataSource>)]
574    #[tokio::test]
575    #[test_log::test]
576    fn sql_data_sources<D: TestableSequencerDataSource>(#[case] _d: PhantomData<D>) {}
577
578    fn client(url: Url) -> impl Client {
579        // Run all tests with a fallback client where the first client is an invalid URL, to stress
580        // test failover.
581        FallbackClient::new(vec![
582            QueryServiceClient::new("http://notarealurl".parse().unwrap()),
583            QueryServiceClient::new(url),
584        ])
585        .unwrap()
586    }
587
588    #[apply(sql_data_sources)]
589    async fn test_block_height<D: TestableSequencerDataSource>(_d: PhantomData<D>) {
590        let port =
591            test_utils::reserve_tcp_port().expect("OS should have ephemeral ports available");
592        let url: Url = format!("http://localhost:{port}").parse().unwrap();
593
594        let test_config = TestConfigBuilder::default().build();
595        let storage = D::create_storage().await;
596        let persistence = <D as TestableSequencerDataSource>::persistence_options(&storage);
597
598        let config = TestNetworkConfigBuilder::<1, _, _>::with_num_nodes()
599            .api_config(D::options(&storage, Options::with_port(port)))
600            .persistences([persistence])
601            .network_config(test_config)
602            .build();
603
604        let _network = TestNetwork::new(config, TEST_UPGRADE).await;
605        let client = client(url);
606
607        // Check that the block height increases over time.
608        let initial_height = client.block_height().await.unwrap();
609        tracing::info!(initial_height);
610
611        loop {
612            sleep(Duration::from_secs(1)).await;
613            let height = client.block_height().await.unwrap();
614            if height > initial_height {
615                tracing::info!(height, initial_height, "height increased");
616                break;
617            }
618            tracing::info!("waiting for height to increase");
619        }
620    }
621
622    #[apply(sql_data_sources)]
623    async fn test_leaf_proof<D: TestableSequencerDataSource>(_d: PhantomData<D>) {
624        let port =
625            test_utils::reserve_tcp_port().expect("OS should have ephemeral ports available");
626        let url: Url = format!("http://localhost:{port}").parse().unwrap();
627
628        let test_config = TestConfigBuilder::default().build();
629        let epoch_height = test_config.hotshot_config().epoch_height;
630        let storage = D::create_storage().await;
631        let persistence = <D as TestableSequencerDataSource>::persistence_options(&storage);
632
633        let config = TestNetworkConfigBuilder::<1, _, _>::with_num_nodes()
634            .api_config(D::options(&storage, Options::with_port(port)))
635            .persistences([persistence])
636            .network_config(test_config)
637            .build();
638
639        let _network = TestNetwork::new(config, TEST_UPGRADE).await;
640        let client = client(url.clone());
641
642        // Wait for a chain of leaves to be produced.
643        let leaves: Vec<LeafQueryData<SeqTypes>> = HttpClient::new(url)
644            .socket("availability/stream/leaves/1")
645            .subscribe()
646            .await
647            .unwrap()
648            .take(2)
649            .try_collect()
650            .await
651            .unwrap();
652
653        // Get leaf proof by height.
654        let proof = client.leaf_proof(LeafId::Number(1), Some(2)).await.unwrap();
655        assert!(matches!(proof.proof(), FinalityProof::Assumption));
656        assert_eq!(
657            proof
658                .verify(LeafProofHint::assumption(leaves[1].leaf()))
659                .await
660                .unwrap(),
661            leaves[0]
662        );
663
664        // Get the same proof by various other IDs.
665        for req in [
666            LeafRequest::Header(BlockId::Number(1)),
667            LeafRequest::Leaf(LeafId::Hash(leaves[0].hash())),
668            LeafRequest::Header(BlockId::Hash(leaves[0].block_hash())),
669        ] {
670            tracing::info!(?req, "get proof by alternative ID");
671            let proof = client.leaf_proof(req, None).await.unwrap();
672            assert!(matches!(proof.proof(), FinalityProof::NewProtocol { .. }));
673            assert_eq!(
674                proof
675                    .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum::new(epoch_height)))
676                    .await
677                    .unwrap(),
678                leaves[0]
679            );
680        }
681
682        // Get a proof by payload hash (this doesn't necessarily return a unique leaf, since
683        // multiple) leaves may have the same payload.
684        let proof = client
685            .leaf_proof(BlockId::PayloadHash(leaves[0].payload_hash()), None)
686            .await
687            .unwrap();
688        assert_eq!(
689            proof
690                .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum::new(epoch_height)))
691                .await
692                .unwrap()
693                .payload_hash(),
694            leaves[0].payload_hash()
695        );
696
697        // Get a range of leaves.
698        assert_eq!(client.get_leaves_in_range(1, 3).await.unwrap(), leaves);
699    }
700
701    #[apply(sql_data_sources)]
702    async fn test_header_proof<D: TestableSequencerDataSource>(_d: PhantomData<D>) {
703        let port =
704            test_utils::reserve_tcp_port().expect("OS should have ephemeral ports available");
705        let url: Url = format!("http://localhost:{port}").parse().unwrap();
706
707        let test_config = TestConfigBuilder::default().build();
708        let storage = D::create_storage().await;
709        let persistence = <D as TestableSequencerDataSource>::persistence_options(&storage);
710
711        let config = TestNetworkConfigBuilder::<1, _, _>::with_num_nodes()
712            .api_config(D::options(&storage, Options::with_port(port)))
713            .persistences([persistence])
714            .network_config(test_config)
715            .build();
716
717        let _network = TestNetwork::new(config, TEST_UPGRADE).await;
718        let client = client(url.clone());
719        let http = HttpClient::new(url);
720
721        // Wait for a chain of blocks to be produced.
722        let headers: Vec<Header> = http
723            .socket("availability/stream/headers/1")
724            .subscribe()
725            .await
726            .unwrap()
727            .take(2)
728            .try_collect()
729            .await
730            .unwrap();
731        // Wait for the state API to catch up.
732        loop {
733            let state_height: u64 = http.get("block-state/block-height").send().await.unwrap();
734            if state_height >= 2 {
735                break;
736            }
737            tracing::info!(state_height, "waiting for block state to reach height 2");
738            sleep(Duration::from_secs(1)).await;
739        }
740
741        // Get header proof by height.
742        let proof = client.header_proof(2, BlockId::Number(1)).await.unwrap();
743        assert_eq!(
744            proof.verify(headers[1].block_merkle_tree_root()).unwrap(),
745            headers[0]
746        );
747
748        // Get the same proof by block hash.
749        let proof = client
750            .header_proof(2, BlockId::Hash(headers[0].commitment()))
751            .await
752            .unwrap();
753        assert_eq!(
754            proof.verify(headers[1].block_merkle_tree_root()).unwrap(),
755            headers[0]
756        );
757
758        // Get a proof by payload hash (this doesn't necessarily return a unique header, since
759        // multiple headers may have the same payload).
760        let proof = client
761            .header_proof(2, BlockId::PayloadHash(headers[0].payload_commitment()))
762            .await
763            .unwrap();
764        assert_eq!(
765            proof
766                .verify(headers[1].block_merkle_tree_root())
767                .unwrap()
768                .payload_commitment(),
769            headers[0].payload_commitment()
770        );
771    }
772
773    #[apply(sql_data_sources)]
774    async fn test_payload_proof<D: TestableSequencerDataSource>(_d: PhantomData<D>) {
775        let port =
776            test_utils::reserve_tcp_port().expect("OS should have ephemeral ports available");
777        let url: Url = format!("http://localhost:{port}").parse().unwrap();
778
779        let test_config = TestConfigBuilder::default().build();
780        let storage = D::create_storage().await;
781        let persistence = <D as TestableSequencerDataSource>::persistence_options(&storage);
782
783        let config = TestNetworkConfigBuilder::<1, _, _>::with_num_nodes()
784            .api_config(D::options(&storage, Options::with_port(port)))
785            .persistences([persistence])
786            .network_config(test_config)
787            .build();
788
789        let _network = TestNetwork::new(config, TEST_UPGRADE).await;
790        let client = client(url.clone());
791
792        // Wait for a few blocks to be produced.
793        let blocks: Vec<BlockQueryData<SeqTypes>> = HttpClient::new(url)
794            .socket("availability/stream/blocks/1")
795            .subscribe()
796            .await
797            .unwrap()
798            .take(2)
799            .try_collect()
800            .await
801            .unwrap();
802
803        let proof = client.payload_proof(1).await.unwrap();
804        assert_eq!(
805            proof.verify(blocks[0].header()).unwrap(),
806            *blocks[0].payload()
807        );
808
809        // The block will be empty, but check at least that the namespace proof method hits the
810        // correct endpoint and deserializes correctly.
811        let ns = NamespaceId::from(0u64);
812        let ns_proof = client.namespace_proof(1, ns).await.unwrap();
813        assert_eq!(ns_proof.verify(blocks[0].header(), ns).unwrap(), vec![]);
814
815        // Get a range of payloads.
816        let proofs = client.payload_proofs_in_range(1, 3).await.unwrap();
817        assert_eq!(proofs.len(), 2);
818        for (block, proof) in blocks.iter().zip(proofs) {
819            assert_eq!(proof.verify(block.header()).unwrap(), *block.payload());
820        }
821    }
822
823    #[apply(sql_data_sources)]
824    async fn test_namespace_proof<D: TestableSequencerDataSource>(_d: PhantomData<D>) {
825        let port =
826            test_utils::reserve_tcp_port().expect("OS should have ephemeral ports available");
827        let url: Url = format!("http://localhost:{port}").parse().unwrap();
828
829        let test_config = TestConfigBuilder::default().build();
830        let storage = D::create_storage().await;
831        let persistence = <D as TestableSequencerDataSource>::persistence_options(&storage);
832
833        let config = TestNetworkConfigBuilder::<1, _, _>::with_num_nodes()
834            .api_config(D::options(&storage, Options::with_port(port)))
835            .persistences([persistence])
836            .network_config(test_config)
837            .build();
838
839        let network = TestNetwork::new(config, TEST_UPGRADE).await;
840        let client = client(url.clone());
841        let http = HttpClient::new(url);
842
843        // Submit a couple of transactions to form non-empty blocks.
844        let ns = NamespaceId::from(1u64);
845        let mut events = network.server.event_stream();
846        let mut txs = vec![];
847        let mut headers = vec![];
848        for _ in 0..2 {
849            let mut bytes = vec![0; 32];
850            rand::thread_rng().fill_bytes(&mut bytes);
851            let tx = Transaction::new(ns, bytes);
852            network.server.submit_transaction(tx.clone()).await.unwrap();
853            let block = wait_for_decide_on_handle(&mut events, &tx).await.0;
854            tracing::info!(block, hash = %tx.commit(), ?tx, "transaction included");
855
856            let header: Header = http
857                .get(&format!("availability/header/{block}"))
858                .send()
859                .await
860                .unwrap();
861
862            txs.push(tx);
863            headers.push(header);
864        }
865
866        // Query namespaces individually.
867        for (tx, header) in txs.iter().zip(&headers) {
868            let proof = client.namespace_proof(header.height(), ns).await.unwrap();
869            assert_eq!(proof.verify(header, ns).unwrap(), std::slice::from_ref(tx));
870        }
871
872        // Query both namespaces by range.
873        let proofs = client
874            .namespace_proofs_in_range(headers[0].height(), headers[1].height() + 1, ns)
875            .await
876            .unwrap();
877        assert_eq!(
878            proofs.len() as u64,
879            headers[1].height() + 1 - headers[0].height()
880        );
881        assert_eq!(
882            proofs[0].verify(&headers[0], ns).unwrap(),
883            std::slice::from_ref(&txs[0])
884        );
885        assert_eq!(
886            proofs[proofs.len() - 1].verify(&headers[1], ns).unwrap(),
887            std::slice::from_ref(&txs[1])
888        );
889        // All other blocks in the range should be empty.
890        for (i, proof) in proofs.iter().enumerate().take(proofs.len() - 1).skip(1) {
891            let header = http
892                .get(&format!(
893                    "availability/header/{}",
894                    headers[0].height() + (i as u64)
895                ))
896                .send()
897                .await
898                .unwrap();
899            assert_eq!(proof.verify(&header, ns).unwrap(), vec![]);
900        }
901
902        let absent = NamespaceId::from(u64::from(ns) + 1);
903        let start = headers[0].height();
904        let end = headers[1].height() + 1;
905        let blocks = client
906            .namespaces_proofs_in_range(start, end, &[ns, absent])
907            .await
908            .unwrap();
909        assert_eq!(blocks.len() as u64, end - start);
910
911        for (i, block) in blocks.iter().enumerate() {
912            let header: Header = http
913                .get(&format!("availability/header/{}", start + i as u64))
914                .send()
915                .await
916                .unwrap();
917            for queried in [ns, absent] {
918                match (header.ns_table().find_ns_id(&queried), block.get(&queried)) {
919                    (Some(_), Some(proof)) => {
920                        proof.verify(&header, queried).unwrap();
921                    },
922                    (None, None) => {},
923                    (present, returned) => panic!(
924                        "block {} namespace {queried}: header present={}, returned={}",
925                        start + i as u64,
926                        present.is_some(),
927                        returned.is_some()
928                    ),
929                }
930            }
931        }
932
933        // The first and last blocks are the ones we submitted `ns` transactions to.
934        assert_eq!(
935            blocks[0].get(&ns).unwrap().verify(&headers[0], ns).unwrap(),
936            std::slice::from_ref(&txs[0])
937        );
938        assert_eq!(
939            blocks[blocks.len() - 1]
940                .get(&ns)
941                .unwrap()
942                .verify(&headers[1], ns)
943                .unwrap(),
944            std::slice::from_ref(&txs[1])
945        );
946    }
947}