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
38pub const NAMESPACES_PARAM_TAG: &str = "NS";
42
43pub trait Client: Send + Sync + 'static {
45 fn block_height(&self) -> impl Send + Future<Output = Result<u64>>;
49
50 fn leaf_proof(
56 &self,
57 id: impl Into<LeafRequest> + Send,
58 finalized: Option<u64>,
59 ) -> impl Send + Future<Output = Result<LeafProof>>;
60
61 fn header_proof(
63 &self,
64 root: u64,
65 id: BlockId<SeqTypes>,
66 ) -> impl Send + Future<Output = Result<HeaderProof>>;
67
68 fn get_leaves_in_range(
70 &self,
71 start: usize,
72 end: usize,
73 ) -> impl Send + Future<Output = Result<Vec<LeafQueryData<SeqTypes>>>>;
74
75 fn get_leaves_for_ranges(
81 &self,
82 ranges: &[Range<u64>],
83 ) -> impl Send + Future<Output = Result<Vec<LeafQueryData<SeqTypes>>>>;
84
85 fn payload_proof(&self, height: u64) -> impl Send + Future<Output = Result<PayloadProof>>;
91
92 fn payload_proofs_in_range(
94 &self,
95 start: u64,
96 end: u64,
97 ) -> impl Send + Future<Output = Result<Vec<PayloadProof>>>;
98
99 fn payload_proofs_for_ranges(
104 &self,
105 ranges: &[Range<u64>],
106 ) -> impl Send + Future<Output = Result<Vec<PayloadProof>>>;
107
108 fn namespace_proof(
114 &self,
115 height: u64,
116 namespace: NamespaceId,
117 ) -> impl Send + Future<Output = Result<NamespaceProof>>;
118
119 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 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 fn stake_table_events(
140 &self,
141 epoch: EpochNumber,
142 ) -> impl Send + Future<Output = Result<Vec<StakeTableEvent>>>;
143
144 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#[cfg(feature = "client")]
159#[derive(Clone, Debug)]
160pub struct QueryServiceClient {
161 client: HttpClient,
162}
163
164#[cfg(feature = "client")]
165impl QueryServiceClient {
166 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 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 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 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 #[builder(default = Duration::from_millis(300))]
334 fallback_delay: Duration,
335
336 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}