1use std::{
5 collections::{BTreeSet, HashMap},
6 num::NonZeroUsize,
7 ops::{Bound, Deref, Range},
8 time::Duration,
9};
10
11use alloy::primitives::utils::format_ether;
12use async_trait::async_trait;
13use chrono::SecondsFormat;
14use disco_types::{error::Error as _, status::StatusCode};
15use espresso_api::{
16 error::{AvailabilityError, SubmitError, to_status},
17 proto,
18 v1::{
19 self, HotShotAvailabilityApi,
20 database::{MigrationStatus, TableSize},
21 node::StakeTableWithEpochNumber,
22 status::NodePublicKeys,
23 },
24};
25use espresso_types::{
26 NamespaceId, NamespaceProofQueryData, NsProof, SeqTypes,
27 v0_3::{RewardAccountV1, RewardAmount as InternalRewardAmount, RewardMerkleTreeV1},
28 v0_4::{
29 RewardAccountQueryDataV2 as InternalRewardAccountQueryData, RewardAccountV2,
30 RewardMerkleTreeV2,
31 },
32 v0_6::RewardClaimError,
33};
34use futures::{StreamExt as _, TryStreamExt as _, join, stream::BoxStream};
35use hotshot_contract_adapter::reward::RewardClaimInput as InternalRewardClaimInput;
36use hotshot_new_protocol::message::Certificate2;
37use hotshot_query_service::{
38 Header as HsHeader, QueryError,
39 availability::{
40 AvailabilityDataSource, BlockId as HsBlockId, BlockQueryData, BlockSummaryQueryData,
41 LeafId as HsLeafId, LeafQueryData, Limits as HsLimits, PayloadQueryData,
42 QueryablePayload as _, TransactionQueryData, TransactionWithProofQueryData,
43 VidCommonQueryData,
44 },
45 data_source::{VersionedDataSource as _, storage::AvailabilityStorage as _},
46 explorer::{
47 BlockIdentifier, BlockRange, Error as ExplorerError, ExplorerDataSource as _,
48 GetBlockSummariesRequest, GetSearchResultsError, GetTransactionSummariesRequest,
49 TransactionIdentifier, TransactionRange, TransactionSummaryFilter,
50 },
51 merklized_state::{
52 MerklizedStateDataSource, MerklizedStateHeightPersistence, Snapshot as HsSnapshot,
53 },
54 node::{NodeDataSource as _, WindowStart},
55 status::HasMetrics as _,
56 types::HeightIndexed,
57};
58use hotshot_types::{
59 data::{EpochNumber, VidShare},
60 utils::{epoch_from_block_number, root_block_in_epoch},
61 vid::avidm::AvidMShare,
62};
63use jf_merkle_tree_compat::{MerkleTreeScheme, prelude::MerkleProof as JfMerkleProof};
64use prometheus::Encoder as _;
65use serde_json;
66use tagged_base64::TaggedBase64;
67
68use super::{
69 RewardMerkleTreeDataSource, RewardMerkleTreeV2Data as InternalRewardTreeData,
70 data_source::{
71 CatchupDataSource, DatabaseMetadataSource, HotShotConfigDataSource, NodeKeysDataSource,
72 NodeStateDataSource, PruningDataSource, RequestResponseDataSource, StakeTableDataSource,
73 StateCertDataSource, StateCertFetchingDataSource, StateSignatureDataSource,
74 SubmitDataSource, TokenDataSource,
75 },
76};
77use crate::state_cert::StateCertFetchError;
78
79const FETCH_TIMEOUT: Duration = Duration::from_millis(500);
87
88const MAX_NAMESPACES_PER_REQUEST: usize = 100;
91
92#[derive(Clone)]
97pub struct NodeApiStateImpl<D> {
98 data_source: D,
99 env_vars: std::sync::Arc<Vec<String>>,
100 public_node_config: Option<std::sync::Arc<crate::options::PublicNodeConfig>>,
101 ranges_concurrency: NonZeroUsize,
102}
103
104impl<D> NodeApiStateImpl<D> {
105 pub fn new(data_source: D) -> Self {
106 Self {
107 data_source,
108 env_vars: std::sync::Arc::new(Vec::new()),
109 public_node_config: None,
110 ranges_concurrency: NonZeroUsize::new(4).unwrap(),
111 }
112 }
113
114 pub fn with_ranges_concurrency(mut self, concurrency: NonZeroUsize) -> Self {
115 self.ranges_concurrency = concurrency;
116 self
117 }
118
119 pub fn with_env_vars(mut self, env_vars: Vec<String>) -> Self {
120 self.env_vars = std::sync::Arc::new(env_vars);
121 self
122 }
123
124 pub fn with_public_node_config(
125 mut self,
126 config: Option<crate::options::PublicNodeConfig>,
127 ) -> Self {
128 self.public_node_config = config.map(std::sync::Arc::new);
129 self
130 }
131
132 fn cloned_public_node_config(&self) -> anyhow::Result<crate::options::PublicNodeConfig> {
133 self.public_node_config.as_deref().cloned().ok_or_else(|| {
134 AvailabilityError::NotFound("runtime config not available".to_string()).into()
135 })
136 }
137}
138
139#[async_trait]
140impl<D> v1::RewardApi for NodeApiStateImpl<D>
141where
142 D: RewardMerkleTreeDataSource + Deref,
143 D::Target: hotshot_query_service::merklized_state::MerklizedStateHeightPersistence
144 + hotshot_query_service::merklized_state::MerklizedStateDataSource<
145 SeqTypes,
146 espresso_types::v0_3::RewardMerkleTreeV1,
147 {
148 <espresso_types::v0_3::RewardMerkleTreeV1 as jf_merkle_tree_compat::MerkleTreeScheme>::ARITY
149 },
150 > + hotshot_query_service::merklized_state::MerklizedStateDataSource<
151 SeqTypes,
152 espresso_types::v0_4::RewardMerkleTreeV2,
153 {
154 <espresso_types::v0_4::RewardMerkleTreeV2 as jf_merkle_tree_compat::MerkleTreeScheme>::ARITY
155 },
156 > + Send
157 + Sync,
158{
159 async fn get_reward_state_height(&self) -> anyhow::Result<u64> {
160 let ds = &*self.data_source;
161 ds.get_last_state_height()
162 .await
163 .map(|h| h as u64)
164 .map_err(classify_query_error)
165 }
166
167 async fn get_reward_state_v2_height(&self) -> anyhow::Result<u64> {
168 self.get_reward_state_height().await
171 }
172
173 async fn get_reward_account_proof_v1(
174 &self,
175 height: u64,
176 address: String,
177 ) -> anyhow::Result<espresso_types::v0_3::RewardAccountQueryDataV1> {
178 let account: RewardAccountV1 = address
179 .parse()
180 .map_err(|_| bad_request(format!("invalid ethereum address: {}", address)))?;
181
182 self.data_source
183 .load_v1_reward_account_proof(height, account)
184 .await
185 .map_err(|err| {
186 not_found(format!(
187 "failed to load v1 reward account {} at height {}: {}",
188 address, height, err
189 ))
190 })
191 }
192
193 async fn get_reward_claim_input(
194 &self,
195 block_height: u64,
196 address: String,
197 ) -> anyhow::Result<InternalRewardClaimInput> {
198 let addr: alloy::primitives::Address = address
200 .parse()
201 .map_err(|_| bad_request(format!("invalid ethereum address: {}", address)))?;
202
203 let proof = self
205 .data_source
206 .load_reward_account_proof_v2(block_height, addr.into())
207 .await
208 .map_err(|err| {
209 not_found(format!(
210 "failed to load reward account {} at height {}: {}",
211 address, block_height, err
212 ))
213 })?;
214
215 let claim_input = proof.to_reward_claim_input().map_err(|err| match err {
217 RewardClaimError::ZeroRewardError => not_found(format!(
218 "zero reward balance for {} at height {}",
219 address, block_height
220 )),
221 RewardClaimError::ProofConversionError(e) => {
222 anyhow::anyhow!(
223 "failed to create solidity proof for {} at height {}: {}",
224 address,
225 block_height,
226 e
227 )
228 },
229 })?;
230
231 Ok(claim_input)
232 }
233
234 async fn get_reward_balance(
235 &self,
236 height: u64,
237 address: String,
238 ) -> anyhow::Result<InternalRewardAmount> {
239 let addr: alloy::primitives::Address = address
241 .parse()
242 .map_err(|_| bad_request(format!("invalid ethereum address: {}", address)))?;
243
244 let proof = self
246 .data_source
247 .load_reward_account_proof_v2(height, addr.into())
248 .await
249 .map_err(|err| {
250 not_found(format!(
251 "failed to load reward account {} at height {}: {}",
252 address, height, err
253 ))
254 })?;
255
256 Ok(InternalRewardAmount(proof.balance))
257 }
258
259 async fn get_latest_reward_balance(
260 &self,
261 address: String,
262 ) -> anyhow::Result<InternalRewardAmount> {
263 let addr: alloy::primitives::Address = address
264 .parse()
265 .map_err(|_| bad_request(format!("invalid ethereum address: {}", address)))?;
266
267 let proof = self
268 .data_source
269 .load_latest_reward_account_proof_v2(addr.into())
270 .await
271 .map_err(|err| {
272 not_found(format!(
273 "failed to load latest reward account {}: {}",
274 address, err
275 ))
276 })?;
277
278 Ok(InternalRewardAmount(proof.balance))
279 }
280
281 async fn get_reward_account_proof(
282 &self,
283 height: u64,
284 address: String,
285 ) -> anyhow::Result<InternalRewardAccountQueryData> {
286 let addr: alloy::primitives::Address = address
288 .parse()
289 .map_err(|_| bad_request(format!("invalid ethereum address: {}", address)))?;
290
291 let proof = self
293 .data_source
294 .load_reward_account_proof_v2(height, addr.into())
295 .await
296 .map_err(|err| {
297 not_found(format!(
298 "failed to load reward account {} at height {}: {}",
299 address, height, err
300 ))
301 })?;
302
303 Ok(proof)
304 }
305
306 async fn get_latest_reward_account_proof(
307 &self,
308 address: String,
309 ) -> anyhow::Result<InternalRewardAccountQueryData> {
310 let addr: alloy::primitives::Address = address
312 .parse()
313 .map_err(|_| bad_request(format!("invalid ethereum address: {}", address)))?;
314
315 let proof = self
316 .data_source
317 .load_latest_reward_account_proof_v2(addr.into())
318 .await
319 .map_err(|err| {
320 not_found(format!(
321 "failed to load latest reward account {}: {}",
322 address, err
323 ))
324 })?;
325
326 Ok(proof)
327 }
328
329 async fn get_reward_amounts(
330 &self,
331 height: u64,
332 offset: u64,
333 limit: u64,
334 ) -> anyhow::Result<Vec<(alloy::primitives::Address, InternalRewardAmount)>> {
335 if limit > 10000 {
336 return Err(bad_request(format!(
337 "limit {} exceeds maximum allowed value of 10000",
338 limit
339 )));
340 }
341
342 let tree_bytes = self.data_source.load_tree(height).await.map_err(|err| {
343 not_found(format!(
344 "failed to load reward tree at height {}: {}",
345 height, err
346 ))
347 })?;
348
349 let tree_data: InternalRewardTreeData =
350 bincode::deserialize(&tree_bytes).map_err(|err| {
351 not_found(format!(
352 "failed to deserialize RewardMerkleTreeV2Data at height {}: {}",
353 height, err
354 ))
355 })?;
356
357 let offset_usize = offset as usize;
358 let limit_usize = limit as usize;
359
360 if offset_usize > tree_data.balances.len() {
361 return Err(not_found(format!("offset {} out of bounds", offset)));
362 }
363
364 let end = std::cmp::min(offset_usize + limit_usize, tree_data.balances.len());
365 let slice = &tree_data.balances[offset_usize..end];
366
367 let result: Vec<(alloy::primitives::Address, InternalRewardAmount)> = slice
368 .iter()
369 .rev()
370 .map(|(account, amount)| (account.0, *amount))
371 .collect();
372
373 Ok(result)
374 }
375
376 async fn get_reward_merkle_tree_v2(&self, height: u64) -> anyhow::Result<Vec<u8>> {
377 self.data_source.load_tree(height).await.map_err(|err| {
378 not_found(format!(
379 "failed to load reward tree at height {}: {}",
380 height, err
381 ))
382 })
383 }
384
385 async fn get_reward_state_path_v1(
386 &self,
387 snapshot: v1::Snapshot,
388 key: String,
389 ) -> anyhow::Result<v1::reward_state_v2::RewardStatePathV1> {
390 let hs_snapshot = match snapshot {
391 v1::Snapshot::Height(h) => HsSnapshot::Index(h),
392 v1::Snapshot::Commit(c) => {
393 let tb64: TaggedBase64 = c
394 .parse()
395 .map_err(|_| bad_request("failed to parse commit param"))?;
396 let commit = (&tb64)
397 .try_into()
398 .map_err(|_| bad_request("failed to parse commit param"))?;
399 HsSnapshot::Commit(commit)
400 },
401 };
402 let key: RewardAccountV1 = key
403 .parse()
404 .map_err(|_| bad_request("failed to parse Key param"))?;
405 let ds = &*self.data_source;
406 MerklizedStateDataSource::<SeqTypes, RewardMerkleTreeV1, _>::get_path(ds, hs_snapshot, key)
407 .await
408 .map_err(classify_query_error)
409 }
410
411 async fn get_reward_state_path_v2(
412 &self,
413 snapshot: v1::Snapshot,
414 key: String,
415 ) -> anyhow::Result<v1::reward_state_v2::RewardStatePathV2> {
416 let hs_snapshot = match snapshot {
417 v1::Snapshot::Height(h) => HsSnapshot::Index(h),
418 v1::Snapshot::Commit(c) => {
419 let tb64: TaggedBase64 = c
420 .parse()
421 .map_err(|_| bad_request("failed to parse commit param"))?;
422 let commit = (&tb64)
423 .try_into()
424 .map_err(|_| bad_request("failed to parse commit param"))?;
425 HsSnapshot::Commit(commit)
426 },
427 };
428 let key: RewardAccountV2 = key
429 .parse()
430 .map_err(|_| bad_request("failed to parse Key param"))?;
431 let ds = &*self.data_source;
432 MerklizedStateDataSource::<SeqTypes, RewardMerkleTreeV2, _>::get_path(ds, hs_snapshot, key)
433 .await
434 .map_err(classify_query_error)
435 }
436}
437
438#[async_trait]
439impl<D> v1::AvailabilityApi for NodeApiStateImpl<D>
440where
441 D: Deref + Clone + Send + Sync + 'static,
442 D::Target: hotshot_query_service::availability::AvailabilityDataSource<SeqTypes>
446 + hotshot_query_service::node::NodeDataSource<SeqTypes>
447 + RequestResponseDataSource<SeqTypes>
448 + StateCertDataSource
449 + StateCertFetchingDataSource<SeqTypes>
450 + Send
451 + Sync,
452{
453 async fn get_namespace_proof(
454 &self,
455 block_id: v1::availability::BlockId,
456 namespace: u32,
457 ) -> anyhow::Result<espresso_types::NamespaceProofQueryData> {
458 let ns_id = NamespaceId::from(namespace);
459
460 let hs_block_id = match block_id {
462 v1::availability::BlockId::Height(h) => HsBlockId::Number(h as usize),
463 v1::availability::BlockId::Hash(h) => {
464 let hash = h
465 .parse()
466 .map_err(|_| bad_request(format!("invalid block hash: {}", h)))?;
467 HsBlockId::Hash(hash)
468 },
469 v1::availability::BlockId::PayloadHash(h) => {
470 let payload_hash = h
471 .parse()
472 .map_err(|_| bad_request(format!("invalid payload hash: {}", h)))?;
473 HsBlockId::PayloadHash(payload_hash)
474 },
475 };
476
477 let ds = &*self.data_source;
479 let timeout = FETCH_TIMEOUT;
480 let (block_fetch, vid_fetch) =
481 join!(ds.get_block(hs_block_id), ds.get_vid_common(hs_block_id));
482 let (block, vid_common) = join!(
483 block_fetch.with_timeout(timeout),
484 vid_fetch.with_timeout(timeout)
485 );
486
487 let block =
488 block.ok_or_else(|| not_found(format!("block {} not available", hs_block_id)))?;
489 let vid_common = vid_common.ok_or_else(|| {
490 not_found(format!(
491 "VID common for block {} not available",
492 hs_block_id
493 ))
494 })?;
495
496 let ns_table = block.payload().ns_table();
498 let Some(ns_index) = ns_table.find_ns_id(&ns_id) else {
499 return Ok(espresso_types::NamespaceProofQueryData {
500 transactions: vec![],
501 proof: None,
502 });
503 };
504
505 let Some(proof) = NsProof::new(block.payload(), &ns_index, vid_common.common()) else {
507 return Ok(espresso_types::NamespaceProofQueryData {
509 transactions: vec![],
510 proof: None,
511 });
512 };
513
514 let transactions = proof.export_all_txs(&ns_id);
515
516 Ok(espresso_types::NamespaceProofQueryData {
517 transactions,
518 proof: Some(proof),
519 })
520 }
521
522 async fn get_namespace_proof_range(
523 &self,
524 from: u64,
525 until: u64,
526 namespace: u32,
527 ) -> anyhow::Result<Vec<espresso_types::NamespaceProofQueryData>> {
528 let ns_id = NamespaceId::from(namespace);
529
530 if until <= from {
532 return Err(bad_request(format!(
533 "invalid range: until ({}) must be greater than from ({})",
534 until, from
535 )));
536 }
537
538 let range_size = until - from;
539 if range_size > NAMESPACE_PROOF_RANGE_LIMIT {
540 return Err(range_exceeded(format!(
541 "range too large: {} blocks (max {})",
542 range_size, NAMESPACE_PROOF_RANGE_LIMIT
543 )));
544 }
545
546 let (blocks_stream, vids_stream) = join!(
548 self.data_source
549 .get_block_range(from as usize..until as usize),
550 self.data_source
551 .get_vid_common_range(from as usize..until as usize)
552 );
553
554 let blocks: Vec<_> = blocks_stream
555 .zip(futures::stream::iter(from..until))
556 .then(|(block, i)| async move {
557 block
558 .with_timeout(FETCH_TIMEOUT)
559 .await
560 .ok_or_else(|| not_found(format!("block {i} not available")))
561 })
562 .try_collect()
563 .await?;
564 let vids: Vec<_> = vids_stream
565 .zip(futures::stream::iter(from..until))
566 .then(|(vid, i)| async move {
567 vid.with_timeout(FETCH_TIMEOUT)
568 .await
569 .ok_or_else(|| not_found(format!("VID common for block {i} not available")))
570 })
571 .try_collect()
572 .await?;
573
574 if blocks.len() != vids.len() {
575 return Err(anyhow::anyhow!(
576 "mismatch between blocks and VID common data"
577 ));
578 }
579
580 let mut proofs = Vec::new();
582
583 for (block, vid) in blocks.into_iter().zip(vids) {
584 let ns_table = block.payload().ns_table();
585
586 if let Some(ns_index) = ns_table.find_ns_id(&ns_id) {
588 if let Some(proof) = NsProof::new(block.payload(), &ns_index, vid.common()) {
589 let transactions = proof.export_all_txs(&ns_id);
590 proofs.push(espresso_types::NamespaceProofQueryData {
591 transactions,
592 proof: Some(proof),
593 });
594 } else {
595 proofs.push(espresso_types::NamespaceProofQueryData {
597 transactions: vec![],
598 proof: None,
599 });
600 }
601 } else {
602 proofs.push(espresso_types::NamespaceProofQueryData {
604 transactions: vec![],
605 proof: None,
606 });
607 }
608 }
609
610 Ok(proofs)
611 }
612
613 async fn stream_namespace_proofs(
614 &self,
615 from: usize,
616 namespace: u32,
617 ) -> anyhow::Result<BoxStream<'static, espresso_types::NamespaceProofQueryData>> {
618 let ns_id = NamespaceId::from(namespace);
619 let ds = self.data_source.clone();
620 let blocks = (*ds).subscribe_blocks(from).await;
621 let vids = (*ds).subscribe_vid_common(from).await;
622
623 let stream = blocks
624 .zip(vids)
625 .map(move |(block, vid)| {
626 let ns_table = block.payload().ns_table();
627 if let Some(ns_index) = ns_table.find_ns_id(&ns_id) {
628 if let Some(proof) = NsProof::new(block.payload(), &ns_index, vid.common()) {
629 let transactions = proof.export_all_txs(&ns_id);
630 NamespaceProofQueryData {
631 transactions,
632 proof: Some(proof),
633 }
634 } else {
635 NamespaceProofQueryData {
636 transactions: vec![],
637 proof: None,
638 }
639 }
640 } else {
641 NamespaceProofQueryData {
642 transactions: vec![],
643 proof: None,
644 }
645 }
646 })
647 .boxed();
648
649 Ok(stream)
650 }
651
652 async fn get_incorrect_encoding_proof(
653 &self,
654 block_id: v1::availability::BlockId,
655 namespace: u32,
656 ) -> anyhow::Result<espresso_types::v0_3::AvidMIncorrectEncodingNsProof> {
657 let ns_id = NamespaceId::from(namespace);
658
659 let hs_block_id = match block_id {
660 v1::availability::BlockId::Height(h) => HsBlockId::Number(h as usize),
661 v1::availability::BlockId::Hash(h) => {
662 let hash = h
663 .parse()
664 .map_err(|_| anyhow::anyhow!("invalid block hash: {}", h))?;
665 HsBlockId::Hash(hash)
666 },
667 v1::availability::BlockId::PayloadHash(h) => {
668 let payload_hash = h
669 .parse()
670 .map_err(|_| anyhow::anyhow!("invalid payload hash: {}", h))?;
671 HsBlockId::PayloadHash(payload_hash)
672 },
673 };
674
675 let ds = &*self.data_source;
676 let timeout = FETCH_TIMEOUT;
677 let (block_fetch, vid_fetch) =
678 join!(ds.get_block(hs_block_id), ds.get_vid_common(hs_block_id));
679 let (block, vid_common) = join!(
680 block_fetch.with_timeout(timeout),
681 vid_fetch.with_timeout(timeout)
682 );
683
684 let block = block.ok_or_else(|| anyhow::anyhow!("block not found"))?;
685 let vid_common = vid_common.ok_or_else(|| anyhow::anyhow!("VID common data not found"))?;
686
687 let ns_table = block.payload().ns_table();
688 let ns_index = ns_table
689 .find_ns_id(&ns_id)
690 .ok_or_else(|| anyhow::anyhow!("namespace {} not present in block", namespace))?;
691
692 if NsProof::new(block.payload(), &ns_index, vid_common.common()).is_some() {
693 return Err(anyhow::anyhow!("block was correctly encoded"));
694 }
695
696 let vid_shares_future = ds
698 .request_vid_shares(block.height(), vid_common.clone(), Duration::from_secs(40))
699 .await;
700 let mut vid_shares = vid_shares_future
701 .await
702 .map_err(|e| anyhow::anyhow!("failed to fetch VID shares: {e:#}"))?;
703
704 if let Ok(local_share) = ds.vid_share(block.height() as usize).await {
705 vid_shares.push(local_share);
706 }
707
708 let avidm_shares: Vec<AvidMShare> = vid_shares
709 .into_iter()
710 .filter_map(|s| {
711 if let VidShare::V1(s) = s {
712 Some(s)
713 } else {
714 None
715 }
716 })
717 .collect();
718
719 match NsProof::v1_1_new_with_incorrect_encoding(
720 &avidm_shares,
721 ns_table,
722 &ns_index,
723 &vid_common.payload_hash(),
724 vid_common.common(),
725 ) {
726 Some(NsProof::V1IncorrectEncoding(proof)) => Ok(proof),
727 _ => Err(anyhow::anyhow!(
728 "failed to generate incorrect encoding proof"
729 )),
730 }
731 }
732
733 async fn get_state_cert(
734 &self,
735 epoch: u64,
736 ) -> anyhow::Result<espresso_types::StateCertQueryDataV1<SeqTypes>> {
737 self.get_state_cert_v2(epoch)
738 .await
739 .map(espresso_types::StateCertQueryDataV1::from)
740 }
741
742 async fn get_state_cert_v2(
743 &self,
744 epoch: u64,
745 ) -> anyhow::Result<espresso_types::StateCertQueryDataV2<SeqTypes>> {
746 let state_cert = self.data_source.get_state_cert_by_epoch(epoch).await?;
748
749 let cert = match state_cert {
750 Some(cert) => cert,
751 None => {
752 const TIMEOUT: Duration = Duration::from_secs(40);
754 let cert = self
755 .data_source
756 .request_state_cert(epoch, TIMEOUT)
757 .await
758 .map_err(state_cert_error)?;
759
760 self.data_source
762 .insert_state_cert(epoch, cert.clone())
763 .await?;
764
765 cert
766 },
767 };
768
769 Ok(espresso_types::StateCertQueryDataV2(cert))
770 }
771}
772
773fn not_found(msg: impl Into<String>) -> anyhow::Error {
774 AvailabilityError::NotFound(msg.into()).into()
775}
776
777fn bad_request(msg: impl Into<String>) -> anyhow::Error {
778 AvailabilityError::BadRequest(msg.into()).into()
779}
780
781fn range_exceeded(msg: impl Into<String>) -> anyhow::Error {
782 AvailabilityError::RangeExceeded(msg.into()).into()
783}
784
785fn enforce_range(from: usize, until: usize, limit: usize) -> anyhow::Result<()> {
786 if until.saturating_sub(from) > limit {
787 return Err(range_exceeded(format!(
788 "range {from}..{until} exceeds limit {limit}"
789 )));
790 }
791 Ok(())
792}
793
794fn validate_ranges(ranges: Vec<Range<u64>>, limit: usize) -> anyhow::Result<Vec<Range<u64>>> {
800 let mut total = 0usize;
801 for range in &ranges {
802 if range.is_empty() {
803 return Err(bad_request(format!(
804 "empty or inverted range {}..{}",
805 range.start, range.end
806 )));
807 }
808 if range.end > i64::MAX as u64 {
811 return Err(bad_request(format!("height {} out of range", range.end)));
812 }
813
814 total = total
815 .checked_add((range.end - range.start) as usize)
816 .ok_or_else(|| range_exceeded(format!("ranges cover more than {limit} heights")))?;
817 if total > limit {
818 return Err(range_exceeded(format!(
819 "ranges cover more than {limit} heights"
820 )));
821 }
822 }
823
824 if !ranges.is_sorted_by(|a, b| a.end <= b.start) {
827 return Err(bad_request("ranges must be ascending and disjoint"));
828 }
829
830 Ok(ranges)
831}
832
833const NAMESPACE_PROOF_RANGE_LIMIT: u64 = 100;
836
837fn small_object_range_limit() -> usize {
838 hotshot_query_service::availability::Options::default().small_object_range_limit
839}
840
841fn large_object_range_limit() -> usize {
842 hotshot_query_service::availability::Options::default().large_object_range_limit
843}
844
845#[async_trait]
846impl<D> HotShotAvailabilityApi for NodeApiStateImpl<D>
847where
848 D: Deref + Clone + Send + Sync + 'static,
849 D::Target: AvailabilityDataSource<SeqTypes>
850 + hotshot_query_service::data_source::VersionedDataSource
851 + Send
852 + Sync,
853 for<'a> <D::Target as hotshot_query_service::data_source::VersionedDataSource>::ReadOnly<'a>:
854 hotshot_query_service::data_source::storage::AvailabilityStorage<SeqTypes>,
855{
856 async fn get_leaf(
857 &self,
858 id: v1::availability::LeafId,
859 ) -> anyhow::Result<LeafQueryData<SeqTypes>> {
860 let hs_id = match id {
861 v1::availability::LeafId::Height(h) => HsLeafId::Number(h as usize),
862 v1::availability::LeafId::Hash(h) => {
863 HsLeafId::Hash(h.parse().map_err(|_| bad_request("invalid leaf hash"))?)
864 },
865 };
866 let ds = &*self.data_source;
867 ds.get_leaf(hs_id)
868 .await
869 .with_timeout(FETCH_TIMEOUT)
870 .await
871 .ok_or_else(|| not_found("leaf not found"))
872 }
873
874 async fn get_leaf_range(
875 &self,
876 from: usize,
877 until: usize,
878 ) -> anyhow::Result<Vec<LeafQueryData<SeqTypes>>> {
879 enforce_range(from, until, small_object_range_limit())?;
880 let timeout = FETCH_TIMEOUT;
881 let ds = &*self.data_source;
882 let stream = ds.get_leaf_range(from..until).await;
883 let mut results = Vec::new();
884 futures::pin_mut!(stream);
885 let mut i = from;
886 while let Some(fetch) = stream.next().await {
887 let item = fetch
888 .with_timeout(timeout)
889 .await
890 .ok_or_else(|| not_found(format!("leaf {} not found", i)))?;
891 results.push(item);
892 i += 1;
893 }
894 Ok(results)
895 }
896
897 async fn get_header(
898 &self,
899 id: v1::availability::BlockId,
900 ) -> anyhow::Result<HsHeader<SeqTypes>> {
901 let hs_id = block_id_to_hs(id)?;
902 let ds = &*self.data_source;
903 ds.get_header(hs_id)
904 .await
905 .with_timeout(FETCH_TIMEOUT)
906 .await
907 .ok_or_else(|| not_found(format!("header not found for {}", hs_id)))
908 }
909
910 async fn get_header_range(
911 &self,
912 from: usize,
913 until: usize,
914 ) -> anyhow::Result<Vec<HsHeader<SeqTypes>>> {
915 enforce_range(from, until, large_object_range_limit())?;
916 let timeout = FETCH_TIMEOUT;
917 let ds = &*self.data_source;
918 let stream = ds.get_header_range(from..until).await;
919 let mut results = Vec::new();
920 futures::pin_mut!(stream);
921 let mut i = from;
922 while let Some(fetch) = stream.next().await {
923 let item = fetch
924 .with_timeout(timeout)
925 .await
926 .ok_or_else(|| not_found(format!("header {} not found", i)))?;
927 results.push(item);
928 i += 1;
929 }
930 Ok(results)
931 }
932
933 async fn get_block(
934 &self,
935 id: v1::availability::BlockId,
936 ) -> anyhow::Result<BlockQueryData<SeqTypes>> {
937 let hs_id = block_id_to_hs(id)?;
938 let ds = &*self.data_source;
939 ds.get_block(hs_id)
940 .await
941 .with_timeout(FETCH_TIMEOUT)
942 .await
943 .ok_or_else(|| not_found(format!("block not found for {}", hs_id)))
944 }
945
946 async fn get_block_range(
947 &self,
948 from: usize,
949 until: usize,
950 ) -> anyhow::Result<Vec<BlockQueryData<SeqTypes>>> {
951 enforce_range(from, until, large_object_range_limit())?;
952 let timeout = FETCH_TIMEOUT;
953 let ds = &*self.data_source;
954 let stream = ds.get_block_range(from..until).await;
955 let mut results = Vec::new();
956 futures::pin_mut!(stream);
957 let mut i = from;
958 while let Some(fetch) = stream.next().await {
959 let item = fetch
960 .with_timeout(timeout)
961 .await
962 .ok_or_else(|| not_found(format!("block {} not found", i)))?;
963 results.push(item);
964 i += 1;
965 }
966 Ok(results)
967 }
968
969 async fn get_payload(
970 &self,
971 id: v1::availability::PayloadId,
972 ) -> anyhow::Result<PayloadQueryData<SeqTypes>> {
973 let hs_id = payload_id_to_hs(id)?;
974 let ds = &*self.data_source;
975 ds.get_payload(hs_id)
976 .await
977 .with_timeout(FETCH_TIMEOUT)
978 .await
979 .ok_or_else(|| not_found(format!("payload not found for {}", hs_id)))
980 }
981
982 async fn get_payload_range(
983 &self,
984 from: usize,
985 until: usize,
986 ) -> anyhow::Result<Vec<PayloadQueryData<SeqTypes>>> {
987 enforce_range(from, until, large_object_range_limit())?;
988 let timeout = FETCH_TIMEOUT;
989 let ds = &*self.data_source;
990 let stream = ds.get_payload_range(from..until).await;
991 let mut results = Vec::new();
992 futures::pin_mut!(stream);
993 let mut i = from;
994 while let Some(fetch) = stream.next().await {
995 let item = fetch
996 .with_timeout(timeout)
997 .await
998 .ok_or_else(|| not_found(format!("payload {} not found", i)))?;
999 results.push(item);
1000 i += 1;
1001 }
1002 Ok(results)
1003 }
1004
1005 async fn get_vid_common(
1006 &self,
1007 id: v1::availability::BlockId,
1008 ) -> anyhow::Result<VidCommonQueryData<SeqTypes>> {
1009 let hs_id = block_id_to_hs(id)?;
1010 let ds = &*self.data_source;
1011 ds.get_vid_common(hs_id)
1012 .await
1013 .with_timeout(FETCH_TIMEOUT)
1014 .await
1015 .ok_or_else(|| not_found(format!("VID common not found for {}", hs_id)))
1016 }
1017
1018 async fn get_vid_common_range(
1019 &self,
1020 from: usize,
1021 until: usize,
1022 ) -> anyhow::Result<Vec<VidCommonQueryData<SeqTypes>>> {
1023 enforce_range(from, until, small_object_range_limit())?;
1024 let timeout = FETCH_TIMEOUT;
1025 let ds = &*self.data_source;
1026 let stream = ds.get_vid_common_range(from..until).await;
1027 let mut results = Vec::new();
1028 futures::pin_mut!(stream);
1029 let mut i = from;
1030 while let Some(fetch) = stream.next().await {
1031 let item = fetch
1032 .with_timeout(timeout)
1033 .await
1034 .ok_or_else(|| not_found(format!("VID common {} not found", i)))?;
1035 results.push(item);
1036 i += 1;
1037 }
1038 Ok(results)
1039 }
1040
1041 async fn get_leaf_ranges(
1042 &self,
1043 ranges: Vec<Range<u64>>,
1044 ) -> anyhow::Result<Vec<LeafQueryData<SeqTypes>>> {
1045 let ranges = validate_ranges(ranges, small_object_range_limit())?;
1046
1047 let heights: u64 = ranges.iter().map(|range| range.end - range.start).sum();
1050 if let Ok(mut tx) = self.data_source.read().await
1051 && let Ok(leaves) = tx.get_leaf_ranges(&ranges).await
1052 && leaves.len() as u64 == heights
1053 {
1054 return Ok(leaves);
1055 }
1056
1057 let ranges: Vec<_> = futures::stream::iter(ranges)
1058 .map(|range| self.get_leaf_range(range.start as usize, range.end as usize))
1059 .buffered(self.ranges_concurrency.get())
1060 .try_collect()
1061 .await?;
1062 Ok(ranges.into_iter().flatten().collect())
1063 }
1064
1065 async fn get_block_ranges(
1066 &self,
1067 ranges: Vec<Range<u64>>,
1068 ) -> anyhow::Result<Vec<BlockQueryData<SeqTypes>>> {
1069 let ranges = validate_ranges(ranges, large_object_range_limit())?;
1070
1071 let heights: u64 = ranges.iter().map(|range| range.end - range.start).sum();
1072 if let Ok(mut tx) = self.data_source.read().await
1073 && let Ok(blocks) = tx.get_block_ranges(&ranges).await
1074 && blocks.len() as u64 == heights
1075 {
1076 return Ok(blocks);
1077 }
1078
1079 let ranges: Vec<_> = futures::stream::iter(ranges)
1080 .map(|range| self.get_block_range(range.start as usize, range.end as usize))
1081 .buffered(self.ranges_concurrency.get())
1082 .try_collect()
1083 .await?;
1084 Ok(ranges.into_iter().flatten().collect())
1085 }
1086
1087 async fn get_vid_common_ranges(
1088 &self,
1089 ranges: Vec<Range<u64>>,
1090 ) -> anyhow::Result<Vec<VidCommonQueryData<SeqTypes>>> {
1091 let ranges = validate_ranges(ranges, small_object_range_limit())?;
1092
1093 let heights: u64 = ranges.iter().map(|range| range.end - range.start).sum();
1094 if let Ok(mut tx) = self.data_source.read().await
1095 && let Ok(common) = tx.get_vid_common_ranges(&ranges).await
1096 && common.len() as u64 == heights
1097 {
1098 return Ok(common);
1099 }
1100
1101 let ranges: Vec<_> = futures::stream::iter(ranges)
1102 .map(|range| self.get_vid_common_range(range.start as usize, range.end as usize))
1103 .buffered(self.ranges_concurrency.get())
1104 .try_collect()
1105 .await?;
1106 Ok(ranges.into_iter().flatten().collect())
1107 }
1108
1109 async fn get_transaction_by_position(
1110 &self,
1111 height: u64,
1112 index: u64,
1113 ) -> anyhow::Result<TransactionQueryData<SeqTypes>> {
1114 let ds = &*self.data_source;
1115 let block = ds
1116 .get_block(HsBlockId::Number(height as usize))
1117 .await
1118 .with_timeout(FETCH_TIMEOUT)
1119 .await
1120 .ok_or_else(|| not_found(format!("block {} not found", height)))?;
1121
1122 let idx = block
1123 .payload()
1124 .nth(block.metadata(), index as usize)
1125 .ok_or_else(|| {
1126 not_found(format!(
1127 "transaction index {} out of bounds in block {}",
1128 index, height
1129 ))
1130 })?;
1131 let tx = block
1132 .transaction(&idx)
1133 .ok_or_else(|| not_found(format!("transaction not found at index {}", index)))?;
1134 TransactionQueryData::new(tx, &block, &idx, index)
1135 .ok_or_else(|| anyhow::anyhow!("failed to build transaction query data"))
1136 }
1137
1138 async fn get_transaction_by_hash(
1139 &self,
1140 hash: String,
1141 ) -> anyhow::Result<TransactionQueryData<SeqTypes>> {
1142 let ds = &*self.data_source;
1143 let tx_hash: hotshot_query_service::availability::TransactionHash<SeqTypes> = hash
1144 .parse()
1145 .map_err(|_| bad_request(format!("invalid transaction hash: {}", hash)))?;
1146 let bwt = ds
1147 .get_block_containing_transaction(tx_hash)
1148 .await
1149 .with_timeout(FETCH_TIMEOUT)
1150 .await
1151 .ok_or_else(|| not_found("transaction not found"))?;
1152 Ok(bwt.transaction)
1153 }
1154
1155 async fn get_transaction_proof_by_position(
1156 &self,
1157 height: u64,
1158 index: u64,
1159 ) -> anyhow::Result<TransactionWithProofQueryData<SeqTypes>> {
1160 let ds = &*self.data_source;
1161 let timeout = FETCH_TIMEOUT;
1162
1163 let (block_fetch, vid_fetch) = futures::join!(
1164 ds.get_block(HsBlockId::Number(height as usize)),
1165 ds.get_vid_common(HsBlockId::Number(height as usize))
1166 );
1167 let (block, vid) = futures::join!(
1168 block_fetch.with_timeout(timeout),
1169 vid_fetch.with_timeout(timeout)
1170 );
1171
1172 let block = block.ok_or_else(|| not_found(format!("block {} not found", height)))?;
1173 let vid =
1174 vid.ok_or_else(|| not_found(format!("VID common not found for block {}", height)))?;
1175
1176 let idx = block
1177 .payload()
1178 .nth(block.metadata(), index as usize)
1179 .ok_or_else(|| {
1180 not_found(format!(
1181 "transaction index {} out of bounds in block {}",
1182 index, height
1183 ))
1184 })?;
1185 let tx = block
1186 .transaction(&idx)
1187 .ok_or_else(|| not_found(format!("transaction not found at index {}", index)))?;
1188 let tx_data = TransactionQueryData::new(tx, &block, &idx, index)
1189 .ok_or_else(|| anyhow::anyhow!("failed to build transaction query data"))?;
1190 let proof = block
1191 .transaction_proof(&vid, &idx)
1192 .ok_or_else(|| anyhow::anyhow!("failed to build transaction proof"))?;
1193 Ok(TransactionWithProofQueryData::new(tx_data, proof))
1194 }
1195
1196 async fn get_transaction_proof_by_hash(
1197 &self,
1198 hash: String,
1199 ) -> anyhow::Result<TransactionWithProofQueryData<SeqTypes>> {
1200 let ds = &*self.data_source;
1201 let timeout = FETCH_TIMEOUT;
1202
1203 let tx_hash: hotshot_query_service::availability::TransactionHash<SeqTypes> = hash
1204 .parse()
1205 .map_err(|_| bad_request(format!("invalid transaction hash: {}", hash)))?;
1206 let bwt = ds
1207 .get_block_containing_transaction(tx_hash)
1208 .await
1209 .with_timeout(timeout)
1210 .await
1211 .ok_or_else(|| not_found("transaction not found"))?;
1212
1213 let vid = ds
1214 .get_vid_common(HsBlockId::Number(bwt.block.height() as usize))
1215 .await
1216 .with_timeout(timeout)
1217 .await
1218 .ok_or_else(|| {
1219 not_found(format!(
1220 "VID common not found for block {}",
1221 bwt.block.height()
1222 ))
1223 })?;
1224
1225 let proof = bwt
1226 .block
1227 .transaction_proof(&vid, &bwt.index)
1228 .ok_or_else(|| anyhow::anyhow!("failed to build transaction proof"))?;
1229 Ok(TransactionWithProofQueryData::new(bwt.transaction, proof))
1230 }
1231
1232 async fn get_block_summary(
1233 &self,
1234 height: usize,
1235 ) -> anyhow::Result<BlockSummaryQueryData<SeqTypes>> {
1236 let ds = &*self.data_source;
1237 let block = ds
1238 .get_block(HsBlockId::Number(height))
1239 .await
1240 .with_timeout(FETCH_TIMEOUT)
1241 .await
1242 .ok_or_else(|| not_found(format!("block {} not found", height)))?;
1243 Ok(BlockSummaryQueryData::from(block))
1244 }
1245
1246 async fn get_block_summary_range(
1247 &self,
1248 from: usize,
1249 until: usize,
1250 ) -> anyhow::Result<Vec<BlockSummaryQueryData<SeqTypes>>> {
1251 enforce_range(from, until, large_object_range_limit())?;
1252 let timeout = FETCH_TIMEOUT;
1253 let ds = &*self.data_source;
1254 let stream = ds.get_block_range(from..until).await;
1255 let mut results = Vec::new();
1256 futures::pin_mut!(stream);
1257 let mut i = from;
1258 while let Some(fetch) = stream.next().await {
1259 let block = fetch
1260 .with_timeout(timeout)
1261 .await
1262 .ok_or_else(|| not_found(format!("block {} not found", i)))?;
1263 results.push(BlockSummaryQueryData::from(block));
1264 i += 1;
1265 }
1266 Ok(results)
1267 }
1268
1269 async fn get_limits(&self) -> anyhow::Result<HsLimits> {
1270 Ok(HsLimits {
1271 small_object_range_limit: small_object_range_limit(),
1272 large_object_range_limit: large_object_range_limit(),
1273 })
1274 }
1275
1276 async fn get_cert2(&self, height: u64) -> anyhow::Result<Option<Certificate2<SeqTypes>>> {
1277 Ok(self
1278 .data_source
1279 .get_cert2(height)
1280 .await
1281 .with_timeout(FETCH_TIMEOUT)
1282 .await)
1283 }
1284
1285 async fn stream_leaves(
1286 &self,
1287 from: usize,
1288 ) -> anyhow::Result<BoxStream<'static, LeafQueryData<SeqTypes>>> {
1289 let ds = self.data_source.clone();
1290 Ok((*ds).subscribe_leaves(from).await.boxed())
1291 }
1292
1293 async fn stream_headers(
1294 &self,
1295 from: usize,
1296 ) -> anyhow::Result<BoxStream<'static, HsHeader<SeqTypes>>> {
1297 let ds = self.data_source.clone();
1298 Ok((*ds).subscribe_headers(from).await.boxed())
1299 }
1300
1301 async fn stream_blocks(
1302 &self,
1303 from: usize,
1304 ) -> anyhow::Result<BoxStream<'static, BlockQueryData<SeqTypes>>> {
1305 let ds = self.data_source.clone();
1306 Ok((*ds).subscribe_blocks(from).await.boxed())
1307 }
1308
1309 async fn stream_payloads(
1310 &self,
1311 from: usize,
1312 ) -> anyhow::Result<BoxStream<'static, PayloadQueryData<SeqTypes>>> {
1313 let ds = self.data_source.clone();
1314 Ok((*ds).subscribe_payloads(from).await.boxed())
1315 }
1316
1317 async fn stream_vid_common(
1318 &self,
1319 from: usize,
1320 ) -> anyhow::Result<BoxStream<'static, VidCommonQueryData<SeqTypes>>> {
1321 let ds = self.data_source.clone();
1322 Ok((*ds).subscribe_vid_common(from).await.boxed())
1323 }
1324
1325 async fn stream_transactions(
1326 &self,
1327 from: usize,
1328 namespace: Option<u32>,
1329 ) -> anyhow::Result<BoxStream<'static, TransactionQueryData<SeqTypes>>> {
1330 let ds = self.data_source.clone();
1331 let stream = (*ds)
1332 .subscribe_blocks(from)
1333 .await
1334 .flat_map(move |block| {
1335 let ns_filter = namespace.map(NamespaceId::from);
1336 let txs: Vec<TransactionQueryData<SeqTypes>> = block
1337 .enumerate()
1338 .enumerate()
1339 .filter_map(|(position_in_block, (tx_index, _tx))| {
1340 let tx = block.transaction(&tx_index)?;
1341 if let Some(ns) = ns_filter
1342 && tx.namespace() != ns
1343 {
1344 return None;
1345 }
1346 TransactionQueryData::new(tx, &block, &tx_index, position_in_block as u64)
1347 })
1348 .collect();
1349 futures::stream::iter(txs)
1350 })
1351 .boxed();
1352 Ok(stream)
1353 }
1354}
1355
1356fn block_id_to_hs(id: v1::availability::BlockId) -> anyhow::Result<HsBlockId<SeqTypes>> {
1357 match id {
1358 v1::availability::BlockId::Height(h) => Ok(HsBlockId::Number(h as usize)),
1359 v1::availability::BlockId::Hash(h) => {
1360 let hash = h
1361 .parse()
1362 .map_err(|_| bad_request(format!("invalid block hash: {}", h)))?;
1363 Ok(HsBlockId::Hash(hash))
1364 },
1365 v1::availability::BlockId::PayloadHash(h) => {
1366 let payload_hash = h
1367 .parse()
1368 .map_err(|_| bad_request(format!("invalid payload hash: {}", h)))?;
1369 Ok(HsBlockId::PayloadHash(payload_hash))
1370 },
1371 }
1372}
1373
1374fn payload_id_to_hs(id: v1::availability::PayloadId) -> anyhow::Result<HsBlockId<SeqTypes>> {
1375 match id {
1376 v1::availability::PayloadId::Height(h) => Ok(HsBlockId::Number(h as usize)),
1377 v1::availability::PayloadId::Hash(h) => {
1378 let payload_hash = h
1379 .parse()
1380 .map_err(|_| bad_request(format!("invalid payload hash: {}", h)))?;
1381 Ok(HsBlockId::PayloadHash(payload_hash))
1382 },
1383 v1::availability::PayloadId::BlockHash(h) => {
1384 let hash = h
1385 .parse()
1386 .map_err(|_| bad_request(format!("invalid block hash: {}", h)))?;
1387 Ok(HsBlockId::Hash(hash))
1388 },
1389 }
1390}
1391
1392fn classify_query_error(err: hotshot_query_service::QueryError) -> anyhow::Error {
1393 match err {
1394 QueryError::NotFound | QueryError::Missing => not_found(err.to_string()),
1395 QueryError::Pruned { .. } => AvailabilityError::Pruned(err.to_string()).into(),
1396 QueryError::Error { .. } => anyhow::anyhow!(err.to_string()),
1397 }
1398}
1399
1400#[async_trait]
1401impl<D> v1::BlockStateApi for NodeApiStateImpl<D>
1402where
1403 D: Deref + Clone + Send + Sync + 'static,
1404 D::Target: hotshot_query_service::merklized_state::MerklizedStateDataSource<
1405 SeqTypes,
1406 espresso_types::BlockMerkleTree,
1407 { <espresso_types::BlockMerkleTree as jf_merkle_tree_compat::MerkleTreeScheme>::ARITY },
1408 > + hotshot_query_service::merklized_state::MerklizedStateHeightPersistence
1409 + Send
1410 + Sync,
1411{
1412 async fn get_block_state_path(
1413 &self,
1414 snapshot: v1::Snapshot,
1415 key: String,
1416 ) -> anyhow::Result<v1::merklized_state::BlockStatePath> {
1417 let hs_snapshot = match snapshot {
1418 v1::Snapshot::Height(h) => HsSnapshot::Index(h),
1419 v1::Snapshot::Commit(c) => {
1420 let tb64: TaggedBase64 = c
1421 .parse()
1422 .map_err(|_| bad_request("failed to parse commit param"))?;
1423 let commit = (&tb64)
1424 .try_into()
1425 .map_err(|_| bad_request("failed to parse commit param"))?;
1426 HsSnapshot::Commit(commit)
1427 },
1428 };
1429 let key: u64 = key
1430 .parse()
1431 .map_err(|_| bad_request("failed to parse Key param"))?;
1432 let ds = &*self.data_source;
1433 MerklizedStateDataSource::<SeqTypes, espresso_types::BlockMerkleTree, _>::get_path(
1434 ds,
1435 hs_snapshot,
1436 key,
1437 )
1438 .await
1439 .map_err(classify_query_error)
1440 }
1441
1442 async fn get_block_state_height(&self) -> anyhow::Result<u64> {
1443 let ds = &*self.data_source;
1444 ds.get_last_state_height()
1445 .await
1446 .map(|h| h as u64)
1447 .map_err(classify_query_error)
1448 }
1449}
1450
1451#[async_trait]
1452impl<D> v1::FeeStateApi for NodeApiStateImpl<D>
1453where
1454 D: Deref + Clone + Send + Sync + 'static,
1455 D::Target: hotshot_query_service::merklized_state::MerklizedStateDataSource<
1456 SeqTypes,
1457 espresso_types::FeeMerkleTree,
1458 { <espresso_types::FeeMerkleTree as jf_merkle_tree_compat::MerkleTreeScheme>::ARITY },
1459 > + hotshot_query_service::merklized_state::MerklizedStateHeightPersistence
1460 + Send
1461 + Sync,
1462{
1463 async fn get_fee_state_path(
1464 &self,
1465 snapshot: v1::Snapshot,
1466 key: String,
1467 ) -> anyhow::Result<v1::merklized_state::FeeStatePath> {
1468 let hs_snapshot = match snapshot {
1469 v1::Snapshot::Height(h) => HsSnapshot::Index(h),
1470 v1::Snapshot::Commit(c) => {
1471 let tb64: TaggedBase64 = c
1472 .parse()
1473 .map_err(|_| bad_request("failed to parse commit param"))?;
1474 let commit = (&tb64)
1475 .try_into()
1476 .map_err(|_| bad_request("failed to parse commit param"))?;
1477 HsSnapshot::Commit(commit)
1478 },
1479 };
1480 let key: espresso_types::FeeAccount = key
1481 .parse()
1482 .map_err(|_| bad_request("failed to parse Key param"))?;
1483 let ds = &*self.data_source;
1484 MerklizedStateDataSource::<SeqTypes, espresso_types::FeeMerkleTree, _>::get_path(
1485 ds,
1486 hs_snapshot,
1487 key,
1488 )
1489 .await
1490 .map_err(classify_query_error)
1491 }
1492
1493 async fn get_fee_state_height(&self) -> anyhow::Result<u64> {
1494 let ds = &*self.data_source;
1495 ds.get_last_state_height()
1496 .await
1497 .map(|h| h as u64)
1498 .map_err(classify_query_error)
1499 }
1500
1501 async fn get_fee_balance_latest(
1502 &self,
1503 address: String,
1504 ) -> anyhow::Result<Option<espresso_types::FeeAmount>> {
1505 let key: espresso_types::FeeAccount = address
1506 .parse()
1507 .map_err(|_| bad_request("failed to parse address"))?;
1508 let ds = &*self.data_source;
1509 let height = ds
1510 .get_last_state_height()
1511 .await
1512 .map_err(classify_query_error)?;
1513 let path: JfMerkleProof<
1514 espresso_types::FeeAmount,
1515 espresso_types::FeeAccount,
1516 jf_merkle_tree_compat::prelude::Sha3Node,
1517 256,
1518 > = MerklizedStateDataSource::<SeqTypes, espresso_types::FeeMerkleTree, _>::get_path(
1519 ds,
1520 HsSnapshot::Index(height as u64),
1521 key,
1522 )
1523 .await
1524 .map_err(classify_query_error)?;
1525 Ok(path.elem().copied())
1526 }
1527}
1528
1529#[async_trait]
1530impl<D> v1::StatusApi for NodeApiStateImpl<D>
1531where
1532 D: Deref + Clone + Send + Sync + 'static,
1533 D::Target: hotshot_query_service::status::StatusDataSource + NodeKeysDataSource + Send + Sync,
1534{
1535 async fn block_height(&self) -> anyhow::Result<u64> {
1536 let ds = &*self.data_source;
1537 let h = hotshot_query_service::status::StatusDataSource::block_height(ds)
1538 .await
1539 .map_err(|e| anyhow::anyhow!("{e}"))?;
1540 Ok(h as u64)
1541 }
1542
1543 async fn success_rate(&self) -> anyhow::Result<f64> {
1544 let ds = &*self.data_source;
1545 hotshot_query_service::status::StatusDataSource::success_rate(ds)
1546 .await
1547 .map_err(|e| anyhow::anyhow!("{e}"))
1548 }
1549
1550 async fn time_since_last_decide(&self) -> anyhow::Result<u64> {
1551 let ds = &*self.data_source;
1552 hotshot_query_service::status::StatusDataSource::elapsed_time_since_last_decide(ds)
1553 .await
1554 .map_err(|e| anyhow::anyhow!("{e}"))
1555 }
1556
1557 async fn metrics(&self) -> anyhow::Result<String> {
1558 let ds = &*self.data_source;
1559 let mut buffer = Vec::new();
1561 prometheus::TextEncoder::new().encode(&ds.metrics().registry().gather(), &mut buffer)?;
1562 Ok(String::from_utf8(buffer)?)
1563 }
1564
1565 async fn keys(&self) -> anyhow::Result<NodePublicKeys> {
1566 self.data_source
1567 .node_public_keys()
1568 .await
1569 .ok_or_else(|| not_found("this node has no validator keys"))
1570 }
1571}
1572
1573#[tonic::async_trait]
1574impl<D> proto::status_service_server::StatusService for NodeApiStateImpl<D>
1575where
1576 D: Deref + Clone + Send + Sync + 'static,
1577 D::Target: hotshot_query_service::status::StatusDataSource + NodeKeysDataSource + Send + Sync,
1578{
1579 async fn get_block_height(
1580 &self,
1581 _request: tonic::Request<proto::GetBlockHeightRequest>,
1582 ) -> Result<tonic::Response<proto::BlockHeightResponse>, tonic::Status> {
1583 let height = <Self as v1::StatusApi>::block_height(self)
1584 .await
1585 .map_err(to_status)?;
1586 Ok(tonic::Response::new(proto::BlockHeightResponse { height }))
1587 }
1588
1589 async fn get_success_rate(
1590 &self,
1591 _request: tonic::Request<proto::GetSuccessRateRequest>,
1592 ) -> Result<tonic::Response<proto::SuccessRateResponse>, tonic::Status> {
1593 let rate = <Self as v1::StatusApi>::success_rate(self)
1594 .await
1595 .map_err(to_status)?;
1596 let rate = if rate.is_finite() { rate } else { 0. };
1600 Ok(tonic::Response::new(proto::SuccessRateResponse { rate }))
1601 }
1602
1603 async fn get_time_since_last_decide(
1604 &self,
1605 _request: tonic::Request<proto::GetTimeSinceLastDecideRequest>,
1606 ) -> Result<tonic::Response<proto::TimeSinceLastDecideResponse>, tonic::Status> {
1607 let seconds = <Self as v1::StatusApi>::time_since_last_decide(self)
1608 .await
1609 .map_err(to_status)?;
1610 Ok(tonic::Response::new(proto::TimeSinceLastDecideResponse {
1611 seconds,
1612 }))
1613 }
1614
1615 async fn get_node_keys(
1616 &self,
1617 _request: tonic::Request<proto::GetNodeKeysRequest>,
1618 ) -> Result<tonic::Response<proto::NodeKeysResponse>, tonic::Status> {
1619 let keys = <Self as v1::StatusApi>::keys(self)
1620 .await
1621 .map_err(to_status)?;
1622 Ok(tonic::Response::new(proto::NodeKeysResponse {
1623 eth_account: keys.eth_account.map(|account| format!("{account:#x}")),
1624 consensus_key: Some(proto::BlsPublicKey {
1625 key: keys.consensus_key.to_string(),
1626 }),
1627 state_ver_key: Some(proto::SchnorrPublicKey {
1628 key: keys.state_ver_key.to_string(),
1629 }),
1630 x25519_key: keys.x25519_key.as_ref().map(ToString::to_string),
1631 p2p_addr: keys.p2p_addr.as_ref().map(ToString::to_string),
1632 }))
1633 }
1634}
1635
1636#[async_trait]
1637impl<D> v1::ConfigApi for NodeApiStateImpl<D>
1638where
1639 D: Deref + Clone + Send + Sync + 'static,
1640 D::Target: HotShotConfigDataSource + Send + Sync,
1641{
1642 async fn hotshot_config(&self) -> anyhow::Result<espresso_types::config::PublicNetworkConfig> {
1643 let ds = &*self.data_source;
1644 Ok(ds.get_config().await)
1645 }
1646
1647 async fn env(&self) -> anyhow::Result<Vec<String>> {
1648 Ok((*self.env_vars).clone())
1649 }
1650
1651 async fn runtime_config(&self) -> anyhow::Result<v1::config::RuntimeConfig> {
1652 Ok(Box::new(self.cloned_public_node_config()?))
1653 }
1654}
1655
1656#[tonic::async_trait]
1657impl<D> proto::config_service_server::ConfigService for NodeApiStateImpl<D>
1658where
1659 D: Deref + Clone + Send + Sync + 'static,
1660 D::Target: HotShotConfigDataSource + Send + Sync,
1661{
1662 async fn get_hotshot_config(
1663 &self,
1664 _request: tonic::Request<proto::GetHotshotConfigRequest>,
1665 ) -> Result<tonic::Response<proto::HotshotConfigResponse>, tonic::Status> {
1666 let config = <Self as v1::ConfigApi>::hotshot_config(self)
1667 .await
1668 .map_err(to_status)?;
1669 Ok(tonic::Response::new(config.into()))
1670 }
1671
1672 async fn get_env(
1673 &self,
1674 _request: tonic::Request<proto::GetEnvRequest>,
1675 ) -> Result<tonic::Response<proto::EnvResponse>, tonic::Status> {
1676 let variables = <Self as v1::ConfigApi>::env(self)
1677 .await
1678 .map_err(to_status)?
1679 .into_iter()
1680 .map(|entry| {
1681 let (name, value) = entry
1682 .split_once('=')
1683 .expect("ConfigApi::env yields KEY=value entries");
1684 proto::EnvVar {
1685 name: name.to_string(),
1686 value: value.to_string(),
1687 }
1688 })
1689 .collect();
1690 Ok(tonic::Response::new(proto::EnvResponse { variables }))
1691 }
1692
1693 async fn get_runtime_config(
1694 &self,
1695 _request: tonic::Request<proto::GetRuntimeConfigRequest>,
1696 ) -> Result<tonic::Response<proto::RuntimeConfigResponse>, tonic::Status> {
1697 let config = self.cloned_public_node_config().map_err(to_status)?;
1698 Ok(tonic::Response::new(config.into()))
1699 }
1700}
1701
1702#[async_trait]
1703impl<D> v1::NodeApi for NodeApiStateImpl<D>
1704where
1705 D: Deref + Clone + Send + Sync + 'static,
1706 D::Target: hotshot_query_service::node::NodeDataSource<SeqTypes>
1707 + StakeTableDataSource<SeqTypes>
1708 + PruningDataSource
1709 + Send
1710 + Sync,
1711{
1712 async fn block_height(&self) -> anyhow::Result<u64> {
1713 let ds = &*self.data_source;
1714 let h = hotshot_query_service::node::NodeDataSource::block_height(ds)
1715 .await
1716 .map_err(classify_query_error)?;
1717 Ok(h as u64)
1718 }
1719
1720 async fn count_transactions(
1721 &self,
1722 from: Option<u64>,
1723 to: Option<u64>,
1724 namespace: Option<u64>,
1725 ) -> anyhow::Result<u64> {
1726 let ds = &*self.data_source;
1727 let from = match from {
1728 Some(f) => Bound::Included(f as usize),
1729 None => Bound::Unbounded,
1730 };
1731 let to = match to {
1732 Some(t) => Bound::Included(t as usize),
1733 None => Bound::Unbounded,
1734 };
1735 let ns = namespace.map(espresso_types::NamespaceId::from);
1736 let count = ds
1737 .count_transactions_in_range((from, to), ns)
1738 .await
1739 .map_err(classify_query_error)?;
1740 Ok(count as u64)
1741 }
1742
1743 async fn payload_size(
1744 &self,
1745 from: Option<u64>,
1746 to: Option<u64>,
1747 namespace: Option<u64>,
1748 ) -> anyhow::Result<u64> {
1749 let ds = &*self.data_source;
1750 let from = match from {
1751 Some(f) => Bound::Included(f as usize),
1752 None => Bound::Unbounded,
1753 };
1754 let to = match to {
1755 Some(t) => Bound::Included(t as usize),
1756 None => Bound::Unbounded,
1757 };
1758 let ns = namespace.map(espresso_types::NamespaceId::from);
1759 let size = ds
1760 .payload_size_in_range((from, to), ns)
1761 .await
1762 .map_err(classify_query_error)?;
1763 Ok(size as u64)
1764 }
1765
1766 async fn get_vid_share(
1767 &self,
1768 id: v1::VidShareId,
1769 ) -> anyhow::Result<hotshot_types::data::VidShare> {
1770 let ds = &*self.data_source;
1771 let node_id: HsBlockId<SeqTypes> = match id {
1772 v1::VidShareId::Height(h) => HsBlockId::Number(h as usize),
1773 v1::VidShareId::Hash(h) => HsBlockId::Hash(
1774 h.parse()
1775 .map_err(|_| bad_request(format!("invalid block hash: {h}")))?,
1776 ),
1777 v1::VidShareId::PayloadHash(h) => HsBlockId::PayloadHash(
1778 h.parse()
1779 .map_err(|_| bad_request(format!("invalid payload hash: {h}")))?,
1780 ),
1781 };
1782 hotshot_query_service::node::NodeDataSource::vid_share(ds, node_id)
1783 .await
1784 .map_err(classify_query_error)
1785 }
1786
1787 async fn sync_status(
1788 &self,
1789 ) -> anyhow::Result<hotshot_query_service::node::SyncStatusQueryData> {
1790 let ds = &*self.data_source;
1791 hotshot_query_service::node::NodeDataSource::sync_status(ds)
1792 .await
1793 .map_err(classify_query_error)
1794 }
1795
1796 async fn get_header_window(
1797 &self,
1798 start: v1::HeaderWindowStart,
1799 end: u64,
1800 ) -> anyhow::Result<
1801 hotshot_query_service::node::TimeWindowQueryData<hotshot_query_service::Header<SeqTypes>>,
1802 > {
1803 let ds = &*self.data_source;
1804 let start: WindowStart<SeqTypes> = match start {
1805 v1::HeaderWindowStart::Time(t) => WindowStart::Time(t),
1806 v1::HeaderWindowStart::Height(h) => WindowStart::Height(h),
1807 v1::HeaderWindowStart::Hash(h) => WindowStart::Hash(
1808 h.parse()
1809 .map_err(|err| bad_request(format!("invalid block hash {h}: {err}")))?,
1810 ),
1811 };
1812 ds.get_header_window(start, end, node_window_limit())
1813 .await
1814 .map_err(classify_query_error)
1815 }
1816
1817 async fn limits(&self) -> anyhow::Result<hotshot_query_service::node::Limits> {
1818 Ok(hotshot_query_service::node::Limits {
1819 window_limit: node_window_limit(),
1820 })
1821 }
1822
1823 async fn stake_table(
1824 &self,
1825 epoch: u64,
1826 ) -> anyhow::Result<Vec<hotshot_types::PeerConfig<SeqTypes>>> {
1827 let ds = &*self.data_source;
1828 ds.get_stake_table(Some(hotshot_types::data::EpochNumber::new(epoch)))
1829 .await
1830 }
1831
1832 async fn stake_table_current(&self) -> anyhow::Result<StakeTableWithEpochNumber<SeqTypes>> {
1833 let ds = &*self.data_source;
1834 ds.get_stake_table_current().await
1835 }
1836
1837 async fn da_stake_table(
1838 &self,
1839 epoch: u64,
1840 ) -> anyhow::Result<Vec<hotshot_types::PeerConfig<SeqTypes>>> {
1841 let ds = &*self.data_source;
1842 ds.get_da_stake_table(Some(hotshot_types::data::EpochNumber::new(epoch)))
1843 .await
1844 }
1845
1846 async fn da_stake_table_current(&self) -> anyhow::Result<StakeTableWithEpochNumber<SeqTypes>> {
1847 let ds = &*self.data_source;
1848 ds.get_da_stake_table_current().await
1849 }
1850
1851 async fn get_validators(
1852 &self,
1853 epoch: u64,
1854 ) -> anyhow::Result<
1855 indexmap::IndexMap<
1856 alloy::primitives::Address,
1857 espresso_types::v0_3::AuthenticatedValidator<espresso_types::PubKey>,
1858 >,
1859 > {
1860 let ds = &*self.data_source;
1861 ds.get_validators(hotshot_types::data::EpochNumber::new(epoch))
1862 .await
1863 }
1864
1865 async fn get_all_validators(
1866 &self,
1867 epoch: u64,
1868 offset: u64,
1869 limit: u64,
1870 ) -> anyhow::Result<Vec<espresso_types::v0_3::RegisteredValidator<espresso_types::PubKey>>>
1871 {
1872 if limit > 1000 {
1873 return Err(bad_request("Limit cannot be greater than 1000"));
1874 }
1875 let ds = &*self.data_source;
1876 ds.get_all_validators(hotshot_types::data::EpochNumber::new(epoch), offset, limit)
1877 .await
1878 }
1879
1880 async fn current_proposal_participation(
1881 &self,
1882 ) -> anyhow::Result<std::collections::HashMap<espresso_types::PubKey, f64>> {
1883 let ds = &*self.data_source;
1884 Ok(ds.current_proposal_participation().await)
1885 }
1886
1887 async fn proposal_participation(
1888 &self,
1889 epoch: u64,
1890 ) -> anyhow::Result<std::collections::HashMap<espresso_types::PubKey, f64>> {
1891 let ds = &*self.data_source;
1892 Ok(ds
1893 .proposal_participation(hotshot_types::data::EpochNumber::new(epoch))
1894 .await)
1895 }
1896
1897 async fn current_vote_participation(
1898 &self,
1899 ) -> anyhow::Result<std::collections::HashMap<espresso_types::PubKey, f64>> {
1900 let ds = &*self.data_source;
1901 Ok(ds.current_vote_participation().await)
1902 }
1903
1904 async fn vote_participation(
1905 &self,
1906 epoch: u64,
1907 ) -> anyhow::Result<std::collections::HashMap<espresso_types::PubKey, f64>> {
1908 let ds = &*self.data_source;
1909 Ok(ds
1910 .vote_participation(hotshot_types::data::EpochNumber::new(epoch))
1911 .await)
1912 }
1913
1914 async fn get_block_reward(
1915 &self,
1916 epoch: Option<u64>,
1917 ) -> anyhow::Result<Option<espresso_types::v0_3::RewardAmount>> {
1918 let ds = &*self.data_source;
1919 ds.get_block_reward(epoch.map(hotshot_types::data::EpochNumber::new))
1920 .await
1921 }
1922
1923 async fn get_oldest_block(
1924 &self,
1925 ) -> anyhow::Result<Option<hotshot_query_service::availability::BlockQueryData<SeqTypes>>> {
1926 let ds = &*self.data_source;
1927 ds.get_oldest_block().await
1928 }
1929
1930 async fn get_oldest_leaf(
1931 &self,
1932 ) -> anyhow::Result<Option<hotshot_query_service::availability::LeafQueryData<SeqTypes>>> {
1933 let ds = &*self.data_source;
1934 ds.get_oldest_leaf().await
1935 }
1936}
1937
1938#[tonic::async_trait]
1939impl<D> proto::node_service_server::NodeService for NodeApiStateImpl<D>
1940where
1941 D: Deref + Clone + Send + Sync + 'static,
1942 D::Target: hotshot_query_service::node::NodeDataSource<SeqTypes>
1943 + StakeTableDataSource<SeqTypes>
1944 + PruningDataSource
1945 + Send
1946 + Sync,
1947{
1948 async fn get_transaction_count(
1949 &self,
1950 request: tonic::Request<proto::GetTransactionCountRequest>,
1951 ) -> Result<tonic::Response<proto::TransactionCountResponse>, tonic::Status> {
1952 let proto::GetTransactionCountRequest {
1953 from,
1954 to,
1955 namespace,
1956 } = request.into_inner();
1957 let count = <Self as v1::NodeApi>::count_transactions(self, from, to, namespace)
1958 .await
1959 .map_err(to_status)?;
1960 Ok(tonic::Response::new(proto::TransactionCountResponse {
1961 count,
1962 }))
1963 }
1964
1965 async fn get_payload_size(
1966 &self,
1967 request: tonic::Request<proto::GetPayloadSizeRequest>,
1968 ) -> Result<tonic::Response<proto::PayloadSizeResponse>, tonic::Status> {
1969 let proto::GetPayloadSizeRequest {
1970 from,
1971 to,
1972 namespace,
1973 } = request.into_inner();
1974 let size = <Self as v1::NodeApi>::payload_size(self, from, to, namespace)
1975 .await
1976 .map_err(to_status)?;
1977 Ok(tonic::Response::new(proto::PayloadSizeResponse { size }))
1978 }
1979
1980 async fn get_sync_status(
1981 &self,
1982 _request: tonic::Request<proto::GetSyncStatusRequest>,
1983 ) -> Result<tonic::Response<proto::SyncStatusResponse>, tonic::Status> {
1984 let status = <Self as v1::NodeApi>::sync_status(self)
1985 .await
1986 .map_err(to_status)?;
1987 Ok(tonic::Response::new(proto::SyncStatusResponse::from(
1988 status,
1989 )))
1990 }
1991
1992 async fn get_block_reward(
1993 &self,
1994 request: tonic::Request<proto::GetBlockRewardRequest>,
1995 ) -> Result<tonic::Response<proto::BlockRewardResponse>, tonic::Status> {
1996 let reward = <Self as v1::NodeApi>::get_block_reward(self, request.into_inner().epoch)
1997 .await
1998 .map_err(to_status)?;
1999 Ok(tonic::Response::new(proto::BlockRewardResponse {
2000 amount: reward.map(|amount| amount.to_string()),
2001 }))
2002 }
2003
2004 async fn get_vid_share(
2005 &self,
2006 request: tonic::Request<proto::GetVidShareRequest>,
2007 ) -> Result<tonic::Response<proto::VidShareResponse>, tonic::Status> {
2008 let request = request.into_inner();
2009 let id = match (request.height, request.hash, request.payload_hash) {
2010 (Some(height), None, None) => v1::VidShareId::Height(height),
2011 (None, Some(hash), None) => v1::VidShareId::Hash(hash),
2012 (None, None, Some(hash)) => v1::VidShareId::PayloadHash(hash),
2013 _ => {
2014 return Err(tonic::Status::invalid_argument(
2015 "set exactly one of height, hash or payload_hash",
2016 ));
2017 },
2018 };
2019 let share = <Self as v1::NodeApi>::get_vid_share(self, id)
2020 .await
2021 .map_err(to_status)?;
2022 Ok(tonic::Response::new(proto::VidShareResponse::try_from(
2023 &share,
2024 )?))
2025 }
2026
2027 async fn get_header_window(
2028 &self,
2029 request: tonic::Request<proto::GetHeaderWindowRequest>,
2030 ) -> Result<tonic::Response<proto::HeaderWindowResponse>, tonic::Status> {
2031 let request = request.into_inner();
2032 let start = match (request.start_time, request.start_height, request.start_hash) {
2033 (Some(time), None, None) => v1::HeaderWindowStart::Time(time),
2034 (None, Some(height), None) => v1::HeaderWindowStart::Height(height),
2035 (None, None, Some(hash)) => v1::HeaderWindowStart::Hash(hash),
2036 _ => {
2037 return Err(tonic::Status::invalid_argument(
2038 "set exactly one of start_time, start_height or start_hash",
2039 ));
2040 },
2041 };
2042 let end = required(request.end, "end")?;
2043 let window = <Self as v1::NodeApi>::get_header_window(self, start, end)
2044 .await
2045 .map_err(to_status)?;
2046 Ok(tonic::Response::new(proto::HeaderWindowResponse::from(
2047 &window,
2048 )))
2049 }
2050
2051 async fn get_node_block_height(
2052 &self,
2053 _request: tonic::Request<proto::GetNodeBlockHeightRequest>,
2054 ) -> Result<tonic::Response<proto::NodeBlockHeightResponse>, tonic::Status> {
2055 let height = <Self as v1::NodeApi>::block_height(self)
2056 .await
2057 .map_err(to_status)?;
2058 Ok(tonic::Response::new(proto::NodeBlockHeightResponse {
2059 height,
2060 }))
2061 }
2062
2063 async fn get_node_limits(
2064 &self,
2065 _request: tonic::Request<proto::GetNodeLimitsRequest>,
2066 ) -> Result<tonic::Response<proto::NodeLimitsResponse>, tonic::Status> {
2067 let limits = <Self as v1::NodeApi>::limits(self)
2068 .await
2069 .map_err(to_status)?;
2070 Ok(tonic::Response::new(proto::NodeLimitsResponse::from(
2071 limits,
2072 )))
2073 }
2074
2075 async fn get_stake_table(
2076 &self,
2077 request: tonic::Request<proto::GetStakeTableRequest>,
2078 ) -> Result<tonic::Response<proto::StakeTableResponse>, tonic::Status> {
2079 let table = match request.into_inner().epoch {
2080 Some(epoch) => StakeTableWithEpochNumber {
2081 epoch: Some(EpochNumber::new(epoch)),
2082 stake_table: <Self as v1::NodeApi>::stake_table(self, epoch)
2083 .await
2084 .map_err(to_status)?,
2085 },
2086 None => <Self as v1::NodeApi>::stake_table_current(self)
2087 .await
2088 .map_err(to_status)?,
2089 };
2090 Ok(tonic::Response::new(table.into()))
2091 }
2092
2093 async fn get_validators(
2094 &self,
2095 request: tonic::Request<proto::GetValidatorsRequest>,
2096 ) -> Result<tonic::Response<proto::ValidatorsResponse>, tonic::Status> {
2097 let epoch = required(request.into_inner().epoch, "epoch")?;
2098 let validators = <Self as v1::NodeApi>::get_validators(self, epoch)
2099 .await
2100 .map_err(to_status)?;
2101 let mut validators: Vec<proto::Validator> = validators
2102 .into_values()
2103 .map(|authenticated| authenticated.into_inner().into())
2104 .collect();
2105 validators.sort_by(|a, b| a.account.cmp(&b.account));
2107 Ok(tonic::Response::new(proto::ValidatorsResponse {
2108 validators,
2109 }))
2110 }
2111
2112 async fn get_all_validators(
2113 &self,
2114 request: tonic::Request<proto::GetAllValidatorsRequest>,
2115 ) -> Result<tonic::Response<proto::ValidatorsResponse>, tonic::Status> {
2116 let request = request.into_inner();
2117 let validators = <Self as v1::NodeApi>::get_all_validators(
2118 self,
2119 required(request.epoch, "epoch")?,
2120 required(request.offset, "offset")?,
2121 required(request.limit, "limit")?,
2122 )
2123 .await
2124 .map_err(to_status)?;
2125 Ok(tonic::Response::new(proto::ValidatorsResponse {
2126 validators: validators.into_iter().map(Into::into).collect(),
2127 }))
2128 }
2129
2130 async fn get_proposal_participation(
2131 &self,
2132 request: tonic::Request<proto::GetProposalParticipationRequest>,
2133 ) -> Result<tonic::Response<proto::ParticipationResponse>, tonic::Status> {
2134 let fractions = match request.into_inner().epoch {
2135 Some(epoch) => <Self as v1::NodeApi>::proposal_participation(self, epoch).await,
2136 None => <Self as v1::NodeApi>::current_proposal_participation(self).await,
2137 }
2138 .map_err(to_status)?;
2139 Ok(tonic::Response::new(fractions.into()))
2140 }
2141
2142 async fn get_vote_participation(
2143 &self,
2144 request: tonic::Request<proto::GetVoteParticipationRequest>,
2145 ) -> Result<tonic::Response<proto::ParticipationResponse>, tonic::Status> {
2146 let fractions = match request.into_inner().epoch {
2147 Some(epoch) => <Self as v1::NodeApi>::vote_participation(self, epoch).await,
2148 None => <Self as v1::NodeApi>::current_vote_participation(self).await,
2149 }
2150 .map_err(to_status)?;
2151 Ok(tonic::Response::new(fractions.into()))
2152 }
2153}
2154
2155impl From<crate::options::PublicNodeConfig> for proto::RuntimeConfigResponse {
2159 fn from(config: crate::options::PublicNodeConfig) -> Self {
2160 let crate::options::PublicNodeConfig {
2164 orchestrator_url,
2165 cliquenet_bind_address,
2166 cliquenet_advertise_address,
2167 public_api_url,
2168 builder_urls,
2169 state_relay_server_url,
2170 state_peers,
2171 config_peers,
2172 is_da,
2173 genesis_file,
2174 genesis: _,
2175 identity,
2176 catchup_base_timeout: _,
2177 local_catchup_timeout: _,
2178 http_catchup_delay: _,
2179 bootstrap_epoch_catchup_timeout: _,
2180 catchup_backoff: _,
2181 follower,
2182 proposal_fetcher: _,
2183 l1: _,
2184 l1_provider_count,
2185 l1_ws_provider_count,
2186 storage,
2187 modules,
2188 } = config;
2189 let crate::options::Identity {
2190 node_name,
2191 node_description,
2192 company_name,
2193 company_website,
2194 country_code,
2195 latitude,
2196 longitude,
2197 operating_system,
2198 node_type,
2199 network_type,
2200 icon_14x14_1x,
2201 icon_14x14_2x,
2202 icon_14x14_3x,
2203 icon_24x24_1x,
2204 icon_24x24_2x,
2205 icon_24x24_3x,
2206 } = identity;
2207 Self {
2208 is_da,
2209 identity: Some(proto::NodeIdentity {
2210 node_name,
2211 node_description,
2212 company_name,
2213 company_website: company_website.map(|url| url.to_string()),
2214 country_code,
2215 latitude,
2216 longitude,
2217 operating_system,
2218 node_type,
2219 network_type,
2220 icon_14x14_1x: icon_14x14_1x.map(|url| url.to_string()),
2221 icon_14x14_2x: icon_14x14_2x.map(|url| url.to_string()),
2222 icon_14x14_3x: icon_14x14_3x.map(|url| url.to_string()),
2223 icon_24x24_1x: icon_24x24_1x.map(|url| url.to_string()),
2224 icon_24x24_2x: icon_24x24_2x.map(|url| url.to_string()),
2225 icon_24x24_3x: icon_24x24_3x.map(|url| url.to_string()),
2226 }),
2227 storage: Some(storage.into()),
2228 genesis_file: genesis_file.to_string(),
2229 public_api_url: public_api_url.map(|url| url.to_string()),
2230 builder_urls: builder_urls.iter().map(ToString::to_string).collect(),
2231 state_relay_server_url: state_relay_server_url.to_string(),
2232 state_peers: state_peers.iter().map(ToString::to_string).collect(),
2233 config_peers: config_peers
2234 .unwrap_or_default()
2235 .iter()
2236 .map(ToString::to_string)
2237 .collect(),
2238 orchestrator_url: orchestrator_url.to_string(),
2239 cliquenet_bind_address: cliquenet_bind_address.unbracketed_string(),
2242 cliquenet_advertise_address: cliquenet_advertise_address
2243 .map(|addr| addr.unbracketed_string()),
2244 l1_provider_count: l1_provider_count as u64,
2245 l1_ws_provider_count: l1_ws_provider_count as u64,
2246 modules: Some(modules.into()),
2247 follower: follower.map(|follower| proto::FollowerModule {
2248 poll_interval_ms: follower.poll_interval.as_millis() as u64,
2249 max_blocks_per_poll: follower.max_blocks_per_poll,
2250 }),
2251 ..Default::default()
2253 }
2254 }
2255}
2256
2257impl From<crate::options::StorageConfig> for proto::NodeStorage {
2258 fn from(storage: crate::options::StorageConfig) -> Self {
2259 Self {
2260 backend: match storage.backend {
2261 crate::options::StorageBackend::Sql => proto::StorageBackend::Sql,
2262 crate::options::StorageBackend::Fs => proto::StorageBackend::Fs,
2263 crate::options::StorageBackend::FsDefault => proto::StorageBackend::FsDefault,
2264 }
2265 .into(),
2266 fs: storage.fs.map(|fs| proto::FsStorage {
2267 path: fs.path.display().to_string(),
2268 consensus_view_retention: fs.consensus_view_retention,
2269 }),
2270 sql: storage.sql.map(Into::into),
2271 }
2272 }
2273}
2274
2275impl From<crate::options::SqlStorageConfig> for proto::SqlStorage {
2276 fn from(sql: crate::options::SqlStorageConfig) -> Self {
2277 let millis = |duration: Duration| duration.as_millis() as u64;
2278 Self {
2279 prune: sql.prune,
2280 archive: sql.archive,
2281 lightweight: sql.lightweight,
2282 disable_proactive_fetching: sql.disable_proactive_fetching,
2283 fetch_rate_limit: sql.fetch_rate_limit.map(|limit| limit as u64),
2284 active_fetch_delay_ms: sql.active_fetch_delay.map(millis),
2285 chunk_fetch_delay_ms: sql.chunk_fetch_delay.map(millis),
2286 sync_status_chunk_size: sql.sync_status_chunk_size.map(|size| size as u64),
2287 sync_status_ttl_ms: sql.sync_status_ttl.map(millis),
2288 proactive_scan_chunk_size: sql.proactive_scan_chunk_size.map(|size| size as u64),
2289 proactive_scan_interval_ms: sql.proactive_scan_interval.map(millis),
2290 idle_connection_timeout_ms: millis(sql.idle_connection_timeout),
2291 connection_timeout_ms: millis(sql.connection_timeout),
2292 slow_statement_threshold_ms: millis(sql.slow_statement_threshold),
2293 statement_timeout_ms: millis(sql.statement_timeout),
2294 min_connections: sql.min_connections,
2295 max_connections: sql.max_connections,
2296 query_min_connections: sql.query_min_connections,
2297 query_max_connections: sql.query_max_connections,
2298 pruning: Some(proto::PruningConfig {
2299 pruning_threshold: sql.pruning.pruning_threshold,
2300 minimum_retention_ms: sql.pruning.minimum_retention.map(millis),
2301 target_retention_ms: sql.pruning.target_retention.map(millis),
2302 batch_size: sql.pruning.batch_size,
2303 max_usage: sql.pruning.max_usage.map(u32::from),
2304 interval_ms: sql.pruning.interval.map(millis),
2305 pages: sql.pruning.pages,
2306 }),
2307 consensus_pruning: Some(proto::ConsensusPruningConfig {
2308 target_retention: sql.consensus_pruning.target_retention,
2309 minimum_retention: sql.consensus_pruning.minimum_retention,
2310 target_usage: sql.consensus_pruning.target_usage,
2311 }),
2312 }
2313 }
2314}
2315
2316impl From<crate::options::ApiModulesConfig> for proto::ApiModules {
2317 fn from(modules: crate::options::ApiModulesConfig) -> Self {
2318 Self {
2319 http: modules.http.map(|http| proto::HttpModule {
2320 port: http.port.into(),
2321 max_connections: http.max_connections.map(|max| max as u64),
2322 tonic_port: http.tonic_port.map(u32::from),
2323 }),
2324 query: modules.query.map(|query| proto::QueryModule {
2325 peers: query.peers.iter().map(ToString::to_string).collect(),
2326 light_client: Some(proto::LightClientModuleOptions {
2327 num_stake_tables_in_memory: query.light_client.num_stake_tables_in_memory
2328 as u64,
2329 }),
2330 light_client_db: Some(proto::LightClientDbOptions {
2331 num_connections: query.light_client_db.num_connections,
2332 num_leaves: query.light_client_db.num_leaves,
2333 lc_path: query
2334 .light_client_db
2335 .lc_path
2336 .map(|path| path.display().to_string()),
2337 }),
2338 }),
2339 submit: modules.submit,
2340 status: modules.status,
2341 catchup: modules.catchup,
2342 config: modules.config,
2343 hotshot_events: false,
2345 explorer: modules.explorer,
2346 light_client: modules.light_client,
2347 }
2348 }
2349}
2350
2351fn node_window_limit() -> usize {
2352 hotshot_query_service::node::Options::default().window_limit
2353}
2354
2355#[async_trait]
2356impl<D> v1::CatchupApi for NodeApiStateImpl<D>
2357where
2358 D: Deref + Clone + Send + Sync + 'static,
2359 D::Target: CatchupDataSource + NodeStateDataSource + Send + Sync,
2360{
2361 async fn get_account(
2362 &self,
2363 height: u64,
2364 view: u64,
2365 address: String,
2366 ) -> anyhow::Result<espresso_types::AccountQueryData> {
2367 let ds = &*self.data_source;
2368 let view = hotshot_types::data::ViewNumber::new(view);
2369 let account: espresso_types::FeeAccount = address
2370 .parse()
2371 .map_err(|err| bad_request(format!("malformed fee account {address}: {err}")))?;
2372 let instance = ds.node_state().await;
2373 ds.get_account(&instance, height, view, account)
2374 .await
2375 .map_err(|err| not_found(format!("{err:#}")))
2376 }
2377
2378 async fn get_accounts(
2379 &self,
2380 height: u64,
2381 view: u64,
2382 accounts: Vec<espresso_types::FeeAccount>,
2383 ) -> anyhow::Result<espresso_types::FeeMerkleTree> {
2384 let ds = &*self.data_source;
2385 let view = hotshot_types::data::ViewNumber::new(view);
2386 let instance = ds.node_state().await;
2387 ds.get_accounts(&instance, height, view, &accounts)
2388 .await
2389 .map_err(|err| not_found(format!("{err:#}")))
2390 }
2391
2392 async fn get_blocks_frontier(
2393 &self,
2394 height: u64,
2395 view: u64,
2396 ) -> anyhow::Result<super::BlocksFrontier> {
2397 let ds = &*self.data_source;
2398 let view = hotshot_types::data::ViewNumber::new(view);
2399 let instance = ds.node_state().await;
2400 ds.get_frontier(&instance, height, view)
2401 .await
2402 .map_err(|err| not_found(format!("{err:#}")))
2403 }
2404
2405 async fn get_chain_config(
2406 &self,
2407 commitment: String,
2408 ) -> anyhow::Result<espresso_types::v0_3::ChainConfig> {
2409 let ds = &*self.data_source;
2410 let parsed: committable::Commitment<espresso_types::v0_3::ChainConfig> = commitment
2411 .parse()
2412 .map_err(|err| bad_request(format!("malformed chain config commitment: {err}")))?;
2413 ds.get_chain_config(parsed)
2414 .await
2415 .map_err(|err| not_found(format!("{err:#}")))
2416 }
2417
2418 async fn get_leaf_chain(&self, height: u64) -> anyhow::Result<Vec<espresso_types::Leaf2>> {
2419 let ds = &*self.data_source;
2420 ds.get_leaf_chain(height)
2421 .await
2422 .map_err(|err| not_found(format!("{err:#}")))
2423 }
2424
2425 async fn get_cert2(
2426 &self,
2427 height: u64,
2428 ) -> anyhow::Result<espresso_types::Certificate2<SeqTypes>> {
2429 let ds = &*self.data_source;
2430 let response = ds
2431 .get_cert2(height)
2432 .await
2433 .map_err(|err| not_found(format!("{err:#}")))?;
2434 response.ok_or_else(|| not_found(format!("no cert2 available for height {height}")))
2435 }
2436
2437 async fn get_reward_account_v1(
2438 &self,
2439 height: u64,
2440 view: u64,
2441 address: String,
2442 ) -> anyhow::Result<espresso_types::v0_3::RewardAccountQueryDataV1> {
2443 let ds = &*self.data_source;
2444 let view = hotshot_types::data::ViewNumber::new(view);
2445 let account: espresso_types::v0_4::RewardAccountV2 = address
2446 .parse()
2447 .map_err(|err| bad_request(format!("malformed reward account {address}: {err}")))?;
2448 let instance = ds.node_state().await;
2449 ds.get_reward_account_v1(&instance, height, view, account.into())
2450 .await
2451 .map_err(|err| not_found(format!("{err:#}")))
2452 }
2453
2454 async fn get_reward_accounts_v1(
2455 &self,
2456 height: u64,
2457 view: u64,
2458 accounts: Vec<espresso_types::v0_3::RewardAccountV1>,
2459 ) -> anyhow::Result<espresso_types::v0_3::RewardMerkleTreeV1> {
2460 let ds = &*self.data_source;
2461 let view = hotshot_types::data::ViewNumber::new(view);
2462 let instance = ds.node_state().await;
2463 ds.get_reward_accounts_v1(&instance, height, view, &accounts)
2464 .await
2465 .map_err(|err| not_found(format!("{err:#}")))
2466 }
2467
2468 async fn get_reward_account_v2(
2469 &self,
2470 height: u64,
2471 view: u64,
2472 address: String,
2473 ) -> anyhow::Result<espresso_types::v0_4::RewardAccountQueryDataV2> {
2474 let ds = &*self.data_source;
2475 let view = hotshot_types::data::ViewNumber::new(view);
2476 let account: espresso_types::v0_4::RewardAccountV2 = address
2477 .parse()
2478 .map_err(|err| bad_request(format!("malformed reward account {address}: {err}")))?;
2479 let instance = ds.node_state().await;
2480 ds.get_reward_account_v2(&instance, height, view, account)
2481 .await
2482 .map_err(|err| not_found(format!("{err:#}")))
2483 }
2484
2485 async fn get_reward_merkle_tree_v2(
2486 &self,
2487 height: u64,
2488 view: u64,
2489 ) -> anyhow::Result<serde_json::Value> {
2490 let ds = &*self.data_source;
2491 let view = hotshot_types::data::ViewNumber::new(view);
2492 let bytes = ds
2493 .get_reward_merkle_tree_v2(height, view)
2494 .await
2495 .map_err(|err| not_found(format!("{err:#}")))?;
2496 Ok(serde_json::to_value(bytes)?)
2499 }
2500
2501 async fn get_state_cert(
2502 &self,
2503 epoch: u64,
2504 ) -> anyhow::Result<
2505 hotshot_types::simple_certificate::LightClientStateUpdateCertificateV2<SeqTypes>,
2506 > {
2507 let ds = &*self.data_source;
2508 ds.get_state_cert(epoch)
2509 .await
2510 .map_err(|err| not_found(format!("{err:#}")))
2511 }
2512}
2513
2514#[tonic::async_trait]
2515impl<D> proto::catchup_service_server::CatchupService for NodeApiStateImpl<D>
2516where
2517 D: Deref + Clone + Send + Sync + 'static,
2518 D::Target: CatchupDataSource + NodeStateDataSource + Send + Sync,
2519{
2520 async fn get_catchup_fee_account(
2521 &self,
2522 request: tonic::Request<proto::GetCatchupFeeAccountRequest>,
2523 ) -> Result<tonic::Response<proto::FeeAccountProofResponse>, tonic::Status> {
2524 let request = request.into_inner();
2525 let (height, view) = catchup_at(request.height, request.view)?;
2526 let address = required(request.address, "address")?;
2527 let query = <Self as v1::CatchupApi>::get_account(self, height, view, address)
2528 .await
2529 .map_err(to_status)?;
2530 Ok(tonic::Response::new(query.into()))
2531 }
2532
2533 async fn get_catchup_fee_accounts(
2534 &self,
2535 request: tonic::Request<proto::GetCatchupFeeAccountsRequest>,
2536 ) -> Result<tonic::Response<proto::FeeMerkleTreeResponse>, tonic::Status> {
2537 let request = request.into_inner();
2538 let (height, view) = catchup_at(request.height, request.view)?;
2539 let accounts = request
2540 .addresses
2541 .iter()
2542 .map(|account| {
2543 account.parse().map_err(|err| {
2544 tonic::Status::invalid_argument(format!(
2545 "malformed fee account {account}: {err}"
2546 ))
2547 })
2548 })
2549 .collect::<Result<Vec<espresso_types::FeeAccount>, _>>()?;
2550 let tree = <Self as v1::CatchupApi>::get_accounts(self, height, view, accounts)
2551 .await
2552 .map_err(to_status)?;
2553 Ok(tonic::Response::new(proto::FeeMerkleTreeResponse {
2554 tree: serialize_tree(&tree)?,
2555 }))
2556 }
2557
2558 async fn get_catchup_blocks_frontier(
2559 &self,
2560 request: tonic::Request<proto::GetCatchupBlocksFrontierRequest>,
2561 ) -> Result<tonic::Response<proto::MerklePathResponse>, tonic::Status> {
2562 let request = request.into_inner();
2563 let (height, view) = catchup_at(request.height, request.view)?;
2564 let frontier = <Self as v1::CatchupApi>::get_blocks_frontier(self, height, view)
2565 .await
2566 .map_err(to_status)?;
2567 Ok(tonic::Response::new((&frontier).into()))
2568 }
2569
2570 async fn get_catchup_chain_config(
2571 &self,
2572 request: tonic::Request<proto::GetCatchupChainConfigRequest>,
2573 ) -> Result<tonic::Response<proto::CatchupChainConfigResponse>, tonic::Status> {
2574 let commitment = required(request.into_inner().commitment, "commitment")?;
2575 let config = <Self as v1::CatchupApi>::get_chain_config(self, commitment)
2576 .await
2577 .map_err(to_status)?;
2578 Ok(tonic::Response::new(proto::CatchupChainConfigResponse {
2579 chain_config: Some((&config).into()),
2580 }))
2581 }
2582
2583 async fn get_catchup_leaf_chain(
2584 &self,
2585 request: tonic::Request<proto::GetCatchupLeafChainRequest>,
2586 ) -> Result<tonic::Response<proto::CatchupLeafChainResponse>, tonic::Status> {
2587 let height = required(request.into_inner().height, "height")?;
2588 let leaves = <Self as v1::CatchupApi>::get_leaf_chain(self, height)
2589 .await
2590 .map_err(to_status)?;
2591 Ok(tonic::Response::new(leaves.as_slice().into()))
2592 }
2593
2594 async fn get_catchup_cert2(
2595 &self,
2596 request: tonic::Request<proto::GetCatchupCert2Request>,
2597 ) -> Result<tonic::Response<proto::Cert2Response>, tonic::Status> {
2598 let height = required(request.into_inner().height, "height")?;
2599 let cert = <Self as v1::CatchupApi>::get_cert2(self, height)
2600 .await
2601 .map_err(to_status)?;
2602 Ok(tonic::Response::new(proto::Cert2Response {
2603 certificate: Some((&cert).into()),
2604 }))
2605 }
2606
2607 async fn get_catchup_reward_account(
2608 &self,
2609 request: tonic::Request<proto::GetCatchupRewardAccountRequest>,
2610 ) -> Result<tonic::Response<proto::RewardAccountProofResponse>, tonic::Status> {
2611 let request = request.into_inner();
2612 let (height, view) = catchup_at(request.height, request.view)?;
2613 let address = required(request.address, "address")?;
2614 let query = <Self as v1::CatchupApi>::get_reward_account_v1(self, height, view, address)
2615 .await
2616 .map_err(to_status)?;
2617 Ok(tonic::Response::new(query.into()))
2618 }
2619
2620 async fn get_catchup_reward_accounts(
2621 &self,
2622 request: tonic::Request<proto::GetCatchupRewardAccountsRequest>,
2623 ) -> Result<tonic::Response<proto::RewardMerkleTreeV1Response>, tonic::Status> {
2624 let request = request.into_inner();
2625 let (height, view) = catchup_at(request.height, request.view)?;
2626 let accounts = request
2627 .addresses
2628 .iter()
2629 .map(|account| {
2630 account.parse().map_err(|err| {
2631 tonic::Status::invalid_argument(format!(
2632 "malformed reward account {account}: {err}"
2633 ))
2634 })
2635 })
2636 .collect::<Result<Vec<RewardAccountV1>, _>>()?;
2637 let tree = <Self as v1::CatchupApi>::get_reward_accounts_v1(self, height, view, accounts)
2638 .await
2639 .map_err(to_status)?;
2640 Ok(tonic::Response::new(proto::RewardMerkleTreeV1Response {
2641 tree: serialize_tree(&tree)?,
2642 }))
2643 }
2644
2645 async fn get_catchup_reward_account_v2(
2646 &self,
2647 request: tonic::Request<proto::GetCatchupRewardAccountV2Request>,
2648 ) -> Result<tonic::Response<proto::RewardAccountProofResponse>, tonic::Status> {
2649 let request = request.into_inner();
2650 let (height, view) = catchup_at(request.height, request.view)?;
2651 let address = required(request.address, "address")?;
2652 let query = <Self as v1::CatchupApi>::get_reward_account_v2(self, height, view, address)
2653 .await
2654 .map_err(to_status)?;
2655 Ok(tonic::Response::new(query.into()))
2656 }
2657
2658 async fn get_catchup_reward_merkle_tree_v2(
2659 &self,
2660 request: tonic::Request<proto::GetCatchupRewardMerkleTreeV2Request>,
2661 ) -> Result<tonic::Response<proto::RewardMerkleTreeV2Response>, tonic::Status> {
2662 let request = request.into_inner();
2663 let (height, view) = catchup_at(request.height, request.view)?;
2664 let tree = self
2666 .data_source
2667 .get_reward_merkle_tree_v2(height, hotshot_types::data::ViewNumber::new(view))
2668 .await
2669 .map_err(|err| to_status(not_found(format!("{err:#}"))))?;
2670 Ok(tonic::Response::new(proto::RewardMerkleTreeV2Response {
2671 tree,
2672 }))
2673 }
2674
2675 async fn get_catchup_state_cert(
2676 &self,
2677 request: tonic::Request<proto::GetCatchupStateCertRequest>,
2678 ) -> Result<tonic::Response<proto::StateCertV2Response>, tonic::Status> {
2679 let epoch = required(request.into_inner().epoch, "epoch")?;
2680 let cert = <Self as v1::CatchupApi>::get_state_cert(self, epoch)
2681 .await
2682 .map_err(to_status)?;
2683 Ok(tonic::Response::new(
2684 (&espresso_types::v0_4::StateCertQueryDataV2(cert)).into(),
2685 ))
2686 }
2687}
2688
2689fn serialize_tree<T: serde::Serialize>(tree: &T) -> Result<Vec<u8>, tonic::Status> {
2692 bincode::serialize(tree)
2693 .map_err(|err| tonic::Status::internal(format!("serializing tree: {err}")))
2694}
2695
2696fn catchup_at(height: Option<u64>, view: Option<u64>) -> Result<(u64, u64), tonic::Status> {
2698 Ok((required(height, "height")?, required(view, "view")?))
2699}
2700
2701#[async_trait]
2702impl<D> v1::SubmitApi for NodeApiStateImpl<D>
2703where
2704 D: Deref + Clone + Send + Sync + 'static,
2705 D::Target: SubmitDataSourceErased + Send + Sync,
2706{
2707 async fn submit(
2708 &self,
2709 tx: espresso_types::Transaction,
2710 ) -> anyhow::Result<committable::Commitment<espresso_types::Transaction>> {
2711 let ds = &*self.data_source;
2712 ds.submit_erased(tx).await.map_err(|err| {
2713 if err.is::<SubmitError>() {
2715 err
2716 } else {
2717 anyhow::anyhow!("{err:#}")
2718 }
2719 })
2720 }
2721}
2722
2723#[tonic::async_trait]
2724impl<D> proto::submit_service_server::SubmitService for NodeApiStateImpl<D>
2725where
2726 D: Deref + Clone + Send + Sync + 'static,
2727 D::Target: SubmitDataSourceErased + Send + Sync,
2728{
2729 async fn submit_transaction(
2730 &self,
2731 request: tonic::Request<proto::SubmitTransactionRequest>,
2732 ) -> Result<tonic::Response<proto::SubmitTransactionResponse>, tonic::Status> {
2733 let request = request.into_inner();
2734 let namespace = required(namespace_from_query(request.namespace)?, "namespace")?;
2735 let payload = required(request.payload, "payload")?;
2736 let transaction = espresso_types::Transaction::new(namespace.into(), payload);
2737 let hash = v1::SubmitApi::submit(self, transaction)
2738 .await
2739 .map_err(to_status)?;
2740 Ok(tonic::Response::new(proto::SubmitTransactionResponse {
2741 hash: hash.to_string(),
2742 }))
2743 }
2744}
2745
2746#[async_trait]
2750pub(crate) trait SubmitDataSourceErased {
2751 async fn submit_erased(
2752 &self,
2753 tx: espresso_types::Transaction,
2754 ) -> anyhow::Result<committable::Commitment<espresso_types::Transaction>>;
2755}
2756
2757#[async_trait]
2758impl<D> SubmitDataSourceErased
2759 for hotshot_query_service::data_source::ExtensibleDataSource<D, crate::api::ApiState>
2760where
2761 D: Send + Sync,
2762{
2763 async fn submit_erased(
2764 &self,
2765 tx: espresso_types::Transaction,
2766 ) -> anyhow::Result<committable::Commitment<espresso_types::Transaction>> {
2767 <Self as SubmitDataSource>::submit(self, tx).await
2768 }
2769}
2770
2771#[async_trait]
2774impl SubmitDataSourceErased for crate::api::ApiState {
2775 async fn submit_erased(
2776 &self,
2777 tx: espresso_types::Transaction,
2778 ) -> anyhow::Result<committable::Commitment<espresso_types::Transaction>> {
2779 <Self as SubmitDataSource>::submit(self, tx).await
2780 }
2781}
2782
2783#[async_trait]
2784impl<D> v1::StateSignatureApi for NodeApiStateImpl<D>
2785where
2786 D: Deref + Clone + Send + Sync + 'static,
2787 D::Target: StateSignatureDataSourceErased + Send + Sync,
2788{
2789 async fn get_state_signature(
2790 &self,
2791 height: u64,
2792 ) -> anyhow::Result<hotshot_types::light_client::LCV3StateSignatureRequestBody> {
2793 let ds = &*self.data_source;
2794 ds.get_state_signature_erased(height)
2795 .await
2796 .ok_or_else(|| not_found("Signature not found."))
2797 }
2798}
2799
2800#[tonic::async_trait]
2801impl<D> proto::state_signature_service_server::StateSignatureService for NodeApiStateImpl<D>
2802where
2803 D: Deref + Clone + Send + Sync + 'static,
2804 D::Target: StateSignatureDataSourceErased + Send + Sync,
2805{
2806 async fn get_state_signature(
2807 &self,
2808 request: tonic::Request<proto::GetStateSignatureRequest>,
2809 ) -> Result<tonic::Response<proto::StateSignatureResponse>, tonic::Status> {
2810 let height = required(request.into_inner().height, "height")?;
2811 let body = v1::StateSignatureApi::get_state_signature(self, height)
2812 .await
2813 .map_err(to_status)?;
2814 Ok(tonic::Response::new((&body).into()))
2815 }
2816}
2817
2818#[async_trait]
2819pub(crate) trait StateSignatureDataSourceErased {
2820 async fn get_state_signature_erased(
2821 &self,
2822 height: u64,
2823 ) -> Option<hotshot_types::light_client::LCV3StateSignatureRequestBody>;
2824}
2825
2826#[async_trait]
2827impl<D> StateSignatureDataSourceErased
2828 for hotshot_query_service::data_source::ExtensibleDataSource<D, crate::api::ApiState>
2829where
2830 D: Send + Sync,
2831{
2832 async fn get_state_signature_erased(
2833 &self,
2834 height: u64,
2835 ) -> Option<hotshot_types::light_client::LCV3StateSignatureRequestBody> {
2836 <Self as StateSignatureDataSource>::get_state_signature(self, height).await
2837 }
2838}
2839
2840#[async_trait]
2843impl StateSignatureDataSourceErased for crate::api::ApiState {
2844 async fn get_state_signature_erased(
2845 &self,
2846 height: u64,
2847 ) -> Option<hotshot_types::light_client::LCV3StateSignatureRequestBody> {
2848 <Self as StateSignatureDataSource>::get_state_signature(self, height).await
2849 }
2850}
2851
2852#[async_trait]
2853impl<D> v1::ExplorerApi for NodeApiStateImpl<D>
2854where
2855 D: Deref + Clone + Send + Sync + 'static,
2856 D::Target: hotshot_query_service::explorer::ExplorerDataSource<SeqTypes> + Send + Sync,
2857{
2858 async fn get_block_detail(
2859 &self,
2860 ident: v1::BlockIdent,
2861 ) -> anyhow::Result<hotshot_query_service::explorer::BlockDetailResponse<SeqTypes>> {
2862 let ds = &*self.data_source;
2863 let target = match ident {
2864 v1::BlockIdent::Height(h) => BlockIdentifier::Height(h as usize),
2865 v1::BlockIdent::Hash(h) => BlockIdentifier::Hash(
2866 h.parse()
2867 .map_err(|err| bad_request(format!("invalid block hash {h}: {err}")))?,
2868 ),
2869 v1::BlockIdent::Latest => BlockIdentifier::Latest,
2870 };
2871 ds.get_block_detail(target)
2872 .await
2873 .map(Into::into)
2874 .map_err(ExplorerError::GetBlockDetail)
2875 .map_err(explorer_error)
2876 }
2877
2878 async fn get_block_summaries(
2879 &self,
2880 target: v1::BlockIdent,
2881 limit: u64,
2882 ) -> anyhow::Result<hotshot_query_service::explorer::BlockSummaryResponse<SeqTypes>> {
2883 let ds = &*self.data_source;
2884 let num_blocks = std::num::NonZeroUsize::new(limit as usize)
2885 .ok_or_else(|| bad_request("limit must be greater than 0"))?;
2886 if num_blocks.get() > 100 {
2887 return Err(bad_request("limit must be <= 100"));
2888 }
2889 let target = match target {
2890 v1::BlockIdent::Height(h) => BlockIdentifier::Height(h as usize),
2891 v1::BlockIdent::Hash(h) => BlockIdentifier::Hash(
2892 h.parse()
2893 .map_err(|err| bad_request(format!("invalid block hash {h}: {err}")))?,
2894 ),
2895 v1::BlockIdent::Latest => BlockIdentifier::Latest,
2896 };
2897 ds.get_block_summaries(GetBlockSummariesRequest(BlockRange { target, num_blocks }))
2898 .await
2899 .map(Into::into)
2900 .map_err(ExplorerError::GetBlockSummaries)
2901 .map_err(explorer_error)
2902 }
2903
2904 async fn get_transaction_detail(
2905 &self,
2906 ident: v1::TxIdent,
2907 ) -> anyhow::Result<hotshot_query_service::explorer::TransactionDetailResponse<SeqTypes>> {
2908 let ds = &*self.data_source;
2909 let target = match ident {
2910 v1::TxIdent::HeightAndOffset(h, o) => {
2911 TransactionIdentifier::HeightAndOffset(h as usize, o as usize)
2912 },
2913 v1::TxIdent::Hash(h) => TransactionIdentifier::Hash(
2914 h.parse()
2915 .map_err(|err| bad_request(format!("invalid tx hash {h}: {err}")))?,
2916 ),
2917 v1::TxIdent::Latest => TransactionIdentifier::Latest,
2918 };
2919 ds.get_transaction_detail(target)
2920 .await
2921 .map(Into::into)
2922 .map_err(ExplorerError::GetTransactionDetail)
2923 .map_err(explorer_error)
2924 }
2925
2926 async fn get_transaction_summaries(
2927 &self,
2928 target: v1::TxIdent,
2929 limit: u64,
2930 filter: v1::TxSummaryFilter,
2931 ) -> anyhow::Result<hotshot_query_service::explorer::TransactionSummariesResponse<SeqTypes>>
2932 {
2933 let ds = &*self.data_source;
2934 let num_transactions = std::num::NonZeroUsize::new(limit as usize)
2935 .ok_or_else(|| bad_request("limit must be greater than 0"))?;
2936 if num_transactions.get() > 100 {
2937 return Err(bad_request("limit must be <= 100"));
2938 }
2939 let target = match target {
2940 v1::TxIdent::HeightAndOffset(h, o) => {
2941 if limit.checked_add(o).is_none_or(|n| n > i64::MAX as u64) {
2944 return Err(bad_request(format!("offset {o} is too large")));
2945 }
2946 TransactionIdentifier::HeightAndOffset(h as usize, o as usize)
2947 },
2948 v1::TxIdent::Hash(h) => TransactionIdentifier::Hash(
2949 h.parse()
2950 .map_err(|err| bad_request(format!("invalid tx hash {h}: {err}")))?,
2951 ),
2952 v1::TxIdent::Latest => TransactionIdentifier::Latest,
2953 };
2954 let filter = match filter {
2955 v1::TxSummaryFilter::None => TransactionSummaryFilter::None,
2956 v1::TxSummaryFilter::Block(b) => TransactionSummaryFilter::Block(b as usize),
2957 v1::TxSummaryFilter::Namespace(n) => TransactionSummaryFilter::RollUp(n.into()),
2958 };
2959 ds.get_transaction_summaries(GetTransactionSummariesRequest {
2960 range: TransactionRange {
2961 target,
2962 num_transactions,
2963 },
2964 filter,
2965 })
2966 .await
2967 .map(Into::into)
2968 .map_err(ExplorerError::GetTransactionSummaries)
2969 .map_err(explorer_error)
2970 }
2971
2972 async fn get_explorer_summary(
2973 &self,
2974 ) -> anyhow::Result<hotshot_query_service::explorer::ExplorerSummaryResponse<SeqTypes>> {
2975 let ds = &*self.data_source;
2976 ds.get_explorer_summary()
2977 .await
2978 .map(Into::into)
2979 .map_err(ExplorerError::GetExplorerSummary)
2980 .map_err(explorer_error)
2981 }
2982
2983 async fn get_search_result(
2984 &self,
2985 query: String,
2986 ) -> anyhow::Result<hotshot_query_service::explorer::SearchResultResponse<SeqTypes>> {
2987 let ds = &*self.data_source;
2988 let parsed: tagged_base64::TaggedBase64 = query
2989 .parse()
2990 .map_err(|err| bad_request(format!("invalid search query {query}: {err}")))?;
2991 ds.get_search_results(parsed)
2992 .await
2993 .map(Into::into)
2994 .map_err(|err| match err {
2995 GetSearchResultsError::InvalidQuery(_) => bad_request(format!(
2996 "unsupported search query {query}: expected a BLOCK~ or TX~ hash"
2997 )),
2998 err => explorer_error(ExplorerError::GetSearchResults(err)),
2999 })
3000 }
3001}
3002
3003#[tonic::async_trait]
3004impl<D> proto::explorer_service_server::ExplorerService for NodeApiStateImpl<D>
3005where
3006 D: Deref + Clone + Send + Sync + 'static,
3007 D::Target: hotshot_query_service::explorer::ExplorerDataSource<SeqTypes> + Send + Sync,
3008{
3009 async fn get_explorer_block_detail(
3010 &self,
3011 request: tonic::Request<proto::GetExplorerBlockDetailRequest>,
3012 ) -> Result<tonic::Response<proto::ExplorerBlockDetailResponse>, tonic::Status> {
3013 let request = request.into_inner();
3014 let ident = block_ident(request.height, request.hash)?;
3015 let detail = <Self as v1::ExplorerApi>::get_block_detail(self, ident)
3016 .await
3017 .map_err(to_status)?;
3018 Ok(tonic::Response::new(proto::ExplorerBlockDetailResponse {
3019 block_detail: Some((&detail.block_detail).into()),
3020 }))
3021 }
3022
3023 async fn get_explorer_block_summaries(
3024 &self,
3025 request: tonic::Request<proto::GetExplorerBlockSummariesRequest>,
3026 ) -> Result<tonic::Response<proto::ExplorerBlockSummariesResponse>, tonic::Status> {
3027 let request = request.into_inner();
3028 let limit = required(request.limit, "limit")?;
3029 let ident = block_ident(request.height, request.hash)?;
3030 let summaries = <Self as v1::ExplorerApi>::get_block_summaries(self, ident, limit)
3031 .await
3032 .map_err(to_status)?;
3033 Ok(tonic::Response::new(
3034 proto::ExplorerBlockSummariesResponse {
3035 block_summaries: summaries.block_summaries.iter().map(Into::into).collect(),
3036 },
3037 ))
3038 }
3039
3040 async fn get_explorer_transaction_detail(
3041 &self,
3042 request: tonic::Request<proto::GetExplorerTransactionDetailRequest>,
3043 ) -> Result<tonic::Response<proto::ExplorerTransactionDetailResponse>, tonic::Status> {
3044 let request = request.into_inner();
3045 let ident = transaction_ident(request.height, request.offset, request.hash)?;
3046 let detail = <Self as v1::ExplorerApi>::get_transaction_detail(self, ident)
3047 .await
3048 .map_err(to_status)?
3049 .transaction_detail;
3050 Ok(tonic::Response::new(
3051 proto::ExplorerTransactionDetailResponse {
3052 details: Some((&detail.details).into()),
3053 data: detail.data.iter().map(Into::into).collect(),
3054 },
3055 ))
3056 }
3057
3058 async fn get_explorer_transaction_summaries(
3059 &self,
3060 request: tonic::Request<proto::GetExplorerTransactionSummariesRequest>,
3061 ) -> Result<tonic::Response<proto::ExplorerTransactionSummariesResponse>, tonic::Status> {
3062 let request = request.into_inner();
3063 let limit = required(request.limit, "limit")?;
3064 let ident = transaction_ident(request.height, request.offset, request.hash)?;
3065 let filter = match (request.block, namespace_from_query(request.namespace)?) {
3066 (Some(_), Some(_)) => {
3067 return Err(tonic::Status::invalid_argument(
3068 "block and namespace are mutually exclusive",
3069 ));
3070 },
3071 (Some(block), None) => v1::TxSummaryFilter::Block(block),
3072 (None, Some(namespace)) => v1::TxSummaryFilter::Namespace(namespace.into()),
3073 (None, None) => v1::TxSummaryFilter::None,
3074 };
3075 let summaries =
3076 <Self as v1::ExplorerApi>::get_transaction_summaries(self, ident, limit, filter)
3077 .await
3078 .map_err(to_status)?;
3079 Ok(tonic::Response::new(
3080 proto::ExplorerTransactionSummariesResponse {
3081 transaction_summaries: summaries
3082 .transaction_summaries
3083 .iter()
3084 .map(Into::into)
3085 .collect(),
3086 },
3087 ))
3088 }
3089
3090 async fn get_explorer_summary(
3091 &self,
3092 _request: tonic::Request<proto::GetExplorerSummaryRequest>,
3093 ) -> Result<tonic::Response<proto::ExplorerSummaryResponse>, tonic::Status> {
3094 let summary = <Self as v1::ExplorerApi>::get_explorer_summary(self)
3095 .await
3096 .map_err(to_status)?
3097 .explorer_summary;
3098 Ok(tonic::Response::new((&summary).into()))
3099 }
3100
3101 async fn get_explorer_search(
3102 &self,
3103 request: tonic::Request<proto::GetExplorerSearchRequest>,
3104 ) -> Result<tonic::Response<proto::ExplorerSearchResponse>, tonic::Status> {
3105 let query = required(request.into_inner().query, "query")?;
3106 let results = <Self as v1::ExplorerApi>::get_search_result(self, query)
3107 .await
3108 .map_err(to_status)?
3109 .search_results;
3110 Ok(tonic::Response::new((&results).into()))
3111 }
3112}
3113
3114fn block_ident(height: Option<u64>, hash: Option<String>) -> Result<v1::BlockIdent, tonic::Status> {
3117 match (height, hash) {
3118 (Some(_), Some(_)) => Err(tonic::Status::invalid_argument(
3119 "height and hash are mutually exclusive",
3120 )),
3121 (Some(height), None) => Ok(v1::BlockIdent::Height(height)),
3122 (None, Some(hash)) => Ok(v1::BlockIdent::Hash(hash)),
3123 (None, None) => Ok(v1::BlockIdent::Latest),
3124 }
3125}
3126
3127fn transaction_ident(
3130 height: Option<u64>,
3131 offset: Option<u64>,
3132 hash: Option<String>,
3133) -> Result<v1::TxIdent, tonic::Status> {
3134 match (height, offset, hash) {
3135 (None, None, None) => Ok(v1::TxIdent::Latest),
3136 (None, None, Some(hash)) => Ok(v1::TxIdent::Hash(hash)),
3137 (Some(height), Some(offset), None) => Ok(v1::TxIdent::HeightAndOffset(height, offset)),
3138 (Some(_), None, None) | (None, Some(_), None) => Err(tonic::Status::invalid_argument(
3139 "height and offset must be given together",
3140 )),
3141 _ => Err(tonic::Status::invalid_argument(
3142 "hash is exclusive with height and offset",
3143 )),
3144 }
3145}
3146
3147#[async_trait]
3148impl<D> v1::LightClientApi for NodeApiStateImpl<D>
3149where
3150 D: Deref + Clone + Send + Sync + 'static,
3151 D::Target: AvailabilityDataSource<SeqTypes>
3152 + hotshot_query_service::merklized_state::MerklizedStateDataSource<
3153 SeqTypes,
3154 espresso_types::BlockMerkleTree,
3155 3,
3156 > + hotshot_query_service::merklized_state::MerklizedStateHeightPersistence
3157 + NodeStateDataSource
3158 + StakeTableDataSource<SeqTypes>
3159 + hotshot_query_service::data_source::VersionedDataSource
3160 + Sized
3161 + Clone
3162 + Send
3163 + Sync
3164 + 'static,
3165 for<'a> <D::Target as hotshot_query_service::data_source::VersionedDataSource>::ReadOnly<'a>:
3166 hotshot_query_service::data_source::storage::NodeStorage<SeqTypes>
3167 + hotshot_query_service::data_source::storage::AvailabilityStorage<SeqTypes>,
3168{
3169 async fn get_leaf_proof(
3170 &self,
3171 query: v1::LeafQuery,
3172 finalized: Option<u64>,
3173 ) -> anyhow::Result<light_client::consensus::leaf::LeafProof> {
3174 let ds = &*self.data_source;
3175 let fetch_timeout = FETCH_TIMEOUT;
3176
3177 let requested = match query {
3178 v1::LeafQuery::Height(h) => HsLeafId::Number(h as usize),
3179 v1::LeafQuery::Hash(h) => HsLeafId::Hash(
3180 h.parse()
3181 .map_err(|err| bad_request(format!("invalid leaf hash {h}: {err}")))?,
3182 ),
3183 v1::LeafQuery::BlockHash(h) => {
3184 let parsed = h
3185 .parse()
3186 .map_err(|err| bad_request(format!("invalid block hash {h}: {err}")))?;
3187 let header = AvailabilityDataSource::get_header(ds, HsBlockId::Hash(parsed))
3188 .await
3189 .with_timeout(fetch_timeout)
3190 .await
3191 .ok_or_else(|| not_found(format!("unknown block hash {h}")))?;
3192 HsLeafId::Number(header.height() as usize)
3193 },
3194 v1::LeafQuery::PayloadHash(h) => {
3195 let parsed = h
3196 .parse()
3197 .map_err(|err| bad_request(format!("invalid payload hash {h}: {err}")))?;
3198 let header = AvailabilityDataSource::get_header(ds, HsBlockId::PayloadHash(parsed))
3199 .await
3200 .with_timeout(fetch_timeout)
3201 .await
3202 .ok_or_else(|| not_found(format!("unknown payload hash {h}")))?;
3203 HsLeafId::Number(header.height() as usize)
3204 },
3205 };
3206
3207 let requested_leaf = AvailabilityDataSource::get_leaf(ds, requested)
3208 .await
3209 .with_timeout(fetch_timeout)
3210 .await
3211 .ok_or_else(|| not_found(format!("unknown leaf {requested}")))?;
3212
3213 crate::api::light_client::get_leaf_proof(
3214 ds,
3215 requested_leaf,
3216 finalized.map(|f| f as usize),
3217 fetch_timeout,
3218 lc_leaf_proof_chain_limit(),
3219 )
3220 .await
3221 .map_err(lc_error)
3222 }
3223
3224 async fn get_header_proof(
3225 &self,
3226 root: u64,
3227 requested: v1::HeaderQuery,
3228 ) -> anyhow::Result<light_client::consensus::header::HeaderProof> {
3229 let ds = &*self.data_source;
3230 let fetch_timeout = FETCH_TIMEOUT;
3231 let requested = match requested {
3232 v1::HeaderQuery::Height(h) => HsBlockId::Number(h as usize),
3233 v1::HeaderQuery::Hash(h) => HsBlockId::Hash(
3234 h.parse()
3235 .map_err(|err| bad_request(format!("invalid block hash {h}: {err}")))?,
3236 ),
3237 v1::HeaderQuery::PayloadHash(h) => HsBlockId::PayloadHash(
3238 h.parse()
3239 .map_err(|err| bad_request(format!("invalid payload hash {h}: {err}")))?,
3240 ),
3241 };
3242 crate::api::light_client::get_header_proof(ds, root, requested, fetch_timeout)
3243 .await
3244 .map_err(lc_error)
3245 }
3246
3247 async fn get_light_client_stake_table(
3248 &self,
3249 epoch: u64,
3250 ) -> anyhow::Result<Vec<espresso_types::v0_3::StakeTableEvent>> {
3251 let ds = &*self.data_source;
3252 let fetch_timeout = FETCH_TIMEOUT;
3253
3254 let node_state = NodeStateDataSource::node_state(ds).await;
3255 let epoch_height = node_state
3256 .epoch_height
3257 .ok_or_else(|| anyhow::anyhow!("epoch state not set"))?;
3258 let first_epoch = epoch_from_block_number(node_state.epoch_start_block, epoch_height);
3259 if epoch < first_epoch + 2 {
3260 return Err(bad_request(format!(
3261 "epoch must be at least {}",
3262 first_epoch + 2
3263 )));
3264 }
3265
3266 let epoch_root_height = root_block_in_epoch(epoch - 2, epoch_height) as usize;
3267 let epoch_root = AvailabilityDataSource::get_header::<HsBlockId<SeqTypes>>(
3268 ds,
3269 HsBlockId::Number(epoch_root_height),
3270 )
3271 .await
3272 .with_timeout(fetch_timeout)
3273 .await
3274 .ok_or_else(|| not_found(format!("missing epoch root header {epoch_root_height}")))?;
3275 let to_l1_block = epoch_root
3276 .l1_finalized()
3277 .ok_or_else(|| anyhow::anyhow!("epoch root header is missing L1 finalized block"))?
3278 .number();
3279
3280 let from_l1_block = if epoch >= first_epoch + 3 {
3281 let prev_epoch_root_height = root_block_in_epoch(epoch - 3, epoch_height) as usize;
3282 let prev_epoch_root = AvailabilityDataSource::get_header::<HsBlockId<SeqTypes>>(
3283 ds,
3284 HsBlockId::Number(prev_epoch_root_height),
3285 )
3286 .await
3287 .with_timeout(fetch_timeout)
3288 .await
3289 .ok_or_else(|| {
3290 not_found(format!(
3291 "missing previous epoch root header {prev_epoch_root_height}"
3292 ))
3293 })?;
3294 prev_epoch_root
3295 .l1_finalized()
3296 .ok_or_else(|| {
3297 anyhow::anyhow!("previous epoch root header is missing L1 finalized block")
3298 })?
3299 .number()
3300 + 1
3301 } else {
3302 0
3303 };
3304
3305 StakeTableDataSource::stake_table_events(ds, from_l1_block, to_l1_block).await
3306 }
3307
3308 async fn get_payload_proof(
3309 &self,
3310 height: u64,
3311 ) -> anyhow::Result<light_client::consensus::payload::PayloadProof> {
3312 let ds = &*self.data_source;
3313 let fetch_timeout = FETCH_TIMEOUT;
3314 let height = height as usize;
3315 let payload = AvailabilityDataSource::get_payload(ds, height)
3316 .await
3317 .with_timeout(fetch_timeout)
3318 .await
3319 .ok_or_else(|| not_found(format!("missing payload {height}")))?;
3320 let vid_common = AvailabilityDataSource::get_vid_common(ds, height)
3321 .await
3322 .with_timeout(fetch_timeout)
3323 .await
3324 .ok_or_else(|| not_found(format!("missing VID common {height}")))?;
3325 Ok(light_client::consensus::payload::PayloadProof::new(
3326 payload.data().clone(),
3327 vid_common.common().clone(),
3328 ))
3329 }
3330
3331 async fn get_payload_proof_range(
3332 &self,
3333 start: u64,
3334 end: u64,
3335 ) -> anyhow::Result<Vec<light_client::consensus::payload::PayloadProof>> {
3336 let ds = &*self.data_source;
3337 let fetch_timeout = FETCH_TIMEOUT;
3338 let start = start as usize;
3339 let end = end as usize;
3340 enforce_range(start, end, lc_large_object_range_limit())?;
3341
3342 let payloads_stream = AvailabilityDataSource::get_payload_range(ds, start..end).await;
3343 let vid_stream = AvailabilityDataSource::get_vid_common_range(ds, start..end).await;
3344 let mut out = Vec::new();
3345 let mut payloads = payloads_stream.enumerate();
3346 let mut vid_commons = vid_stream.enumerate();
3347 loop {
3348 let (next_payload, next_vid) =
3349 futures::future::join(payloads.next(), vid_commons.next()).await;
3350 let (Some((i, payload_fut)), Some((_, vid_fut))) = (next_payload, next_vid) else {
3351 break;
3352 };
3353 let payload = payload_fut
3354 .with_timeout(fetch_timeout)
3355 .await
3356 .ok_or_else(|| not_found(format!("missing payload {}", start + i)))?;
3357 let vid_common = vid_fut
3358 .with_timeout(fetch_timeout)
3359 .await
3360 .ok_or_else(|| not_found(format!("missing VID common {}", start + i)))?;
3361 out.push(light_client::consensus::payload::PayloadProof::new(
3362 payload.data().clone(),
3363 vid_common.common().clone(),
3364 ));
3365 }
3366 Ok(out)
3367 }
3368
3369 async fn get_payload_proof_ranges(
3370 &self,
3371 ranges: Vec<Range<u64>>,
3372 ) -> anyhow::Result<Vec<light_client::consensus::payload::PayloadProof>> {
3373 let ranges = validate_ranges(ranges, lc_large_object_range_limit())?;
3374
3375 let heights: u64 = ranges.iter().map(|range| range.end - range.start).sum();
3376 let read = async {
3377 let mut tx = self.data_source.read().await.ok()?;
3378 let blocks = tx.get_block_ranges(&ranges).await.ok()?;
3379 let vid_common = tx.get_vid_common_ranges(&ranges).await.ok()?;
3380 (blocks.len() as u64 == heights && vid_common.len() as u64 == heights)
3381 .then_some((blocks, vid_common))
3382 }
3383 .await;
3384 if let Some((blocks, vid_common)) = read {
3385 let mut vid_common: HashMap<u64, _> = vid_common
3388 .into_iter()
3389 .map(|common| (common.height(), common))
3390 .collect();
3391 return blocks
3392 .into_iter()
3393 .map(|block| {
3394 let common = vid_common.remove(&block.height()).ok_or_else(|| {
3395 not_found(format!("VID common {} not found", block.height()))
3396 })?;
3397 Ok(light_client::consensus::payload::PayloadProof::new(
3398 block.payload().clone(),
3399 common.common().clone(),
3400 ))
3401 })
3402 .collect();
3403 }
3404
3405 let ranges: Vec<_> = futures::stream::iter(ranges)
3406 .map(|range| self.get_payload_proof_range(range.start, range.end))
3407 .buffered(self.ranges_concurrency.get())
3408 .try_collect()
3409 .await?;
3410 Ok(ranges.into_iter().flatten().collect())
3411 }
3412
3413 async fn get_lc_namespace_proof(
3414 &self,
3415 height: u64,
3416 namespace: u64,
3417 ) -> anyhow::Result<light_client::consensus::namespace::NamespaceProof> {
3418 let ds = &*self.data_source;
3419 let fetch_timeout = FETCH_TIMEOUT;
3420 let end = height
3421 .checked_add(1)
3422 .ok_or_else(|| not_found(format!("no block at height {height}")))?;
3423 let mut proofs = crate::api::light_client::get_namespace_proof_range(
3424 ds,
3425 height as usize,
3426 end as usize,
3427 namespace,
3428 fetch_timeout,
3429 lc_large_object_range_limit(),
3430 )
3431 .await
3432 .map_err(lc_error)?;
3433 if proofs.len() != 1 {
3434 return Err(anyhow::anyhow!("internal consistency error"));
3435 }
3436 Ok(proofs.remove(0))
3437 }
3438
3439 async fn get_lc_namespace_proof_range(
3440 &self,
3441 start: u64,
3442 end: u64,
3443 namespace: u64,
3444 ) -> anyhow::Result<Vec<light_client::consensus::namespace::NamespaceProof>> {
3445 let ds = &*self.data_source;
3446 let fetch_timeout = FETCH_TIMEOUT;
3447 crate::api::light_client::get_namespace_proof_range(
3448 ds,
3449 start as usize,
3450 end as usize,
3451 namespace,
3452 fetch_timeout,
3453 lc_large_object_range_limit(),
3454 )
3455 .await
3456 .map_err(lc_error)
3457 }
3458
3459 async fn get_lc_namespaces_proof_range(
3460 &self,
3461 start: u64,
3462 end: u64,
3463 namespaces: String,
3464 ) -> anyhow::Result<
3465 Vec<std::collections::HashMap<u64, light_client::consensus::namespace::NamespaceProof>>,
3466 > {
3467 let namespaces = crate::api::light_client::parse_namespaces_str(&namespaces)
3468 .map_err(|err| bad_request(err.to_string()))?;
3469 let ds = &*self.data_source;
3470 let fetch_timeout = FETCH_TIMEOUT;
3471 crate::api::light_client::get_namespaces_proof_range(
3472 ds,
3473 start as usize,
3474 end as usize,
3475 &namespaces,
3476 fetch_timeout,
3477 lc_large_object_range_limit(),
3478 )
3479 .await
3480 .map_err(lc_error)
3481 }
3482}
3483
3484#[tonic::async_trait]
3485impl<D> proto::light_client_service_server::LightClientService for NodeApiStateImpl<D>
3486where
3487 D: Deref + Clone + Send + Sync + 'static,
3488 D::Target: AvailabilityDataSource<SeqTypes>
3489 + hotshot_query_service::merklized_state::MerklizedStateDataSource<
3490 SeqTypes,
3491 espresso_types::BlockMerkleTree,
3492 3,
3493 > + hotshot_query_service::merklized_state::MerklizedStateHeightPersistence
3494 + NodeStateDataSource
3495 + StakeTableDataSource<SeqTypes>
3496 + hotshot_query_service::data_source::VersionedDataSource
3497 + Sized
3498 + Clone
3499 + Send
3500 + Sync
3501 + 'static,
3502 for<'a> <D::Target as hotshot_query_service::data_source::VersionedDataSource>::ReadOnly<'a>:
3503 hotshot_query_service::data_source::storage::NodeStorage<SeqTypes>
3504 + hotshot_query_service::data_source::storage::AvailabilityStorage<SeqTypes>,
3505{
3506 async fn get_light_client_leaf_proof(
3507 &self,
3508 request: tonic::Request<proto::GetLightClientLeafProofRequest>,
3509 ) -> Result<tonic::Response<proto::LightClientLeafProofResponse>, tonic::Status> {
3510 let proto::GetLightClientLeafProofRequest {
3511 height,
3512 hash,
3513 block_hash,
3514 payload_hash,
3515 finalized,
3516 } = request.into_inner();
3517 let query = leaf_query(height, hash, block_hash, payload_hash)?;
3518 let proof = <Self as v1::LightClientApi>::get_leaf_proof(self, query, finalized)
3519 .await
3520 .map_err(to_status)?;
3521 Ok(tonic::Response::new((&proof).into()))
3522 }
3523
3524 async fn get_light_client_header_proof(
3525 &self,
3526 request: tonic::Request<proto::GetLightClientHeaderProofRequest>,
3527 ) -> Result<tonic::Response<proto::LightClientHeaderProofResponse>, tonic::Status> {
3528 let proto::GetLightClientHeaderProofRequest {
3529 root,
3530 height,
3531 block_hash,
3532 payload_hash,
3533 } = request.into_inner();
3534 let root = required(root, "root")?;
3535 let query = header_query(height, block_hash, payload_hash)?;
3536 let proof = <Self as v1::LightClientApi>::get_header_proof(self, root, query)
3537 .await
3538 .map_err(to_status)?;
3539 Ok(tonic::Response::new(
3540 proto::LightClientHeaderProofResponse {
3541 header: Some(proof.header().into()),
3542 proof: Some(proof.proof().into()),
3543 },
3544 ))
3545 }
3546
3547 async fn get_light_client_stake_table(
3548 &self,
3549 request: tonic::Request<proto::GetLightClientStakeTableRequest>,
3550 ) -> Result<tonic::Response<proto::LightClientStakeTableResponse>, tonic::Status> {
3551 let epoch = required(request.into_inner().epoch, "epoch")?;
3552 let events = <Self as v1::LightClientApi>::get_light_client_stake_table(self, epoch)
3553 .await
3554 .map_err(to_status)?;
3555 Ok(tonic::Response::new(proto::LightClientStakeTableResponse {
3556 events: events.iter().map(Into::into).collect(),
3557 }))
3558 }
3559
3560 async fn get_light_client_payload_proof(
3561 &self,
3562 request: tonic::Request<proto::GetLightClientPayloadProofRequest>,
3563 ) -> Result<tonic::Response<proto::LightClientPayloadProofResponse>, tonic::Status> {
3564 let height = required(request.into_inner().height, "height")?;
3565 let proof = <Self as v1::LightClientApi>::get_payload_proof(self, height)
3566 .await
3567 .map_err(to_status)?;
3568 Ok(tonic::Response::new((&proof).try_into()?))
3569 }
3570
3571 async fn get_light_client_payload_proof_range(
3572 &self,
3573 request: tonic::Request<proto::GetLightClientPayloadProofRangeRequest>,
3574 ) -> Result<tonic::Response<proto::LightClientPayloadProofRangeResponse>, tonic::Status> {
3575 let request = request.into_inner();
3576 let range = range_from_query(request.from, request.until)?;
3577 let proofs = <Self as v1::LightClientApi>::get_payload_proof_ranges(self, vec![range])
3578 .await
3579 .map_err(to_status)?;
3580 Ok(tonic::Response::new(
3581 proto::LightClientPayloadProofRangeResponse {
3582 proofs: proofs
3583 .iter()
3584 .map(TryInto::try_into)
3585 .collect::<Result<_, _>>()?,
3586 },
3587 ))
3588 }
3589
3590 async fn get_light_client_payload_proof_ranges(
3591 &self,
3592 request: tonic::Request<proto::GetLightClientPayloadProofRangesRequest>,
3593 ) -> Result<tonic::Response<proto::LightClientPayloadProofRangeResponse>, tonic::Status> {
3594 let ranges = ranges_from_body(request.into_inner().ranges)?;
3595 let proofs = <Self as v1::LightClientApi>::get_payload_proof_ranges(self, ranges)
3596 .await
3597 .map_err(to_status)?;
3598 Ok(tonic::Response::new(
3599 proto::LightClientPayloadProofRangeResponse {
3600 proofs: proofs
3601 .iter()
3602 .map(TryInto::try_into)
3603 .collect::<Result<_, _>>()?,
3604 },
3605 ))
3606 }
3607
3608 async fn get_light_client_namespace_proof(
3609 &self,
3610 request: tonic::Request<proto::GetLightClientNamespaceProofRequest>,
3611 ) -> Result<tonic::Response<proto::LightClientNamespaceProofResponse>, tonic::Status> {
3612 let request = request.into_inner();
3613 let height = required(request.height, "height")?;
3614 let namespace = required(namespace_from_query(request.namespace)?, "namespace")?.into();
3615 let proof = <Self as v1::LightClientApi>::get_lc_namespace_proof(self, height, namespace)
3616 .await
3617 .map_err(to_status)?;
3618 Ok(tonic::Response::new((&proof).try_into()?))
3619 }
3620
3621 async fn get_light_client_namespace_proof_range(
3622 &self,
3623 request: tonic::Request<proto::GetLightClientNamespaceProofRangeRequest>,
3624 ) -> Result<tonic::Response<proto::LightClientNamespaceProofRangeResponse>, tonic::Status> {
3625 let request = request.into_inner();
3626 let range = range_from_query(request.from, request.until)?;
3627 let namespace = required(namespace_from_query(request.namespace)?, "namespace")?.into();
3628 let proofs = <Self as v1::LightClientApi>::get_lc_namespace_proof_range(
3629 self,
3630 range.start,
3631 range.end,
3632 namespace,
3633 )
3634 .await
3635 .map_err(to_status)?;
3636 Ok(tonic::Response::new(
3637 proto::LightClientNamespaceProofRangeResponse {
3638 proofs: proofs
3639 .iter()
3640 .map(TryInto::try_into)
3641 .collect::<Result<_, _>>()?,
3642 },
3643 ))
3644 }
3645
3646 async fn get_light_client_namespaces_proof_range(
3647 &self,
3648 request: tonic::Request<proto::GetLightClientNamespacesProofRangeRequest>,
3649 ) -> Result<tonic::Response<proto::LightClientNamespacesProofRangeResponse>, tonic::Status>
3650 {
3651 let request = request.into_inner();
3652 let range = range_from_query(request.from, request.until)?;
3653 let namespaces = request
3654 .namespaces
3655 .into_iter()
3656 .map(|namespace| namespace_id(namespace).map(u64::from))
3657 .collect::<Result<BTreeSet<_>, _>>()?;
3658 if namespaces.len() > MAX_NAMESPACES_PER_REQUEST {
3659 return Err(tonic::Status::invalid_argument(format!(
3660 "at most {MAX_NAMESPACES_PER_REQUEST} distinct namespaces per request"
3661 )));
3662 }
3663 let namespaces: Vec<u64> = namespaces.into_iter().collect();
3664 let blocks = crate::api::light_client::get_namespaces_proof_range(
3667 &*self.data_source,
3668 range.start as usize,
3669 range.end as usize,
3670 &namespaces,
3671 FETCH_TIMEOUT,
3672 lc_large_object_range_limit(),
3673 )
3674 .await
3675 .map_err(|err| to_status(lc_error(err)))?;
3676 let blocks = blocks
3677 .iter()
3678 .map(|block| {
3679 Ok(proto::LightClientNamespacesProof {
3680 proofs: block
3681 .iter()
3682 .filter_map(|(namespace, proof)| {
3683 let contents = proof.contents()?.try_into();
3684 Some(contents.map(|contents| (*namespace, contents)))
3685 })
3686 .collect::<Result<_, tonic::Status>>()?,
3687 })
3688 })
3689 .collect::<Result<_, tonic::Status>>()?;
3690 Ok(tonic::Response::new(
3691 proto::LightClientNamespacesProofRangeResponse { blocks },
3692 ))
3693 }
3694}
3695
3696fn leaf_query(
3697 height: Option<u64>,
3698 hash: Option<String>,
3699 block_hash: Option<String>,
3700 payload_hash: Option<String>,
3701) -> Result<v1::LeafQuery, tonic::Status> {
3702 match (height, hash, block_hash, payload_hash) {
3703 (Some(height), None, None, None) => Ok(v1::LeafQuery::Height(height)),
3704 (None, Some(hash), None, None) => Ok(v1::LeafQuery::Hash(hash)),
3705 (None, None, Some(block_hash), None) => Ok(v1::LeafQuery::BlockHash(block_hash)),
3706 (None, None, None, Some(payload_hash)) => Ok(v1::LeafQuery::PayloadHash(payload_hash)),
3707 _ => Err(tonic::Status::invalid_argument(
3708 "set exactly one of height, hash, block_hash or payload_hash",
3709 )),
3710 }
3711}
3712
3713fn header_query(
3714 height: Option<u64>,
3715 block_hash: Option<String>,
3716 payload_hash: Option<String>,
3717) -> Result<v1::HeaderQuery, tonic::Status> {
3718 match (height, block_hash, payload_hash) {
3719 (Some(height), None, None) => Ok(v1::HeaderQuery::Height(height)),
3720 (None, Some(block_hash), None) => Ok(v1::HeaderQuery::Hash(block_hash)),
3721 (None, None, Some(payload_hash)) => Ok(v1::HeaderQuery::PayloadHash(payload_hash)),
3722 _ => Err(tonic::Status::invalid_argument(
3723 "set exactly one of height, block_hash or payload_hash",
3724 )),
3725 }
3726}
3727
3728fn lc_large_object_range_limit() -> usize {
3729 hotshot_query_service::availability::Options::default().large_object_range_limit
3730}
3731
3732pub(crate) fn lc_error(err: hotshot_query_service::Error) -> anyhow::Error {
3735 match err.status() {
3736 StatusCode::NOT_FOUND => not_found(err.to_string()),
3737 StatusCode::GONE => AvailabilityError::Pruned(err.to_string()).into(),
3738 StatusCode::BAD_REQUEST => bad_request(err.to_string()),
3739 _ => anyhow::anyhow!("{err}"),
3740 }
3741}
3742
3743fn explorer_error(err: ExplorerError) -> anyhow::Error {
3745 lc_error(hotshot_query_service::Error::Explorer { source: err })
3746}
3747
3748fn state_cert_error(err: StateCertFetchError) -> anyhow::Error {
3749 match err {
3750 StateCertFetchError::EpochsNotStarted { .. }
3751 | StateCertFetchError::BeforeFirstEpoch { .. }
3752 | StateCertFetchError::RootNotDecided { .. } => not_found(err.to_string()),
3753 StateCertFetchError::FetchError(_)
3754 | StateCertFetchError::ValidationError(_)
3755 | StateCertFetchError::Other(_) => err.into(),
3756 }
3757}
3758
3759fn lc_leaf_proof_chain_limit() -> usize {
3764 hotshot_query_service::availability::Options::default().small_object_range_limit
3765}
3766
3767#[async_trait]
3768impl<D> v1::TokenApi for NodeApiStateImpl<D>
3769where
3770 D: Deref + Clone + Send + Sync + 'static,
3771 D::Target: TokenDataSource<SeqTypes> + NodeStateDataSource + Send + Sync,
3772{
3773 async fn total_minted_supply(&self) -> anyhow::Result<String> {
3774 let ds = &*self.data_source;
3775 let value = ds
3776 .get_total_supply_l1()
3777 .await
3778 .map_err(|err| anyhow::anyhow!("failed to get total supply: {err:#}"))?;
3779 Ok(format_ether(value))
3780 }
3781
3782 async fn circulating_supply(&self) -> anyhow::Result<String> {
3783 let calc = fetch_supply_inputs(&*self.data_source).await?;
3784 Ok(format_ether(calc.circulating_supply()))
3785 }
3786
3787 async fn circulating_supply_ethereum(&self) -> anyhow::Result<String> {
3788 let calc = fetch_supply_inputs(&*self.data_source).await?;
3789 Ok(format_ether(calc.circulating_supply_ethereum()))
3790 }
3791
3792 async fn total_issued_supply(&self) -> anyhow::Result<String> {
3793 let calc = fetch_supply_inputs(&*self.data_source).await?;
3794 Ok(format_ether(calc.total_issued_supply()))
3795 }
3796
3797 async fn total_reward_distributed(&self) -> anyhow::Result<String> {
3798 let calc = fetch_supply_inputs(&*self.data_source).await?;
3799 Ok(format_ether(calc.total_reward_distributed()))
3800 }
3801}
3802
3803async fn fetch_supply_inputs<S>(
3804 ds: &S,
3805) -> anyhow::Result<crate::api::unlock_schedule::SupplyCalculator>
3806where
3807 S: TokenDataSource<SeqTypes> + NodeStateDataSource + Sync + ?Sized,
3808{
3809 let node_state = ds.node_state().await;
3810 let chain_id = node_state.chain_config.chain_id;
3811
3812 let header = ds.get_decided_header().await;
3813 let now_secs = header.timestamp_internal();
3814 let total_reward_distributed = header.total_reward_distributed();
3815
3816 let initial_supply = ds
3817 .get_initial_supply_l1()
3818 .await
3819 .map_err(|err| anyhow::anyhow!("failed to get initial supply: {err:#}"))?;
3820
3821 let total_supply_l1 = ds
3822 .get_total_supply_l1()
3823 .await
3824 .map_err(|err| anyhow::anyhow!("failed to get total supply: {err:#}"))?;
3825
3826 Ok(crate::api::unlock_schedule::SupplyCalculator::new(
3827 chain_id,
3828 now_secs,
3829 initial_supply,
3830 total_supply_l1,
3831 total_reward_distributed,
3832 ))
3833}
3834
3835#[tonic::async_trait]
3836impl<D> proto::token_service_server::TokenService for NodeApiStateImpl<D>
3837where
3838 D: Deref + Clone + Send + Sync + 'static,
3839 D::Target: TokenDataSource<SeqTypes> + NodeStateDataSource + Send + Sync,
3840{
3841 async fn get_total_minted_supply(
3842 &self,
3843 _request: tonic::Request<proto::GetTotalMintedSupplyRequest>,
3844 ) -> Result<tonic::Response<proto::TotalMintedSupplyResponse>, tonic::Status> {
3845 let amount = <Self as v1::TokenApi>::total_minted_supply(self)
3846 .await
3847 .map_err(to_status)?;
3848 Ok(tonic::Response::new(proto::TotalMintedSupplyResponse {
3849 amount,
3850 }))
3851 }
3852
3853 async fn get_circulating_supply(
3854 &self,
3855 _request: tonic::Request<proto::GetCirculatingSupplyRequest>,
3856 ) -> Result<tonic::Response<proto::CirculatingSupplyResponse>, tonic::Status> {
3857 let amount = <Self as v1::TokenApi>::circulating_supply(self)
3858 .await
3859 .map_err(to_status)?;
3860 Ok(tonic::Response::new(proto::CirculatingSupplyResponse {
3861 amount,
3862 }))
3863 }
3864
3865 async fn get_circulating_supply_ethereum(
3866 &self,
3867 _request: tonic::Request<proto::GetCirculatingSupplyEthereumRequest>,
3868 ) -> Result<tonic::Response<proto::CirculatingSupplyEthereumResponse>, tonic::Status> {
3869 let amount = <Self as v1::TokenApi>::circulating_supply_ethereum(self)
3870 .await
3871 .map_err(to_status)?;
3872 Ok(tonic::Response::new(
3873 proto::CirculatingSupplyEthereumResponse { amount },
3874 ))
3875 }
3876
3877 async fn get_total_issued_supply(
3878 &self,
3879 _request: tonic::Request<proto::GetTotalIssuedSupplyRequest>,
3880 ) -> Result<tonic::Response<proto::TotalIssuedSupplyResponse>, tonic::Status> {
3881 let amount = <Self as v1::TokenApi>::total_issued_supply(self)
3882 .await
3883 .map_err(to_status)?;
3884 Ok(tonic::Response::new(proto::TotalIssuedSupplyResponse {
3885 amount,
3886 }))
3887 }
3888
3889 async fn get_total_reward_distributed(
3890 &self,
3891 _request: tonic::Request<proto::GetTotalRewardDistributedRequest>,
3892 ) -> Result<tonic::Response<proto::TotalRewardDistributedResponse>, tonic::Status> {
3893 let amount = <Self as v1::TokenApi>::total_reward_distributed(self)
3894 .await
3895 .map_err(to_status)?;
3896 Ok(tonic::Response::new(
3897 proto::TotalRewardDistributedResponse { amount },
3898 ))
3899 }
3900}
3901
3902#[async_trait]
3903impl<D> v1::DatabaseApi for NodeApiStateImpl<D>
3904where
3905 D: Deref + Clone + Send + Sync + 'static,
3906 D::Target: DatabaseMetadataSource + Send + Sync,
3907{
3908 async fn get_table_sizes(&self) -> anyhow::Result<Vec<TableSize>> {
3909 let ds = &*self.data_source;
3910 ds.get_table_sizes().await
3911 }
3912
3913 async fn get_migration_status(&self) -> anyhow::Result<Vec<MigrationStatus>> {
3914 let ds = &*self.data_source;
3915 ds.get_migration_status().await
3916 }
3917}
3918
3919#[tonic::async_trait]
3920impl<D> proto::database_service_server::DatabaseService for NodeApiStateImpl<D>
3921where
3922 D: Deref + Clone + Send + Sync + 'static,
3923 D::Target: DatabaseMetadataSource + Send + Sync,
3924{
3925 async fn get_table_sizes(
3926 &self,
3927 _request: tonic::Request<proto::GetTableSizesRequest>,
3928 ) -> Result<tonic::Response<proto::TableSizesResponse>, tonic::Status> {
3929 let tables = <Self as v1::DatabaseApi>::get_table_sizes(self)
3930 .await
3931 .map_err(to_status)?
3932 .into_iter()
3933 .map(|table| proto::TableSize {
3934 table_name: table.table_name,
3935 row_count: table.row_count,
3936 total_size_bytes: table.total_size_bytes,
3937 })
3938 .collect();
3939 Ok(tonic::Response::new(proto::TableSizesResponse { tables }))
3940 }
3941
3942 async fn get_migration_status(
3943 &self,
3944 _request: tonic::Request<proto::GetMigrationStatusRequest>,
3945 ) -> Result<tonic::Response<proto::MigrationStatusResponse>, tonic::Status> {
3946 let migrations = <Self as v1::DatabaseApi>::get_migration_status(self)
3947 .await
3948 .map_err(to_status)?
3949 .into_iter()
3950 .map(|migration| proto::MigrationStatus {
3951 name: migration.name,
3952 started_at: migration
3955 .started_at
3956 .to_rfc3339_opts(SecondsFormat::AutoSi, true),
3957 completed_at: migration
3958 .completed_at
3959 .map(|time| time.to_rfc3339_opts(SecondsFormat::AutoSi, true)),
3960 last_offset: migration.last_offset,
3961 })
3962 .collect();
3963 Ok(tonic::Response::new(proto::MigrationStatusResponse {
3964 migrations,
3965 }))
3966 }
3967}
3968
3969fn block_id_from_query(
3970 height: Option<u64>,
3971 hash: Option<String>,
3972 payload_hash: Option<String>,
3973) -> Result<v1::availability::BlockId, tonic::Status> {
3974 match (height, hash, payload_hash) {
3975 (Some(height), None, None) => Ok(v1::availability::BlockId::Height(height)),
3976 (None, Some(hash), None) => Ok(v1::availability::BlockId::Hash(hash)),
3977 (None, None, Some(payload_hash)) => {
3978 Ok(v1::availability::BlockId::PayloadHash(payload_hash))
3979 },
3980 _ => Err(tonic::Status::invalid_argument(
3981 "set exactly one of height, hash or payload_hash",
3982 )),
3983 }
3984}
3985
3986fn namespace_from_query(namespace: Option<u64>) -> Result<Option<u32>, tonic::Status> {
3987 namespace.map(namespace_id).transpose()
3988}
3989
3990fn namespace_id(namespace: u64) -> Result<u32, tonic::Status> {
3993 u32::try_from(namespace)
3994 .map_err(|_| tonic::Status::invalid_argument("namespace does not fit in 32 bits"))
3995}
3996
3997fn required<T>(value: Option<T>, name: &str) -> Result<T, tonic::Status> {
3998 value.ok_or_else(|| tonic::Status::invalid_argument(format!("{name} is required")))
3999}
4000
4001fn range_from_query(from: Option<u64>, until: Option<u64>) -> Result<Range<u64>, tonic::Status> {
4002 Ok(required(from, "from")?..required(until, "until")?)
4003}
4004
4005fn end_at_first_error<T, S>(items: S) -> BoxStream<'static, Result<T, tonic::Status>>
4008where
4009 T: Send + 'static,
4010 S: futures::Stream<Item = Result<T, tonic::Status>> + Send + 'static,
4011{
4012 items
4013 .scan(false, |failed, item| {
4014 if *failed {
4015 return futures::future::ready(None);
4016 }
4017 *failed = item.is_err();
4018 futures::future::ready(Some(item))
4019 })
4020 .boxed()
4021}
4022
4023fn ranges_from_body(ranges: Vec<proto::HeightRange>) -> Result<Vec<Range<u64>>, tonic::Status> {
4024 ranges
4025 .into_iter()
4026 .map(|range| range_from_query(range.from, range.until))
4027 .collect()
4028}
4029
4030#[tonic::async_trait]
4031impl<D> proto::availability_service_server::AvailabilityService for NodeApiStateImpl<D>
4032where
4033 D: Deref + Clone + Send + Sync + 'static,
4034 D::Target: AvailabilityDataSource<SeqTypes>
4036 + hotshot_query_service::data_source::VersionedDataSource
4037 + hotshot_query_service::node::NodeDataSource<SeqTypes>
4038 + RequestResponseDataSource<SeqTypes>
4039 + StateCertDataSource
4040 + StateCertFetchingDataSource<SeqTypes>
4041 + Send
4042 + Sync,
4043 for<'a> <D::Target as hotshot_query_service::data_source::VersionedDataSource>::ReadOnly<'a>:
4044 hotshot_query_service::data_source::storage::AvailabilityStorage<SeqTypes>,
4045{
4046 async fn get_limits(
4047 &self,
4048 _request: tonic::Request<proto::GetLimitsRequest>,
4049 ) -> Result<tonic::Response<proto::LimitsResponse>, tonic::Status> {
4050 let limits = <Self as v1::HotShotAvailabilityApi>::get_limits(self)
4051 .await
4052 .map_err(to_status)?;
4053 Ok(tonic::Response::new(proto::LimitsResponse {
4054 small_object_range_limit: limits.small_object_range_limit as u64,
4055 large_object_range_limit: limits.large_object_range_limit as u64,
4056 namespace_proof_range_limit: NAMESPACE_PROOF_RANGE_LIMIT,
4057 }))
4058 }
4059
4060 async fn get_header(
4061 &self,
4062 request: tonic::Request<proto::GetHeaderRequest>,
4063 ) -> Result<tonic::Response<proto::HeaderResponse>, tonic::Status> {
4064 let request = request.into_inner();
4065 let id = block_id_from_query(request.height, request.hash, request.payload_hash)?;
4066 let header = <Self as v1::HotShotAvailabilityApi>::get_header(self, id)
4067 .await
4068 .map_err(to_status)?;
4069 Ok(tonic::Response::new(proto::HeaderResponse::from(&header)))
4070 }
4071
4072 async fn get_header_range(
4073 &self,
4074 request: tonic::Request<proto::GetHeaderRangeRequest>,
4075 ) -> Result<tonic::Response<proto::HeaderRangeResponse>, tonic::Status> {
4076 let request = request.into_inner();
4077 let range = range_from_query(request.from, request.until)?;
4078 let headers = <Self as v1::HotShotAvailabilityApi>::get_header_range(
4079 self,
4080 range.start as usize,
4081 range.end as usize,
4082 )
4083 .await
4084 .map_err(to_status)?;
4085 Ok(tonic::Response::new(proto::HeaderRangeResponse::from(
4086 &*headers,
4087 )))
4088 }
4089
4090 async fn get_leaf(
4091 &self,
4092 request: tonic::Request<proto::GetLeafRequest>,
4093 ) -> Result<tonic::Response<proto::LeafResponse>, tonic::Status> {
4094 let request = request.into_inner();
4095 let id = match (request.height, request.hash) {
4096 (Some(height), None) => v1::availability::LeafId::Height(height),
4097 (None, Some(hash)) => v1::availability::LeafId::Hash(hash),
4098 _ => {
4099 return Err(tonic::Status::invalid_argument(
4100 "set exactly one of height or hash",
4101 ));
4102 },
4103 };
4104 let leaf = <Self as v1::HotShotAvailabilityApi>::get_leaf(self, id)
4105 .await
4106 .map_err(to_status)?;
4107 Ok(tonic::Response::new(proto::LeafResponse::from(&leaf)))
4108 }
4109
4110 async fn get_leaf_range(
4111 &self,
4112 request: tonic::Request<proto::GetLeafRangeRequest>,
4113 ) -> Result<tonic::Response<proto::LeafRangeResponse>, tonic::Status> {
4114 let request = request.into_inner();
4115 let range = range_from_query(request.from, request.until)?;
4116 let leaves = <Self as v1::HotShotAvailabilityApi>::get_leaf_range(
4117 self,
4118 range.start as usize,
4119 range.end as usize,
4120 )
4121 .await
4122 .map_err(to_status)?;
4123 Ok(tonic::Response::new(proto::LeafRangeResponse::from(
4124 &*leaves,
4125 )))
4126 }
4127
4128 async fn get_leaf_ranges(
4129 &self,
4130 request: tonic::Request<proto::GetLeafRangesRequest>,
4131 ) -> Result<tonic::Response<proto::LeafRangeResponse>, tonic::Status> {
4132 let ranges = ranges_from_body(request.into_inner().ranges)?;
4133 let leaves = <Self as v1::HotShotAvailabilityApi>::get_leaf_ranges(self, ranges)
4134 .await
4135 .map_err(to_status)?;
4136 Ok(tonic::Response::new(proto::LeafRangeResponse::from(
4137 &*leaves,
4138 )))
4139 }
4140
4141 async fn get_cert2(
4142 &self,
4143 request: tonic::Request<proto::GetCert2Request>,
4144 ) -> Result<tonic::Response<proto::Cert2Response>, tonic::Status> {
4145 let height = required(request.into_inner().height, "height")?;
4146 let cert2 = <Self as v1::HotShotAvailabilityApi>::get_cert2(self, height)
4147 .await
4148 .map_err(to_status)?
4149 .ok_or_else(|| {
4150 tonic::Status::not_found(format!("no cert2 available for height {height}"))
4151 })?;
4152 Ok(tonic::Response::new(proto::Cert2Response {
4153 certificate: Some((&cert2).into()),
4154 }))
4155 }
4156
4157 async fn get_block(
4158 &self,
4159 request: tonic::Request<proto::GetBlockRequest>,
4160 ) -> Result<tonic::Response<proto::BlockResponse>, tonic::Status> {
4161 let request = request.into_inner();
4162 let id = block_id_from_query(request.height, request.hash, request.payload_hash)?;
4163 let block = <Self as v1::HotShotAvailabilityApi>::get_block(self, id)
4164 .await
4165 .map_err(to_status)?;
4166 Ok(tonic::Response::new(proto::BlockResponse::from(&block)))
4167 }
4168
4169 async fn get_block_range(
4170 &self,
4171 request: tonic::Request<proto::GetBlockRangeRequest>,
4172 ) -> Result<tonic::Response<proto::BlockRangeResponse>, tonic::Status> {
4173 let request = request.into_inner();
4174 let range = range_from_query(request.from, request.until)?;
4175 let blocks = <Self as v1::HotShotAvailabilityApi>::get_block_range(
4176 self,
4177 range.start as usize,
4178 range.end as usize,
4179 )
4180 .await
4181 .map_err(to_status)?;
4182 Ok(tonic::Response::new(proto::BlockRangeResponse::from(
4183 &*blocks,
4184 )))
4185 }
4186
4187 async fn get_block_ranges(
4188 &self,
4189 request: tonic::Request<proto::GetBlockRangesRequest>,
4190 ) -> Result<tonic::Response<proto::BlockRangeResponse>, tonic::Status> {
4191 let ranges = ranges_from_body(request.into_inner().ranges)?;
4192 let blocks = <Self as v1::HotShotAvailabilityApi>::get_block_ranges(self, ranges)
4193 .await
4194 .map_err(to_status)?;
4195 Ok(tonic::Response::new(proto::BlockRangeResponse::from(
4196 &*blocks,
4197 )))
4198 }
4199
4200 async fn get_payload(
4201 &self,
4202 request: tonic::Request<proto::GetPayloadRequest>,
4203 ) -> Result<tonic::Response<proto::PayloadResponse>, tonic::Status> {
4204 let request = request.into_inner();
4205 let id = match (request.height, request.hash, request.block_hash) {
4206 (Some(height), None, None) => v1::availability::PayloadId::Height(height),
4207 (None, Some(hash), None) => v1::availability::PayloadId::Hash(hash),
4208 (None, None, Some(block_hash)) => v1::availability::PayloadId::BlockHash(block_hash),
4209 _ => {
4210 return Err(tonic::Status::invalid_argument(
4211 "set exactly one of height, hash or block_hash",
4212 ));
4213 },
4214 };
4215 let payload = <Self as v1::HotShotAvailabilityApi>::get_payload(self, id)
4216 .await
4217 .map_err(to_status)?;
4218 Ok(tonic::Response::new(proto::PayloadResponse::from(&payload)))
4219 }
4220
4221 async fn get_payload_range(
4222 &self,
4223 request: tonic::Request<proto::GetPayloadRangeRequest>,
4224 ) -> Result<tonic::Response<proto::PayloadRangeResponse>, tonic::Status> {
4225 let request = request.into_inner();
4226 let range = range_from_query(request.from, request.until)?;
4227 let payloads = <Self as v1::HotShotAvailabilityApi>::get_payload_range(
4228 self,
4229 range.start as usize,
4230 range.end as usize,
4231 )
4232 .await
4233 .map_err(to_status)?;
4234 Ok(tonic::Response::new(proto::PayloadRangeResponse::from(
4235 &*payloads,
4236 )))
4237 }
4238
4239 async fn get_vid_common(
4240 &self,
4241 request: tonic::Request<proto::GetVidCommonRequest>,
4242 ) -> Result<tonic::Response<proto::VidCommonResponse>, tonic::Status> {
4243 let request = request.into_inner();
4244 let id = block_id_from_query(request.height, request.hash, request.payload_hash)?;
4245 let common = <Self as v1::HotShotAvailabilityApi>::get_vid_common(self, id)
4246 .await
4247 .map_err(to_status)?;
4248 Ok(tonic::Response::new(proto::VidCommonResponse::try_from(
4249 &common,
4250 )?))
4251 }
4252
4253 async fn get_vid_common_range(
4254 &self,
4255 request: tonic::Request<proto::GetVidCommonRangeRequest>,
4256 ) -> Result<tonic::Response<proto::VidCommonRangeResponse>, tonic::Status> {
4257 let request = request.into_inner();
4258 let range = range_from_query(request.from, request.until)?;
4259 let items = <Self as v1::HotShotAvailabilityApi>::get_vid_common_range(
4260 self,
4261 range.start as usize,
4262 range.end as usize,
4263 )
4264 .await
4265 .map_err(to_status)?;
4266 Ok(tonic::Response::new(
4267 proto::VidCommonRangeResponse::try_from(&*items)?,
4268 ))
4269 }
4270
4271 async fn get_vid_common_ranges(
4272 &self,
4273 request: tonic::Request<proto::GetVidCommonRangesRequest>,
4274 ) -> Result<tonic::Response<proto::VidCommonRangeResponse>, tonic::Status> {
4275 let ranges = ranges_from_body(request.into_inner().ranges)?;
4276 let items = <Self as v1::HotShotAvailabilityApi>::get_vid_common_ranges(self, ranges)
4277 .await
4278 .map_err(to_status)?;
4279 Ok(tonic::Response::new(
4280 proto::VidCommonRangeResponse::try_from(&*items)?,
4281 ))
4282 }
4283
4284 async fn get_transaction(
4285 &self,
4286 request: tonic::Request<proto::GetTransactionRequest>,
4287 ) -> Result<tonic::Response<proto::TransactionResponse>, tonic::Status> {
4288 let request = request.into_inner();
4289 let tx = match (request.height, request.index, request.hash) {
4290 (Some(height), Some(index), None) => {
4291 <Self as v1::HotShotAvailabilityApi>::get_transaction_by_position(
4292 self, height, index,
4293 )
4294 .await
4295 },
4296 (None, None, Some(hash)) => {
4297 <Self as v1::HotShotAvailabilityApi>::get_transaction_by_hash(self, hash).await
4298 },
4299 _ => {
4300 return Err(tonic::Status::invalid_argument(
4301 "set height and index, or hash",
4302 ));
4303 },
4304 }
4305 .map_err(to_status)?;
4306 Ok(tonic::Response::new(proto::TransactionResponse::from(&tx)))
4307 }
4308
4309 async fn get_transaction_proof(
4310 &self,
4311 request: tonic::Request<proto::GetTransactionProofRequest>,
4312 ) -> Result<tonic::Response<proto::TransactionWithProofResponse>, tonic::Status> {
4313 let request = request.into_inner();
4314 let tx = match (request.height, request.index, request.hash) {
4315 (Some(height), Some(index), None) => {
4316 <Self as v1::HotShotAvailabilityApi>::get_transaction_proof_by_position(
4317 self, height, index,
4318 )
4319 .await
4320 },
4321 (None, None, Some(hash)) => {
4322 <Self as v1::HotShotAvailabilityApi>::get_transaction_proof_by_hash(self, hash)
4323 .await
4324 },
4325 _ => {
4326 return Err(tonic::Status::invalid_argument(
4327 "set height and index, or hash",
4328 ));
4329 },
4330 }
4331 .map_err(to_status)?;
4332 Ok(tonic::Response::new(
4333 proto::TransactionWithProofResponse::try_from(&tx)?,
4334 ))
4335 }
4336
4337 async fn get_block_summary(
4338 &self,
4339 request: tonic::Request<proto::GetBlockSummaryRequest>,
4340 ) -> Result<tonic::Response<proto::BlockSummaryResponse>, tonic::Status> {
4341 let height = required(request.into_inner().height, "height")? as usize;
4342 let summary = <Self as v1::HotShotAvailabilityApi>::get_block_summary(self, height)
4343 .await
4344 .map_err(to_status)?;
4345 Ok(tonic::Response::new(proto::BlockSummaryResponse::from(
4346 &summary,
4347 )))
4348 }
4349
4350 async fn get_block_summary_range(
4351 &self,
4352 request: tonic::Request<proto::GetBlockSummaryRangeRequest>,
4353 ) -> Result<tonic::Response<proto::BlockSummaryRangeResponse>, tonic::Status> {
4354 let request = request.into_inner();
4355 let range = range_from_query(request.from, request.until)?;
4356 let summaries = <Self as v1::HotShotAvailabilityApi>::get_block_summary_range(
4357 self,
4358 range.start as usize,
4359 range.end as usize,
4360 )
4361 .await
4362 .map_err(to_status)?;
4363 Ok(tonic::Response::new(
4364 proto::BlockSummaryRangeResponse::from(&*summaries),
4365 ))
4366 }
4367
4368 async fn get_namespace_proof(
4369 &self,
4370 request: tonic::Request<proto::GetNamespaceProofRequest>,
4371 ) -> Result<tonic::Response<proto::NamespaceProofResponse>, tonic::Status> {
4372 let request = request.into_inner();
4373 let id = block_id_from_query(request.height, request.hash, request.payload_hash)?;
4374 let namespace = required(namespace_from_query(request.namespace)?, "namespace")?;
4375 let proof = <Self as v1::AvailabilityApi>::get_namespace_proof(self, id, namespace)
4376 .await
4377 .map_err(to_status)?;
4378 Ok(tonic::Response::new(
4379 proto::NamespaceProofResponse::try_from(&proof)?,
4380 ))
4381 }
4382
4383 async fn get_namespace_proof_range(
4384 &self,
4385 request: tonic::Request<proto::GetNamespaceProofRangeRequest>,
4386 ) -> Result<tonic::Response<proto::NamespaceProofRangeResponse>, tonic::Status> {
4387 let request = request.into_inner();
4388 let range = range_from_query(request.from, request.until)?;
4389 let namespace = required(namespace_from_query(request.namespace)?, "namespace")?;
4390 let proofs = <Self as v1::AvailabilityApi>::get_namespace_proof_range(
4391 self,
4392 range.start,
4393 range.end,
4394 namespace,
4395 )
4396 .await
4397 .map_err(to_status)?;
4398 Ok(tonic::Response::new(
4399 proto::NamespaceProofRangeResponse::try_from(&*proofs)?,
4400 ))
4401 }
4402
4403 async fn get_incorrect_encoding_proof(
4404 &self,
4405 request: tonic::Request<proto::GetIncorrectEncodingProofRequest>,
4406 ) -> Result<tonic::Response<proto::IncorrectEncodingProofResponse>, tonic::Status> {
4407 let request = request.into_inner();
4408 let height = required(request.height, "height")?;
4409 let namespace = required(namespace_from_query(request.namespace)?, "namespace")?;
4410 let proof = <Self as v1::AvailabilityApi>::get_incorrect_encoding_proof(
4411 self,
4412 v1::availability::BlockId::Height(height),
4413 namespace,
4414 )
4415 .await
4416 .map_err(to_status)?;
4417 Ok(tonic::Response::new(
4418 proto::IncorrectEncodingProofResponse {
4419 proof: Some((&proof).try_into()?),
4420 },
4421 ))
4422 }
4423
4424 async fn get_state_cert(
4425 &self,
4426 request: tonic::Request<proto::GetStateCertRequest>,
4427 ) -> Result<tonic::Response<proto::StateCertV1Response>, tonic::Status> {
4428 let cert = <Self as v1::AvailabilityApi>::get_state_cert(
4429 self,
4430 required(request.into_inner().epoch, "epoch")?,
4431 )
4432 .await
4433 .map_err(to_status)?;
4434 Ok(tonic::Response::new(proto::StateCertV1Response::from(
4435 &cert,
4436 )))
4437 }
4438
4439 async fn get_state_cert_v2(
4440 &self,
4441 request: tonic::Request<proto::GetStateCertV2Request>,
4442 ) -> Result<tonic::Response<proto::StateCertV2Response>, tonic::Status> {
4443 let cert = <Self as v1::AvailabilityApi>::get_state_cert_v2(
4444 self,
4445 required(request.into_inner().epoch, "epoch")?,
4446 )
4447 .await
4448 .map_err(to_status)?;
4449 Ok(tonic::Response::new(proto::StateCertV2Response::from(
4450 &cert,
4451 )))
4452 }
4453
4454 type StreamLeavesStream = BoxStream<'static, Result<proto::LeafResponse, tonic::Status>>;
4455
4456 async fn stream_leaves(
4457 &self,
4458 request: tonic::Request<proto::StreamLeavesRequest>,
4459 ) -> Result<tonic::Response<Self::StreamLeavesStream>, tonic::Status> {
4460 let from = required(request.into_inner().from, "from")? as usize;
4461 let leaves = <Self as v1::HotShotAvailabilityApi>::stream_leaves(self, from)
4462 .await
4463 .map_err(to_status)?;
4464 Ok(tonic::Response::new(
4465 leaves
4466 .map(|leaf| Ok(proto::LeafResponse::from(&leaf)))
4467 .boxed(),
4468 ))
4469 }
4470
4471 type StreamHeadersStream = BoxStream<'static, Result<proto::HeaderResponse, tonic::Status>>;
4472
4473 async fn stream_headers(
4474 &self,
4475 request: tonic::Request<proto::StreamHeadersRequest>,
4476 ) -> Result<tonic::Response<Self::StreamHeadersStream>, tonic::Status> {
4477 let from = required(request.into_inner().from, "from")? as usize;
4478 let headers = <Self as v1::HotShotAvailabilityApi>::stream_headers(self, from)
4479 .await
4480 .map_err(to_status)?;
4481 Ok(tonic::Response::new(
4482 headers
4483 .map(|header| Ok(proto::HeaderResponse::from(&header)))
4484 .boxed(),
4485 ))
4486 }
4487
4488 type StreamBlocksStream = BoxStream<'static, Result<proto::BlockResponse, tonic::Status>>;
4489
4490 async fn stream_blocks(
4491 &self,
4492 request: tonic::Request<proto::StreamBlocksRequest>,
4493 ) -> Result<tonic::Response<Self::StreamBlocksStream>, tonic::Status> {
4494 let from = required(request.into_inner().from, "from")? as usize;
4495 let blocks = <Self as v1::HotShotAvailabilityApi>::stream_blocks(self, from)
4496 .await
4497 .map_err(to_status)?;
4498 Ok(tonic::Response::new(
4499 blocks
4500 .map(|block| Ok(proto::BlockResponse::from(&block)))
4501 .boxed(),
4502 ))
4503 }
4504
4505 type StreamPayloadsStream = BoxStream<'static, Result<proto::PayloadResponse, tonic::Status>>;
4506
4507 async fn stream_payloads(
4508 &self,
4509 request: tonic::Request<proto::StreamPayloadsRequest>,
4510 ) -> Result<tonic::Response<Self::StreamPayloadsStream>, tonic::Status> {
4511 let from = required(request.into_inner().from, "from")? as usize;
4512 let payloads = <Self as v1::HotShotAvailabilityApi>::stream_payloads(self, from)
4513 .await
4514 .map_err(to_status)?;
4515 Ok(tonic::Response::new(
4516 payloads
4517 .map(|payload| Ok(proto::PayloadResponse::from(&payload)))
4518 .boxed(),
4519 ))
4520 }
4521
4522 type StreamVidCommonStream =
4523 BoxStream<'static, Result<proto::VidCommonResponse, tonic::Status>>;
4524
4525 async fn stream_vid_common(
4526 &self,
4527 request: tonic::Request<proto::StreamVidCommonRequest>,
4528 ) -> Result<tonic::Response<Self::StreamVidCommonStream>, tonic::Status> {
4529 let from = required(request.into_inner().from, "from")? as usize;
4530 let items = <Self as v1::HotShotAvailabilityApi>::stream_vid_common(self, from)
4531 .await
4532 .map_err(to_status)?;
4533 Ok(tonic::Response::new(end_at_first_error(
4534 items.map(|item| proto::VidCommonResponse::try_from(&item)),
4535 )))
4536 }
4537
4538 type StreamTransactionsStream =
4539 BoxStream<'static, Result<proto::TransactionResponse, tonic::Status>>;
4540
4541 async fn stream_transactions(
4542 &self,
4543 request: tonic::Request<proto::StreamTransactionsRequest>,
4544 ) -> Result<tonic::Response<Self::StreamTransactionsStream>, tonic::Status> {
4545 let request = request.into_inner();
4546 let transactions = <Self as v1::HotShotAvailabilityApi>::stream_transactions(
4547 self,
4548 required(request.from, "from")? as usize,
4549 namespace_from_query(request.namespace)?,
4550 )
4551 .await
4552 .map_err(to_status)?;
4553 Ok(tonic::Response::new(
4554 transactions
4555 .map(|tx| Ok(proto::TransactionResponse::from(&tx)))
4556 .boxed(),
4557 ))
4558 }
4559
4560 type StreamNamespaceProofsStream =
4561 BoxStream<'static, Result<proto::NamespaceProofResponse, tonic::Status>>;
4562
4563 async fn stream_namespace_proofs(
4564 &self,
4565 request: tonic::Request<proto::StreamNamespaceProofsRequest>,
4566 ) -> Result<tonic::Response<Self::StreamNamespaceProofsStream>, tonic::Status> {
4567 let request = request.into_inner();
4568 let namespace = required(namespace_from_query(request.namespace)?, "namespace")?;
4569 let proofs = <Self as v1::AvailabilityApi>::stream_namespace_proofs(
4570 self,
4571 required(request.from, "from")? as usize,
4572 namespace,
4573 )
4574 .await
4575 .map_err(to_status)?;
4576 Ok(tonic::Response::new(end_at_first_error(proofs.map(
4577 |proof| proto::NamespaceProofResponse::try_from(&proof),
4578 ))))
4579 }
4580}
4581
4582#[tonic::async_trait]
4583impl<D> proto::merklized_state_service_server::MerklizedStateService for NodeApiStateImpl<D>
4584where
4585 D: Deref + Clone + Send + Sync + 'static,
4586 D::Target: hotshot_query_service::merklized_state::MerklizedStateDataSource<
4587 SeqTypes,
4588 espresso_types::BlockMerkleTree,
4589 { <espresso_types::BlockMerkleTree as jf_merkle_tree_compat::MerkleTreeScheme>::ARITY },
4590 > + hotshot_query_service::merklized_state::MerklizedStateDataSource<
4591 SeqTypes,
4592 espresso_types::FeeMerkleTree,
4593 { <espresso_types::FeeMerkleTree as jf_merkle_tree_compat::MerkleTreeScheme>::ARITY },
4594 > + hotshot_query_service::merklized_state::MerklizedStateHeightPersistence
4595 + Send
4596 + Sync,
4597{
4598 async fn get_block_state_path(
4599 &self,
4600 request: tonic::Request<proto::GetBlockStatePathRequest>,
4601 ) -> Result<tonic::Response<proto::MerklePathResponse>, tonic::Status> {
4602 let request = request.into_inner();
4603 let key = required(request.key, "key")?;
4604 let snapshot = snapshot_from_query(request.height, request.commit)?;
4605 let proof =
4607 <Self as v1::BlockStateApi>::get_block_state_path(self, snapshot, key.to_string())
4608 .await
4609 .map_err(to_status)?;
4610 Ok(tonic::Response::new(proto::MerklePathResponse::from(
4611 &proof,
4612 )))
4613 }
4614
4615 async fn get_fee_state_path(
4616 &self,
4617 request: tonic::Request<proto::GetFeeStatePathRequest>,
4618 ) -> Result<tonic::Response<proto::MerklePathResponse>, tonic::Status> {
4619 let request = request.into_inner();
4620 let address = required(request.address, "address")?;
4621 let snapshot = snapshot_from_query(request.height, request.commit)?;
4622 let proof = <Self as v1::FeeStateApi>::get_fee_state_path(self, snapshot, address)
4623 .await
4624 .map_err(to_status)?;
4625 Ok(tonic::Response::new(proto::MerklePathResponse::from(
4626 &proof,
4627 )))
4628 }
4629
4630 async fn get_latest_fee_balance(
4631 &self,
4632 request: tonic::Request<proto::GetLatestFeeBalanceRequest>,
4633 ) -> Result<tonic::Response<proto::FeeBalanceResponse>, tonic::Status> {
4634 let address = required(request.into_inner().address, "address")?;
4635 let balance = <Self as v1::FeeStateApi>::get_fee_balance_latest(self, address)
4636 .await
4637 .map_err(to_status)?;
4638 Ok(tonic::Response::new(proto::FeeBalanceResponse {
4639 balance: balance.unwrap_or_default().to_string(),
4640 }))
4641 }
4642
4643 async fn get_state_height(
4644 &self,
4645 _request: tonic::Request<proto::GetStateHeightRequest>,
4646 ) -> Result<tonic::Response<proto::StateHeightResponse>, tonic::Status> {
4647 let height = <Self as v1::BlockStateApi>::get_block_state_height(self)
4648 .await
4649 .map_err(to_status)?;
4650 Ok(tonic::Response::new(proto::StateHeightResponse { height }))
4651 }
4652}
4653
4654#[tonic::async_trait]
4655impl<D> proto::reward_state_service_server::RewardStateService for NodeApiStateImpl<D>
4656where
4657 D: RewardMerkleTreeDataSource + Deref + Clone + Send + Sync + 'static,
4658 D::Target: MerklizedStateHeightPersistence
4659 + MerklizedStateDataSource<
4660 SeqTypes,
4661 RewardMerkleTreeV1,
4662 { <RewardMerkleTreeV1 as MerkleTreeScheme>::ARITY },
4663 > + MerklizedStateDataSource<
4664 SeqTypes,
4665 RewardMerkleTreeV2,
4666 { <RewardMerkleTreeV2 as MerkleTreeScheme>::ARITY },
4667 > + Send
4668 + Sync,
4669{
4670 async fn get_reward_balance(
4671 &self,
4672 request: tonic::Request<proto::GetRewardBalanceRequest>,
4673 ) -> Result<tonic::Response<proto::RewardBalanceResponse>, tonic::Status> {
4674 let request = request.into_inner();
4675 let address = required(request.address, "address")?;
4676 let balance = match request.height {
4677 Some(height) => v1::RewardApi::get_reward_balance(self, height, address).await,
4678 None => v1::RewardApi::get_latest_reward_balance(self, address).await,
4679 }
4680 .map_err(to_status)?;
4681 Ok(tonic::Response::new(proto::RewardBalanceResponse {
4682 balance: balance.to_string(),
4683 }))
4684 }
4685
4686 async fn get_reward_account_proof(
4687 &self,
4688 request: tonic::Request<proto::GetRewardAccountProofRequest>,
4689 ) -> Result<tonic::Response<proto::RewardAccountProofResponse>, tonic::Status> {
4690 let request = request.into_inner();
4691 let address = required(request.address, "address")?;
4692 let query = match request.height {
4693 Some(height) => v1::RewardApi::get_reward_account_proof(self, height, address).await,
4694 None => v1::RewardApi::get_latest_reward_account_proof(self, address).await,
4695 }
4696 .map_err(to_status)?;
4697 Ok(tonic::Response::new(
4698 proto::RewardAccountProofResponse::from(query),
4699 ))
4700 }
4701
4702 async fn get_reward_claim_input(
4703 &self,
4704 request: tonic::Request<proto::GetRewardClaimInputRequest>,
4705 ) -> Result<tonic::Response<proto::RewardClaimInputResponse>, tonic::Status> {
4706 let request = request.into_inner();
4707 let address = required(request.address, "address")?;
4708 let height = required(request.height, "height")?;
4709 let InternalRewardClaimInput {
4710 lifetime_rewards,
4711 auth_data,
4712 } = v1::RewardApi::get_reward_claim_input(self, height, address)
4713 .await
4714 .map_err(to_status)?;
4715 Ok(tonic::Response::new(proto::RewardClaimInputResponse {
4716 lifetime_rewards: lifetime_rewards.to_string(),
4717 auth_data: alloy::primitives::Bytes::from(auth_data).to_string(),
4718 }))
4719 }
4720
4721 async fn get_reward_amounts(
4722 &self,
4723 request: tonic::Request<proto::GetRewardAmountsRequest>,
4724 ) -> Result<tonic::Response<proto::RewardAmountsResponse>, tonic::Status> {
4725 let request = request.into_inner();
4726 let height = required(request.height, "height")?;
4727 let offset = required(request.offset, "offset")?;
4728 let limit = required(request.limit, "limit")?;
4729 let amounts = v1::RewardApi::get_reward_amounts(self, height, offset, limit)
4730 .await
4731 .map_err(to_status)?;
4732 Ok(tonic::Response::new(proto::RewardAmountsResponse {
4735 amounts: amounts
4736 .into_iter()
4737 .rev()
4738 .map(|(address, amount)| proto::RewardAmountPair {
4739 address: address.to_string(),
4740 amount: amount.to_string(),
4741 })
4742 .collect(),
4743 }))
4744 }
4745
4746 async fn get_reward_merkle_tree_v2(
4747 &self,
4748 request: tonic::Request<proto::GetRewardMerkleTreeV2Request>,
4749 ) -> Result<tonic::Response<proto::RewardMerkleTreeV2Response>, tonic::Status> {
4750 let height = required(request.into_inner().height, "height")?;
4751 let tree = v1::RewardApi::get_reward_merkle_tree_v2(self, height)
4752 .await
4753 .map_err(to_status)?;
4754 Ok(tonic::Response::new(proto::RewardMerkleTreeV2Response {
4755 tree,
4756 }))
4757 }
4758}
4759
4760fn snapshot_from_query(
4761 height: Option<u64>,
4762 commit: Option<String>,
4763) -> Result<v1::Snapshot, tonic::Status> {
4764 match (height, commit) {
4765 (Some(height), None) => Ok(v1::Snapshot::Height(height)),
4766 (None, Some(commit)) => Ok(v1::Snapshot::Commit(commit)),
4767 _ => Err(tonic::Status::invalid_argument(
4768 "set exactly one of height or commit",
4769 )),
4770 }
4771}
4772
4773#[cfg(test)]
4774mod tests {
4775 use std::net::{IpAddr, Ipv6Addr};
4776
4777 use alloy::primitives::{Address, U256};
4778 use base64::Engine as _;
4779 use committable::Committable as _;
4780 use espresso_types::{PubKey, v0_3::RegisteredValidator};
4781 use hotshot_query_service::{
4782 explorer::errors::BadQuery,
4783 node::{ResourceSyncStatus, SyncStatus, SyncStatusRange},
4784 };
4785 use hotshot_types::{
4786 addr::NetAddr,
4787 vid::{
4788 advz::advz_scheme,
4789 avidm::{AvidMScheme, init_avidm_param},
4790 avidm_gf2::{AvidmGf2Scheme, init_avidm_gf2_param},
4791 },
4792 x25519,
4793 };
4794 use jf_advz::VidScheme as _;
4795 use proto::vid_share_response::Share;
4796
4797 use super::*;
4798
4799 fn json_bytes(value: &serde_json::Value) -> Vec<u8> {
4801 <Vec<u8> as serde::Deserialize>::deserialize(value).unwrap()
4802 }
4803
4804 fn load_vector<T>(path: &str) -> (T, serde_json::Value)
4805 where
4806 T: serde::de::DeserializeOwned,
4807 {
4808 let json: serde_json::Value =
4809 serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
4810 (T::deserialize(&json).unwrap(), json)
4811 }
4812
4813 fn reference_header(version: &str) -> (espresso_types::Header, serde_json::Value) {
4819 let path = format!("../../../data/{version}/header.json");
4820 let json: serde_json::Value =
4821 serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
4822 let header: espresso_types::Header = serde_json::from_value(json.clone()).unwrap();
4823 let fields = json.get("fields").cloned().unwrap_or(json);
4825 (header, fields)
4826 }
4827
4828 fn assert_same_fields(message: &str, reference: &serde_json::Value) {
4832 use prost::Message as _;
4833 let descriptors =
4834 prost_types::FileDescriptorSet::decode(espresso_api::FILE_DESCRIPTOR_SET).unwrap();
4835 let descriptor = descriptors
4836 .file
4837 .iter()
4838 .flat_map(|file| &file.message_type)
4839 .find(|candidate| candidate.name() == message)
4840 .unwrap_or_else(|| panic!("no proto message {message}"));
4841 let declared: std::collections::BTreeSet<&str> = descriptor
4842 .field
4843 .iter()
4844 .map(|field| match field.oneof_index {
4845 Some(index) if !field.proto3_optional() => {
4849 descriptor.oneof_decl[index as usize].name()
4850 },
4851 _ => field.name(),
4852 })
4853 .collect();
4854 let referenced: std::collections::BTreeSet<&str> = reference
4855 .as_object()
4856 .unwrap()
4857 .keys()
4858 .map(String::as_str)
4859 .filter(|name| *name != "_pd")
4862 .collect();
4863 assert_eq!(
4864 declared, referenced,
4865 "{message} fields drifted from the reference vector"
4866 );
4867 }
4868
4869 #[test]
4875 fn every_header_version_maps_to_its_arm_and_fields() {
4876 for (version, shape, dropped) in [
4877 ("v1", "HeaderV1", None),
4878 ("v2", "HeaderV1", None),
4879 ("v3", "HeaderV3", None),
4880 ("v4", "HeaderV4", None),
4881 ("v5", "HeaderV5", None),
4882 ("v6", "HeaderV5", None),
4883 ("v7", "HeaderV5", Some("builder_commitment")),
4884 ] {
4885 let (header, fields) = reference_header(version);
4886 let mut expected = fields.clone();
4887 if let Some(field) = dropped {
4888 expected[field] = String::new().into();
4889 }
4890 assert_same_fields(shape, &expected);
4891
4892 use proto::header_response::Header;
4893 let converted = proto::HeaderResponse::from(&header).header.unwrap();
4894 macro_rules! assert_shared_fields {
4898 ($header:expr) => {{
4899 let header = $header;
4900 assert_eq!(header.height, fields["height"].as_u64().unwrap());
4901 assert_eq!(header.timestamp, fields["timestamp"].as_u64().unwrap());
4902 assert_eq!(header.l1_head, fields["l1_head"].as_u64().unwrap());
4903 assert_eq!(
4904 header.payload_commitment,
4905 fields["payload_commitment"].as_str().unwrap()
4906 );
4907 assert_eq!(
4908 header.block_merkle_tree_root,
4909 fields["block_merkle_tree_root"].as_str().unwrap()
4910 );
4911 assert_eq!(
4912 header.fee_merkle_tree_root,
4913 fields["fee_merkle_tree_root"].as_str().unwrap()
4914 );
4915 let fee_info = header.fee_info.as_ref().unwrap();
4916 assert_eq!(
4917 fee_info.account,
4918 fields["fee_info"]["account"].as_str().unwrap()
4919 );
4920 assert_eq!(
4921 fee_info.amount,
4922 fields["fee_info"]["amount"].as_str().unwrap()
4923 );
4924 assert_eq!(
4925 header.ns_table.as_ref().unwrap().bytes,
4926 base64::engine::general_purpose::STANDARD
4927 .decode(fields["ns_table"]["bytes"].as_str().unwrap())
4928 .unwrap()
4929 );
4930 assert_eq!(
4931 header.l1_finalized.is_some(),
4932 !fields["l1_finalized"].is_null()
4933 );
4934 assert_eq!(
4935 header.builder_signature.is_some(),
4936 !fields["builder_signature"].is_null()
4937 );
4938 assert!(header.chain_config.is_some());
4939 }};
4940 }
4941 macro_rules! assert_builder_commitment {
4943 ($header:expr) => {
4944 assert_eq!(
4945 $header.builder_commitment,
4946 fields["builder_commitment"].as_str().unwrap()
4947 );
4948 };
4949 }
4950 let arm = match converted {
4951 Header::V1(header) => {
4952 assert_builder_commitment!(&header);
4953 assert_shared_fields!(header);
4954 "v1"
4955 },
4956 Header::V2(header) => {
4957 assert_builder_commitment!(&header);
4958 assert_shared_fields!(header);
4959 "v2"
4960 },
4961 Header::V3(header) => {
4962 assert_builder_commitment!(&header);
4963 assert_shared_fields!(&header);
4964 assert_eq!(
4965 header.reward_merkle_tree_root,
4966 fields["reward_merkle_tree_root"].as_str().unwrap()
4967 );
4968 "v3"
4969 },
4970 Header::V4(header) => {
4971 assert_builder_commitment!(&header);
4972 assert_shared_fields!(&header);
4973 assert_eq!(
4974 header.timestamp_millis,
4975 fields["timestamp_millis"].as_u64().unwrap()
4976 );
4977 assert_eq!(
4978 header.total_reward_distributed,
4979 fields["total_reward_distributed"].as_str().unwrap()
4980 );
4981 assert_eq!(
4982 header.next_stake_table_hash.as_deref(),
4983 fields["next_stake_table_hash"].as_str()
4984 );
4985 "v4"
4986 },
4987 Header::V5(header) => {
4988 assert_builder_commitment!(&header);
4989 assert_shared_fields!(&header);
4990 "v5"
4991 },
4992 Header::V6(header) => {
4993 assert_builder_commitment!(&header);
4994 assert_shared_fields!(&header);
4995 "v6"
4996 },
4997 Header::V7(header) => {
4998 assert_eq!(
4999 header.builder_commitment, "",
5000 "0.7 carries no builder commitment"
5001 );
5002 assert_shared_fields!(&header);
5003 "v7"
5004 },
5005 };
5006 assert_eq!(arm, version, "{version} header selected the {arm} arm");
5007 }
5008 }
5009
5010 #[test]
5011 fn v6_header_mirrors_the_reference_vector() {
5012 let (header, fields) = reference_header("v6");
5013 assert_same_fields("HeaderV5", &fields);
5014 let proto::HeaderResponse { header: converted } = (&header).into();
5015 let Some(proto::header_response::Header::V6(converted)) = converted else {
5016 panic!("a 0.6 header must convert to the V6 arm, got {converted:?}");
5017 };
5018
5019 assert_eq!(converted.height, fields["height"].as_u64().unwrap());
5020 assert_eq!(converted.timestamp, fields["timestamp"].as_u64().unwrap());
5021 assert_eq!(
5022 converted.timestamp_millis,
5023 fields["timestamp_millis"].as_u64().unwrap()
5024 );
5025 assert_eq!(converted.l1_head, fields["l1_head"].as_u64().unwrap());
5026 assert_eq!(
5027 converted.payload_commitment,
5028 fields["payload_commitment"].as_str().unwrap()
5029 );
5030 assert_eq!(
5031 converted.builder_commitment,
5032 fields["builder_commitment"].as_str().unwrap()
5033 );
5034 assert_eq!(
5035 converted.block_merkle_tree_root,
5036 fields["block_merkle_tree_root"].as_str().unwrap()
5037 );
5038 assert_eq!(
5039 converted.fee_merkle_tree_root,
5040 fields["fee_merkle_tree_root"].as_str().unwrap()
5041 );
5042 assert_eq!(
5043 converted.reward_merkle_tree_root,
5044 fields["reward_merkle_tree_root"].as_str().unwrap()
5045 );
5046 assert_eq!(
5047 converted.total_reward_distributed,
5048 fields["total_reward_distributed"].as_str().unwrap()
5049 );
5050 assert_eq!(
5051 converted.next_stake_table_hash.as_deref(),
5052 fields["next_stake_table_hash"].as_str()
5053 );
5054
5055 let fee_info = converted.fee_info.unwrap();
5056 assert_eq!(fee_info.account, fields["fee_info"]["account"]);
5057 assert_eq!(fee_info.amount, fields["fee_info"]["amount"]);
5058
5059 let l1_finalized = converted.l1_finalized.unwrap();
5060 assert_eq!(
5061 l1_finalized.number,
5062 fields["l1_finalized"]["number"].as_u64().unwrap()
5063 );
5064 assert_eq!(l1_finalized.timestamp, fields["l1_finalized"]["timestamp"]);
5065 assert_eq!(l1_finalized.hash, fields["l1_finalized"]["hash"]);
5066
5067 let signature = converted.builder_signature.unwrap();
5068 assert_eq!(signature.r, fields["builder_signature"]["r"]);
5069 assert_eq!(signature.s, fields["builder_signature"]["s"]);
5070 assert_eq!(
5071 signature.v,
5072 fields["builder_signature"]["v"].as_u64().unwrap() as u32
5073 );
5074
5075 let ns_table = converted.ns_table.unwrap();
5077 assert_eq!(
5078 base64::engine::general_purpose::STANDARD.encode(&ns_table.bytes),
5079 fields["ns_table"]["bytes"].as_str().unwrap()
5080 );
5081
5082 let config = match converted.chain_config.unwrap().chain_config.unwrap() {
5083 proto::resolvable_chain_config::ChainConfig::Full(config) => config,
5084 other => panic!("the reference header carries a full config, got {other:?}"),
5085 };
5086 let expected = &fields["chain_config"]["chain_config"]["Left"];
5087 assert_same_fields("ChainConfig", expected);
5088
5089 assert_eq!(config.chain_id, expected["chain_id"]);
5090 assert_eq!(
5091 config.max_block_size,
5092 expected["max_block_size"]
5093 .as_str()
5094 .unwrap()
5095 .parse::<u64>()
5096 .unwrap()
5097 );
5098 assert_eq!(config.base_fee, expected["base_fee"]);
5099 assert_eq!(config.fee_recipient, expected["fee_recipient"]);
5100 assert_eq!(
5101 config.fee_contract.as_deref(),
5102 expected["fee_contract"].as_str()
5103 );
5104 assert_eq!(
5105 config.stake_table_contract.as_deref(),
5106 expected["stake_table_contract"].as_str()
5107 );
5108
5109 assert_eq!(converted.leader_counts.len(), 100);
5110 let expected_counts: Vec<u32> = fields["leader_counts"]
5111 .as_array()
5112 .unwrap()
5113 .iter()
5114 .map(|count| count.as_u64().unwrap() as u32)
5115 .collect();
5116 assert_eq!(converted.leader_counts, expected_counts);
5117 }
5118
5119 #[test]
5122 fn commitment_only_chain_config_keeps_the_commitment() {
5123 let config = espresso_types::v0_3::ChainConfig::default();
5124 let commitment = config.commit();
5125 let resolvable = espresso_types::v0_3::ResolvableChainConfig::from(commitment);
5126
5127 let converted = proto::ResolvableChainConfig::from(resolvable)
5128 .chain_config
5129 .unwrap();
5130 assert_eq!(
5131 converted,
5132 proto::resolvable_chain_config::ChainConfig::Commitment(commitment.to_string())
5133 );
5134 }
5135
5136 #[test]
5140 fn every_vid_share_arm_maps_to_its_own_shape() {
5141 let payload = b"two namespaces worth of payload bytes, dispersed";
5142 let weights = [1u32, 1, 1];
5143 let ns_table = vec![0..24usize, 24..payload.len()];
5144
5145 let mut advz = advz_scheme(3);
5146 let share = VidShare::V0(advz.disperse(payload).unwrap().shares.remove(0));
5147 let Share::V0(advz) = proto::VidShareResponse::try_from(&share)
5148 .unwrap()
5149 .share
5150 .unwrap()
5151 else {
5152 panic!("the V0 arm");
5153 };
5154 assert!(advz.aggregate_proofs.starts_with("FIELD~"));
5155 let v1 = &serde_json::to_value(&share).unwrap()["V0"];
5156 assert_eq!(
5157 advz.aggregate_proofs,
5158 v1["aggregate_proofs"].as_str().unwrap()
5159 );
5160 assert_eq!(advz.evals, v1["evals"].as_str().unwrap());
5161 let proof = advz.evals_proof.unwrap();
5162 assert_eq!(proof.pos, v1["evals_proof"]["pos"].as_str().unwrap());
5163 let v1_nodes = v1["evals_proof"]["proof"].as_array().unwrap();
5164 assert!(v1_nodes.len() > 1, "{v1}");
5165 assert_eq!(proof.proof.len(), v1_nodes.len());
5166 for (v1_node, node) in v1_nodes.iter().zip(&proof.proof) {
5167 assert_advz_node_matches_v1(v1_node, node);
5168 }
5169
5170 let param = init_avidm_param(3).unwrap();
5171 let (_, mut shares) =
5172 AvidMScheme::ns_disperse(¶m, &weights, payload, ns_table.clone()).unwrap();
5173 let share = VidShare::V1(shares.remove(0));
5174 let Share::V1(avidm) = proto::VidShareResponse::try_from(&share)
5175 .unwrap()
5176 .share
5177 .unwrap()
5178 else {
5179 panic!("the V1 arm");
5180 };
5181 assert_eq!(avidm.ns_lens, [24, 24]);
5182 assert_eq!(avidm.ns_commits.len(), 2);
5183 assert!(avidm.ns_commits[0].starts_with("AvidMCommit~"));
5184 assert_eq!(avidm.content.len(), 2);
5185 assert!(avidm.content[0].payload.starts_with("FIELD~"));
5186
5187 let param = init_avidm_gf2_param(3).unwrap();
5188 let (_, _, mut shares) =
5189 AvidmGf2Scheme::ns_disperse(¶m, &weights, payload, ns_table).unwrap();
5190 let share = VidShare::V2(shares.remove(0));
5191 let Share::V2(gf2) = proto::VidShareResponse::try_from(&share)
5192 .unwrap()
5193 .share
5194 .unwrap()
5195 else {
5196 panic!("the V2 arm");
5197 };
5198 assert_eq!(gf2.namespaces.len(), 2);
5199 assert!(!gf2.namespaces[0].payload.is_empty());
5200 assert!(gf2.namespaces[0].mt_proofs[0].starts_with("MERKLE_PROOF~"));
5201 }
5202
5203 fn assert_advz_node_matches_v1(v1: &serde_json::Value, v2: &proto::AdvzMerkleNode) {
5204 use proto::advz_merkle_node::Node;
5205 match (v2.node.as_ref().unwrap(), v1) {
5206 (Node::Leaf(leaf), v1) if v1.get("Leaf").is_some() => {
5207 let v1 = &v1["Leaf"];
5208 assert_eq!(leaf.elem, v1["elem"].as_str().unwrap());
5209 assert_eq!(leaf.pos, v1["pos"].as_str().unwrap());
5210 assert_eq!(leaf.value, v1["value"].as_str().unwrap());
5211 },
5212 (Node::Branch(branch), v1) if v1.get("Branch").is_some() => {
5213 let v1 = &v1["Branch"];
5214 assert_eq!(branch.value, v1["value"].as_str().unwrap());
5215 let v1_children = v1["children"].as_array().unwrap();
5216 assert_eq!(branch.children.len(), v1_children.len());
5217 for (v1_child, v2_child) in v1_children.iter().zip(&branch.children) {
5218 assert_advz_node_matches_v1(v1_child, v2_child);
5219 }
5220 },
5221 (Node::ForgottenSubtree(subtree), v1) if v1.get("ForgettenSubtree").is_some() => {
5222 assert_eq!(
5223 subtree.value,
5224 v1["ForgettenSubtree"]["value"].as_str().unwrap()
5225 );
5226 },
5227 (Node::Empty(_), v1) if v1.as_str() == Some("Empty") => {},
5228 (v2, v1) => panic!("{v2:?} against {v1}"),
5229 }
5230 }
5231
5232 #[test]
5234 fn validator_maps_hex_quantities_and_sorts_delegators() {
5235 let delegator = |byte: u8| Address::from([byte; 20]);
5236 let key = x25519::Keypair::generated_from_seed_indexed([3; 32], 0)
5237 .unwrap()
5238 .public_key();
5239 let stake = U256::from(1_000_000_000_000_000_000u64);
5240 let p2p_addr = NetAddr::Inet(IpAddr::V6(Ipv6Addr::LOCALHOST), 9977);
5241 let registered = RegisteredValidator::<PubKey> {
5242 account: delegator(0xab),
5243 stake_table_key: None,
5244 state_ver_key: None,
5245 stake,
5246 commission: 1234,
5247 delegators: HashMap::from([
5248 (delegator(0xff), U256::from(10)),
5249 (delegator(0x01), U256::from(255)),
5250 ]),
5251 authenticated: true,
5252 x25519_key: Some(key),
5253 p2p_addr: Some(p2p_addr.clone()),
5254 };
5255
5256 let proto = proto::Validator::from(registered);
5257
5258 let x25519_key = proto.x25519_key.as_deref().unwrap();
5260 assert_eq!(x25519_key.parse::<x25519::PublicKey>().unwrap(), key);
5261 assert!(x25519_key.starts_with("X25519_PK~"));
5262 assert_ne!(
5263 Some(x25519_key),
5264 serde_json::to_value(key).unwrap().as_str()
5265 );
5266 assert_eq!(
5268 proto.p2p_addr.as_deref(),
5269 serde_json::to_value(&p2p_addr).unwrap().as_str()
5270 );
5271 assert_ne!(
5272 proto.p2p_addr.as_deref(),
5273 Some(p2p_addr.to_string().as_str())
5274 );
5275
5276 assert_eq!(proto.account, "0xabababababababababababababababababababab");
5277 assert_eq!(
5279 proto.stake,
5280 serde_json::to_value(stake).unwrap().as_str().unwrap()
5281 );
5282 assert_eq!(proto.commission, 1234);
5283 assert!(proto.authenticated);
5284 assert_eq!(proto.stake_table_key, None);
5285 assert_eq!(proto.state_ver_key, None);
5286 assert_eq!(
5287 proto
5288 .delegators
5289 .iter()
5290 .map(|delegator| (delegator.account.as_str(), delegator.amount.as_str()))
5291 .collect::<Vec<_>>(),
5292 [
5293 ("0x0101010101010101010101010101010101010101", "0xff"),
5294 ("0xffffffffffffffffffffffffffffffffffffffff", "0xa"),
5295 ]
5296 );
5297 }
5298
5299 #[test]
5302 fn sync_status_ranges_keep_their_bounds_and_status() {
5303 let converted = proto::ResourceSyncStatus::from(ResourceSyncStatus {
5304 missing: 7,
5305 ranges: vec![
5306 SyncStatusRange {
5307 start: 0,
5308 end: 3,
5309 status: SyncStatus::Pruned,
5310 },
5311 SyncStatusRange {
5312 start: 3,
5313 end: 5,
5314 status: SyncStatus::Present,
5315 },
5316 SyncStatusRange {
5317 start: 5,
5318 end: 12,
5319 status: SyncStatus::Missing,
5320 },
5321 ],
5322 });
5323
5324 assert_eq!(converted.missing, 7);
5325 let ranges: Vec<_> = converted
5326 .ranges
5327 .iter()
5328 .map(|range| (range.start, range.end, range.status()))
5329 .collect();
5330 assert_eq!(
5331 ranges,
5332 [
5333 (0, 3, proto::SyncStatus::Pruned),
5334 (3, 5, proto::SyncStatus::Present),
5335 (5, 12, proto::SyncStatus::Missing),
5336 ]
5337 );
5338 }
5339
5340 fn custom(status: StatusCode) -> hotshot_query_service::Error {
5341 hotshot_query_service::Error::Custom {
5342 message: "boom".into(),
5343 status,
5344 }
5345 }
5346
5347 #[tokio::test]
5351 async fn a_stream_ends_at_its_first_error() {
5352 let items = futures::stream::iter([
5353 Ok(1),
5354 Err(tonic::Status::internal("conversion failed")),
5355 Ok(2),
5356 ]);
5357 let delivered: Vec<_> = end_at_first_error(items).collect().await;
5358 assert_eq!(delivered.len(), 2);
5359 assert_eq!(delivered[0].as_ref().unwrap(), &1);
5360 assert!(delivered[1].is_err());
5361 }
5362
5363 #[test]
5364 fn range_at_limit_is_allowed() {
5365 let limit = small_object_range_limit();
5366 enforce_range(0, limit, limit).unwrap();
5367 enforce_range(3, limit + 3, limit).unwrap();
5368 }
5369
5370 #[test]
5371 fn range_past_limit_is_rejected() {
5372 for limit in [small_object_range_limit(), large_object_range_limit()] {
5373 let err = enforce_range(0, limit + 1, limit).unwrap_err();
5374 assert!(matches!(
5375 err.downcast_ref::<AvailabilityError>(),
5376 Some(AvailabilityError::RangeExceeded(_))
5377 ));
5378 }
5379 }
5380
5381 #[test]
5385 fn synthesized_certificates_convert_arm_by_arm() {
5386 use std::marker::PhantomData;
5387
5388 use committable::Commitment;
5389 use espresso_types::PubKey;
5390 use hotshot_types::{
5391 data::{EpochNumber, ViewChangeEvidence2, ViewNumber},
5392 simple_certificate::{
5393 TimeoutCertificate2, UpgradeCertificate, ViewSyncFinalizeCertificate2,
5394 },
5395 simple_vote::{TimeoutData2, UpgradeProposalData, ViewSyncFinalizeData2, Vote2Data},
5396 traits::signature_key::SignatureKey as _,
5397 };
5398 use proto::view_change_evidence2::Evidence;
5399
5400 let (_, private_key) = PubKey::generated_from_seed_indexed([7; 32], 0);
5401 let signature = PubKey::sign(&private_key, b"vote").unwrap();
5402 let mut signers = bitvec::vec::BitVec::<usize, bitvec::order::Lsb0>::repeat(false, 4);
5403 signers.set(1, true);
5404 signers.set(3, true);
5405
5406 let timeout = TimeoutCertificate2::<SeqTypes>::new(
5407 TimeoutData2 {
5408 view: ViewNumber::new(9),
5409 epoch: Some(EpochNumber::new(2)),
5410 },
5411 Commitment::from_raw([1; 32]),
5412 ViewNumber::new(9),
5413 Some((signature.clone(), signers)),
5414 PhantomData,
5415 );
5416 let Some(Evidence::Timeout(converted)) =
5417 proto::ViewChangeEvidence2::from(&ViewChangeEvidence2::Timeout(timeout)).evidence
5418 else {
5419 panic!("a timeout certificate must select the timeout arm");
5420 };
5421 let data = converted.data.unwrap();
5422 assert_eq!(data.view, 9);
5423 assert_eq!(data.epoch, Some(2));
5424 assert_eq!(converted.view_number, 9);
5425 assert_eq!(
5426 converted.vote_commitment,
5427 Commitment::<TimeoutData2>::from_raw([1; 32]).to_string()
5428 );
5429 let signatures = converted.signatures.unwrap();
5430 assert_eq!(signatures.signers, Vec::from([false, true, false, true]));
5431 assert_eq!(signatures.signature, signature.to_string());
5432 assert!(signatures.signature.starts_with("BLS_SIG~"));
5433 assert_eq!(
5434 serde_json::to_value(&signature).unwrap(),
5435 serde_json::Value::String(signatures.signature)
5436 );
5437
5438 let view_sync = ViewSyncFinalizeCertificate2::<SeqTypes>::new(
5439 ViewSyncFinalizeData2 {
5440 relay: 3,
5441 round: ViewNumber::new(10),
5442 epoch: None,
5443 },
5444 Commitment::from_raw([2; 32]),
5445 ViewNumber::new(10),
5446 None,
5447 PhantomData,
5448 );
5449 let Some(Evidence::ViewSync(converted)) =
5450 proto::ViewChangeEvidence2::from(&ViewChangeEvidence2::ViewSync(view_sync)).evidence
5451 else {
5452 panic!("a view sync certificate must select the view_sync arm");
5453 };
5454 let data = converted.data.unwrap();
5455 assert_eq!((data.relay, data.round, data.epoch), (3, 10, None));
5456 assert!(converted.signatures.is_none());
5457
5458 let upgrade = UpgradeCertificate::<SeqTypes>::new(
5459 UpgradeProposalData {
5460 old_version: vbs::version::Version { major: 0, minor: 3 },
5461 new_version: vbs::version::Version { major: 0, minor: 4 },
5462 decide_by: ViewNumber::new(20),
5463 new_version_hash: Vec::from([0xab, 0xcd]),
5464 old_version_last_view: ViewNumber::new(19),
5465 new_version_first_view: ViewNumber::new(21),
5466 },
5467 Commitment::from_raw([3; 32]),
5468 ViewNumber::new(15),
5469 None,
5470 PhantomData,
5471 );
5472 let data = proto::UpgradeCertificate::from(&upgrade).data.unwrap();
5473 assert_eq!(data.old_version.unwrap().minor, 3);
5474 assert_eq!(data.new_version.unwrap().minor, 4);
5475 assert_eq!(data.new_version_hash, Vec::from([0xab, 0xcd]));
5476 assert_eq!(
5477 (
5478 data.decide_by,
5479 data.old_version_last_view,
5480 data.new_version_first_view
5481 ),
5482 (20, 19, 21)
5483 );
5484
5485 let cert2 = Certificate2::<SeqTypes>::new(
5486 Vote2Data {
5487 leaf_commit: Commitment::from_raw([4; 32]),
5488 epoch: EpochNumber::new(5),
5489 block_number: 77,
5490 },
5491 Commitment::from_raw([5; 32]),
5492 ViewNumber::new(30),
5493 None,
5494 PhantomData,
5495 );
5496 let converted = proto::Certificate2::from(&cert2);
5497 let data = converted.data.unwrap();
5498 assert_eq!(
5499 data.leaf_commit,
5500 Commitment::<hotshot_types::data::Leaf2<SeqTypes>>::from_raw([4; 32]).to_string()
5501 );
5502 assert_eq!((data.epoch, data.block_number), (5, 77));
5503 assert_eq!(converted.view_number, 30);
5504 }
5505
5506 #[test]
5507 fn namespace_proofs_mirror_the_reference_vectors() {
5508 use base64::Engine as _;
5509 use proto::ns_proof::Proof;
5510 let b64 = base64::engine::general_purpose::STANDARD;
5511
5512 fn check_transactions(converted: &proto::NamespaceProofResponse, json: &serde_json::Value) {
5513 let expected = json["transactions"].as_array().unwrap();
5514 assert_eq!(converted.transactions.len(), expected.len());
5515 for (tx, expected) in converted.transactions.iter().zip(expected) {
5516 assert_eq!(tx.namespace, expected["namespace"].as_u64().unwrap());
5517 assert_eq!(
5518 base64::engine::general_purpose::STANDARD.encode(&tx.payload),
5519 expected["payload"].as_str().unwrap()
5520 );
5521 }
5522 }
5523
5524 let (reference, json): (NamespaceProofQueryData, _) =
5525 load_vector("../../../data/v3/ns_proof_V0.json");
5526 assert_same_fields("NamespaceProofResponse", &json);
5527 let expected = &json["proof"]["V0"];
5528 assert_same_fields("AdvzNsProof", expected);
5529 let converted = proto::NamespaceProofResponse::try_from(&reference).unwrap();
5530 check_transactions(&converted, &json);
5531 let Some(Proof::V0(advz)) = converted.proof.unwrap().proof else {
5532 panic!("the ADVZ vector must select the v0 arm");
5533 };
5534 assert_eq!(advz.ns_index, json_bytes(&expected["ns_index"]));
5535 assert_eq!(
5536 b64.encode(&advz.ns_payload),
5537 expected["ns_payload"].as_str().unwrap()
5538 );
5539 let range_proof = advz.ns_proof.unwrap();
5540 let expected_proof = &expected["ns_proof"];
5541 assert_same_fields("LargeRangeProof", expected_proof);
5542 assert_eq!(range_proof.prefix_elems, expected_proof["prefix_elems"]);
5543 assert_eq!(range_proof.suffix_elems, expected_proof["suffix_elems"]);
5544 assert_eq!(
5545 range_proof.prefix_bytes,
5546 json_bytes(&expected_proof["prefix_bytes"])
5547 );
5548 assert_eq!(
5549 range_proof.suffix_bytes,
5550 json_bytes(&expected_proof["suffix_bytes"])
5551 );
5552
5553 for (path, arm) in [
5554 ("../../../data/v4/ns_proof_V1.json", "V1"),
5555 ("../../../data/v6/ns_proof_V2.json", "V2"),
5556 ] {
5557 let (reference, json): (NamespaceProofQueryData, _) = load_vector(path);
5558 let expected = &json["proof"][arm];
5559 assert_same_fields("NsProofPayload", expected);
5560 let converted = proto::NamespaceProofResponse::try_from(&reference).unwrap();
5561 check_transactions(&converted, &json);
5562 let payload = match (arm, converted.proof.unwrap().proof) {
5563 ("V1", Some(Proof::V1(payload))) | ("V2", Some(Proof::V2(payload))) => payload,
5564 (arm, other) => panic!("the {arm} vector selected the wrong arm: {other:?}"),
5565 };
5566 assert_eq!(payload.ns_index, expected["ns_index"].as_u64().unwrap());
5567 assert_eq!(
5568 b64.encode(&payload.ns_payload),
5569 expected["ns_payload"].as_str().unwrap()
5570 );
5571 assert_eq!(payload.ns_proof, expected["ns_proof"]);
5572 }
5573 }
5574
5575 #[test]
5579 fn bad_encoding_namespace_proof_mirrors_its_v1_rendering() {
5580 let empty = tagged_base64::TaggedBase64::new("FIELD", &0u64.to_le_bytes())
5583 .unwrap()
5584 .to_string();
5585 let commitment = reference_header("v3").1["payload_commitment"]
5586 .as_str()
5587 .unwrap()
5588 .to_string();
5589 let merkle_proof: serde_json::Value = serde_json::from_str(
5590 &std::fs::read_to_string("../../../data/v3/ns_proof_V1.json").unwrap(),
5591 )
5592 .unwrap();
5593 let merkle_proof = merkle_proof["proof"]["V1"]["ns_proof"].as_str().unwrap();
5594
5595 let json = serde_json::json!({
5596 "ns_index": 1,
5597 "ns_commit": commitment,
5598 "ns_mt_proof": merkle_proof,
5599 "ns_proof": { "recovered_poly": empty, "raw_shares": empty },
5600 });
5601 let reference: espresso_types::v0_3::AvidMIncorrectEncodingNsProof =
5602 serde_json::from_value(json.clone()).unwrap();
5603 assert_eq!(
5604 serde_json::to_value(&reference).unwrap(),
5605 json,
5606 "v1 no longer renders the proof the way this test claims"
5607 );
5608
5609 let converted = proto::AvidmBadEncodingNsProof::try_from(&reference).unwrap();
5610 assert_eq!(converted.ns_index, 1);
5611 assert_eq!(converted.ns_commit, commitment);
5612 assert_eq!(converted.ns_mt_proof, merkle_proof);
5613 let inner = converted.ns_proof.unwrap();
5614 assert_eq!(inner.recovered_poly, empty);
5615 assert_eq!(inner.raw_shares, empty);
5616 }
5617
5618 #[test]
5620 fn state_certs_mirror_the_reference_vectors() {
5621 let json: serde_json::Value = serde_json::from_str(
5622 &std::fs::read_to_string("../../../data/v3/state_cert.json").unwrap(),
5623 )
5624 .unwrap();
5625 let reference: espresso_types::v0_3::StateCertQueryDataV1<SeqTypes> =
5626 serde_json::from_value(json.clone()).unwrap();
5627 assert_same_fields("StateCertV1Response", &json);
5628 let converted = proto::StateCertV1Response::from(&reference);
5629 assert_eq!(converted.epoch, json["epoch"].as_u64().unwrap());
5630 assert_eq!(converted.light_client_state, json["light_client_state"]);
5631 assert_eq!(
5632 converted.next_stake_table_state,
5633 json["next_stake_table_state"]
5634 );
5635 assert_eq!(
5636 converted.signatures.len(),
5637 json["signatures"].as_array().unwrap().len()
5638 );
5639
5640 let json: serde_json::Value = serde_json::from_str(
5641 &std::fs::read_to_string("../../../data/v4/state_cert.json").unwrap(),
5642 )
5643 .unwrap();
5644 let reference: espresso_types::v0_4::StateCertQueryDataV2<SeqTypes> =
5645 serde_json::from_value(json.clone()).unwrap();
5646 assert_same_fields("StateCertV2Response", &json);
5647 let converted = proto::StateCertV2Response::from(&reference);
5648 assert_eq!(converted.epoch, json["epoch"].as_u64().unwrap());
5649 assert_eq!(converted.light_client_state, json["light_client_state"]);
5650 assert_eq!(converted.auth_root, json["auth_root"]);
5651 assert_eq!(
5652 converted.signatures.len(),
5653 json["signatures"].as_array().unwrap().len()
5654 );
5655 }
5656
5657 #[test]
5660 fn transaction_with_proof_mirrors_the_reference_vector() {
5661 use base64::Engine as _;
5662 use proto::tx_proof::Proof;
5663
5664 let json: serde_json::Value = serde_json::from_str(
5665 &std::fs::read_to_string("../../../data/v1/transaction_query_data.json").unwrap(),
5666 )
5667 .unwrap();
5668 let reference: Vec<TransactionWithProofQueryData<SeqTypes>> =
5669 serde_json::from_value(json.clone()).unwrap();
5670 let first = &json[0];
5671 assert_same_fields("TransactionWithProofResponse", first);
5672
5673 let converted = proto::TransactionWithProofResponse::try_from(&reference[0]).unwrap();
5674 assert_eq!(converted.hash, first["hash"]);
5675 assert_eq!(converted.index, first["index"].as_u64().unwrap());
5676 assert_eq!(converted.block_hash, first["block_hash"]);
5677 assert_eq!(
5678 converted.block_height,
5679 first["block_height"].as_u64().unwrap()
5680 );
5681 assert_eq!(converted.namespace, first["namespace"].as_u64().unwrap());
5682 assert_eq!(
5683 u64::from(converted.pos_in_namespace),
5684 first["pos_in_namespace"].as_u64().unwrap()
5685 );
5686 let transaction = converted.transaction.unwrap();
5687 assert_eq!(
5688 transaction.namespace,
5689 first["transaction"]["namespace"].as_u64().unwrap()
5690 );
5691 assert_eq!(
5692 base64::engine::general_purpose::STANDARD.encode(&transaction.payload),
5693 first["transaction"]["payload"].as_str().unwrap()
5694 );
5695
5696 let Some(Proof::V1(proof)) = converted.proof.unwrap().proof else {
5697 panic!("the AvidM vector must select the v1 proof arm");
5698 };
5699 let expected = &first["proof"]["V1"];
5700 assert_same_fields("AvidmTxProof", expected);
5701 assert_eq!(proof.tx_index, json_bytes(&expected["tx_index"]));
5702 let ns_proof = proof.ns_proof.unwrap();
5703 let expected_ns = &expected["ns_proof"];
5704 assert_same_fields("NsProofPayload", expected_ns);
5705 assert_eq!(ns_proof.ns_index, expected_ns["ns_index"].as_u64().unwrap());
5706 assert_eq!(
5707 base64::engine::general_purpose::STANDARD.encode(&ns_proof.ns_payload),
5708 expected_ns["ns_payload"].as_str().unwrap()
5709 );
5710 assert_eq!(ns_proof.ns_proof, expected_ns["ns_proof"]);
5711 }
5712
5713 #[test]
5714 fn vid_common_mirrors_the_reference_vectors() {
5715 use proto::vid_common::Common;
5716
5717 let (reference, json): (VidCommonQueryData<SeqTypes>, _) =
5718 load_vector("../../../data/v1/vid_common_v0.json");
5719 assert_same_fields("VidCommonResponse", &json);
5720 assert_same_fields("AdvzCommon", &json["common"]["V0"]);
5721 let converted = proto::VidCommonResponse::try_from(&reference).unwrap();
5722 assert_eq!(converted.height, json["height"].as_u64().unwrap());
5723 assert_eq!(converted.block_hash, json["block_hash"]);
5724 assert_eq!(converted.payload_hash, json["payload_hash"]);
5725 let Some(Common::V0(advz)) = converted.common.unwrap().common else {
5726 panic!("the ADVZ vector must select the v0 arm");
5727 };
5728 let expected = &json["common"]["V0"];
5729 assert_eq!(advz.poly_commits, expected["poly_commits"]);
5730 assert_eq!(advz.all_evals_digest, expected["all_evals_digest"]);
5731 assert_eq!(
5732 u64::from(advz.payload_byte_len),
5733 expected["payload_byte_len"].as_u64().unwrap()
5734 );
5735 assert_eq!(
5736 u64::from(advz.num_storage_nodes),
5737 expected["num_storage_nodes"].as_u64().unwrap()
5738 );
5739 assert_eq!(
5740 u64::from(advz.multiplicity),
5741 expected["multiplicity"].as_u64().unwrap()
5742 );
5743
5744 let (reference, json): (VidCommonQueryData<SeqTypes>, _) =
5745 load_vector("../../../data/v1/vid_common_v1.json");
5746 assert_same_fields("AvidmCommon", &json["common"]["V1"]);
5747 let Some(Common::V1(avidm)) = proto::VidCommonResponse::try_from(&reference)
5748 .unwrap()
5749 .common
5750 .unwrap()
5751 .common
5752 else {
5753 panic!("the AvidM vector must select the v1 arm");
5754 };
5755 let expected = &json["common"]["V1"];
5756 assert_eq!(
5757 avidm.total_weights,
5758 expected["total_weights"].as_u64().unwrap()
5759 );
5760 assert_eq!(
5761 avidm.recovery_threshold,
5762 expected["recovery_threshold"].as_u64().unwrap()
5763 );
5764
5765 let (reference, json): (VidCommonQueryData<SeqTypes>, _) =
5766 load_vector("../../../data/v2/vid_common_v2.json");
5767 assert_same_fields("AvidmGf2Common", &json["common"]["V2"]);
5768 let Some(Common::V2(gf2)) = proto::VidCommonResponse::try_from(&reference)
5769 .unwrap()
5770 .common
5771 .unwrap()
5772 .common
5773 else {
5774 panic!("the AvidmGf2 vector must select the v2 arm");
5775 };
5776 let expected = &json["common"]["V2"];
5777 let param = gf2.param.unwrap();
5778 assert_eq!(
5779 param.total_weights,
5780 expected["param"]["total_weights"].as_u64().unwrap()
5781 );
5782 assert_eq!(
5783 param.recovery_threshold,
5784 expected["param"]["recovery_threshold"].as_u64().unwrap()
5785 );
5786 let expected_commits: Vec<&str> = expected["ns_commits"]
5787 .as_array()
5788 .unwrap()
5789 .iter()
5790 .map(|commit| commit.as_str().unwrap())
5791 .collect();
5792 assert_eq!(gf2.ns_commits, expected_commits);
5793 let expected_lens: Vec<u64> = expected["ns_lens"]
5794 .as_array()
5795 .unwrap()
5796 .iter()
5797 .map(|len| len.as_u64().unwrap())
5798 .collect();
5799 assert_eq!(gf2.ns_lens, expected_lens);
5800 }
5801
5802 #[test]
5805 fn block_and_payload_mirror_the_reference_vectors() {
5806 use base64::Engine as _;
5807 let b64 = base64::engine::general_purpose::STANDARD;
5808
5809 let json: serde_json::Value = serde_json::from_str(
5810 &std::fs::read_to_string("../../../data/v1/block_query_data.json").unwrap(),
5811 )
5812 .unwrap();
5813 let reference: BlockQueryData<SeqTypes> = serde_json::from_value(json.clone()).unwrap();
5814 let block = proto::BlockResponse::from(&reference);
5815 assert_same_fields("BlockResponse", &json);
5816 assert_eq!(block.hash, json["hash"]);
5817 assert_eq!(block.size, json["size"].as_u64().unwrap());
5818 assert_eq!(
5819 block.num_transactions,
5820 json["num_transactions"].as_u64().unwrap()
5821 );
5822 let Some(proto::header_response::Header::V1(header)) = block.header.unwrap().header else {
5823 panic!("an unwrapped 0.1-shaped header must convert to the V1 arm");
5824 };
5825 assert_eq!(header.height, json["header"]["height"].as_u64().unwrap());
5826 let payload = block.payload.unwrap();
5827 assert_eq!(
5828 b64.encode(&payload.raw_payload),
5829 json["payload"]["raw_payload"].as_str().unwrap()
5830 );
5831 assert_eq!(
5832 b64.encode(&payload.ns_table.unwrap().bytes),
5833 json["payload"]["ns_table"]["bytes"].as_str().unwrap()
5834 );
5835
5836 let json: serde_json::Value = serde_json::from_str(
5837 &std::fs::read_to_string("../../../data/v1/payload_query_data.json").unwrap(),
5838 )
5839 .unwrap();
5840 let reference: PayloadQueryData<SeqTypes> = serde_json::from_value(json.clone()).unwrap();
5841 let payload = proto::PayloadResponse::from(&reference);
5842 assert_same_fields("PayloadResponse", &json);
5843 assert_eq!(payload.height, json["height"].as_u64().unwrap());
5844 assert_eq!(payload.block_hash, json["block_hash"]);
5845 assert_eq!(payload.hash, json["hash"]);
5846 assert_eq!(payload.size, json["size"].as_u64().unwrap());
5847 let data = payload.data.unwrap();
5848 assert_eq!(
5849 b64.encode(&data.raw_payload),
5850 json["data"]["raw_payload"].as_str().unwrap()
5851 );
5852 assert_eq!(
5853 b64.encode(&data.ns_table.unwrap().bytes),
5854 json["data"]["ns_table"]["bytes"].as_str().unwrap()
5855 );
5856 }
5857
5858 #[test]
5861 fn block_summary_mirrors_its_v1_rendering() {
5862 let json: serde_json::Value = serde_json::from_str(
5863 &std::fs::read_to_string("../../../data/v1/block_query_data.json").unwrap(),
5864 )
5865 .unwrap();
5866 let block: BlockQueryData<SeqTypes> = serde_json::from_value(json).unwrap();
5867 let summary = BlockSummaryQueryData::from(block);
5868 let expected = serde_json::to_value(&summary).unwrap();
5869 let converted = proto::BlockSummaryResponse::from(&summary);
5870
5871 assert_same_fields("BlockSummaryResponse", &expected);
5872 assert_eq!(converted.hash, expected["hash"]);
5873 assert_eq!(converted.size, expected["size"].as_u64().unwrap());
5874 assert_eq!(
5875 converted.num_transactions,
5876 expected["num_transactions"].as_u64().unwrap()
5877 );
5878
5879 let expected_namespaces = expected["namespaces"].as_object().unwrap();
5882 assert!(
5883 expected_namespaces.len() > 1,
5884 "the vector should span several namespaces"
5885 );
5886 assert_eq!(converted.namespaces.len(), expected_namespaces.len());
5887 let mut counted = 0;
5888 for (namespace, expected_info) in expected_namespaces {
5889 let info = &converted.namespaces[&namespace.parse::<u64>().unwrap()];
5890 assert_eq!(
5891 info.num_transactions,
5892 expected_info["num_transactions"].as_u64().unwrap()
5893 );
5894 assert_eq!(info.size, expected_info["size"].as_u64().unwrap());
5895 counted += info.num_transactions;
5896 }
5897 assert_eq!(counted, converted.num_transactions);
5898 }
5899
5900 #[test]
5901 fn leaf_mirrors_the_reference_vector() {
5902 let json: serde_json::Value = serde_json::from_str(
5903 &std::fs::read_to_string("../../../data/v3/leaf_query_data.json").unwrap(),
5904 )
5905 .unwrap();
5906 let reference: LeafQueryData<SeqTypes> = serde_json::from_value(json.clone()).unwrap();
5907 let converted = proto::LeafResponse::from(&reference);
5908
5909 assert_same_fields("Leaf2", &json["leaf"]);
5910 assert_same_fields("QuorumCertificate2", &json["qc"]);
5911
5912 let leaf = converted.leaf.unwrap();
5913 let expected = &json["leaf"];
5914 assert_eq!(leaf.view_number, expected["view_number"].as_u64().unwrap());
5915 assert_eq!(leaf.parent_commitment, expected["parent_commitment"]);
5916 assert_eq!(leaf.with_epoch, expected["with_epoch"].as_bool().unwrap());
5917 assert!(leaf.next_epoch_justify_qc.is_none());
5918 assert!(leaf.upgrade_certificate.is_none());
5919 assert!(leaf.view_change_evidence.is_none());
5920 assert!(leaf.next_drb_result.is_empty());
5921
5922 let header_json = &expected["block_header"];
5925 assert!(
5926 header_json.get("fields").is_none(),
5927 "vector header grew a version wrapper; update the expected arm"
5928 );
5929 let Some(proto::header_response::Header::V1(header)) = leaf.block_header.unwrap().header
5930 else {
5931 panic!("an unwrapped 0.1-shaped header must convert to the V1 arm");
5932 };
5933 assert_eq!(header.height, header_json["height"].as_u64().unwrap());
5934
5935 assert!(leaf.block_payload.is_none());
5938
5939 let justify = leaf.justify_qc.unwrap();
5940 let expected_justify = &expected["justify_qc"];
5941 assert_eq!(
5942 justify.view_number,
5943 expected_justify["view_number"].as_u64().unwrap()
5944 );
5945 assert_eq!(justify.vote_commitment, expected_justify["vote_commitment"]);
5946 assert_eq!(
5947 justify.data.as_ref().unwrap().leaf_commit,
5948 expected_justify["data"]["leaf_commit"]
5949 );
5950
5951 let qc = converted.qc.unwrap();
5952 let expected_qc = &json["qc"];
5953 assert_eq!(qc.view_number, expected_qc["view_number"].as_u64().unwrap());
5954 assert_eq!(qc.vote_commitment, expected_qc["vote_commitment"]);
5955 let data = qc.data.unwrap();
5956 assert_eq!(data.leaf_commit, expected_qc["data"]["leaf_commit"]);
5957 assert_eq!(data.epoch, expected_qc["data"]["epoch"].as_u64());
5958 assert_eq!(
5959 data.block_number,
5960 expected_qc["data"]["block_number"].as_u64()
5961 );
5962 assert!(qc.signatures.is_none());
5964 }
5965
5966 #[test]
5970 fn v1_header_mirrors_the_reference_vector() {
5971 let (header, fields) = reference_header("v1");
5972 assert_same_fields("HeaderV1", &fields);
5973 let proto::HeaderResponse { header: converted } = (&header).into();
5974 let Some(proto::header_response::Header::V1(converted)) = converted else {
5975 panic!("a 0.1 header must convert to the V1 arm, got {converted:?}");
5976 };
5977
5978 assert_eq!(converted.height, fields["height"].as_u64().unwrap());
5979 assert_eq!(converted.timestamp, fields["timestamp"].as_u64().unwrap());
5980 assert_eq!(converted.l1_head, fields["l1_head"].as_u64().unwrap());
5981 assert_eq!(
5982 converted.payload_commitment,
5983 fields["payload_commitment"].as_str().unwrap()
5984 );
5985 let fee_info = converted.fee_info.unwrap();
5986 assert_eq!(fee_info.account, fields["fee_info"]["account"]);
5987 assert_eq!(fee_info.amount, fields["fee_info"]["amount"]);
5988 }
5989
5990 #[test]
5991 fn ranges_within_limits_are_allowed() {
5992 let ranges = validate_ranges(vec![0..5, 10..12], 100).unwrap();
5993 assert_eq!(ranges, [0..5, 10..12]);
5994 validate_ranges(vec![], 100).unwrap();
5995 }
5996
5997 #[test]
5998 fn oversized_or_empty_ranges_are_rejected() {
5999 let err = validate_ranges(vec![0..60, 100..160], 100).unwrap_err();
6001 assert!(matches!(
6002 err.downcast_ref::<AvailabilityError>(),
6003 Some(AvailabilityError::RangeExceeded(_))
6004 ));
6005
6006 let many = (0..101u64).map(|i| i * 2..i * 2 + 1).collect();
6008 let err = validate_ranges(many, 100).unwrap_err();
6009 assert!(matches!(
6010 err.downcast_ref::<AvailabilityError>(),
6011 Some(AvailabilityError::RangeExceeded(_))
6012 ));
6013
6014 #[allow(clippy::single_range_in_vec_init)]
6016 let err = validate_ranges(vec![5..5], 100).unwrap_err();
6017 assert!(matches!(
6018 err.downcast_ref::<AvailabilityError>(),
6019 Some(AvailabilityError::BadRequest(_))
6020 ));
6021
6022 let err = validate_ranges(vec![0..100, 0..u64::MAX], 100).unwrap_err();
6024 assert!(matches!(
6025 err.downcast_ref::<AvailabilityError>(),
6026 Some(AvailabilityError::BadRequest(_) | AvailabilityError::RangeExceeded(_))
6027 ));
6028 }
6029
6030 #[test]
6031 fn unordered_ranges_are_rejected() {
6032 validate_ranges(vec![0..5, 5..7], 100).unwrap();
6034 for ranges in [vec![5..7, 0..5], vec![0..5, 3..7]] {
6035 let err = validate_ranges(ranges, 100).unwrap_err();
6036 assert!(matches!(
6037 err.downcast_ref::<AvailabilityError>(),
6038 Some(AvailabilityError::BadRequest(_))
6039 ));
6040 }
6041 }
6042
6043 #[test]
6047 fn range_limits_track_known_defaults() {
6048 assert_eq!(small_object_range_limit(), 500);
6049 assert_eq!(large_object_range_limit(), 100);
6050 assert_eq!(node_window_limit(), 500);
6051 }
6052
6053 #[test]
6056 fn lc_error_preserves_bad_request() {
6057 let err = lc_error(custom(StatusCode::BAD_REQUEST));
6058 assert!(matches!(
6059 err.downcast_ref::<AvailabilityError>(),
6060 Some(AvailabilityError::BadRequest(_))
6061 ));
6062 }
6063
6064 #[test]
6065 fn lc_error_preserves_not_found() {
6066 let err = lc_error(custom(StatusCode::NOT_FOUND));
6067 assert!(matches!(
6068 err.downcast_ref::<AvailabilityError>(),
6069 Some(AvailabilityError::NotFound(_))
6070 ));
6071 }
6072
6073 #[test]
6074 fn lc_error_preserves_gone() {
6075 let err = lc_error(custom(StatusCode::GONE));
6076 assert!(matches!(
6077 err.downcast_ref::<AvailabilityError>(),
6078 Some(AvailabilityError::Pruned(_))
6079 ));
6080 }
6081
6082 #[test]
6083 fn lc_error_other_statuses_stay_internal() {
6084 let err = lc_error(custom(StatusCode::INTERNAL_SERVER_ERROR));
6085 assert!(err.downcast_ref::<AvailabilityError>().is_none());
6086 }
6087
6088 #[test]
6090 fn explorer_error_preserves_not_found() {
6091 let err = explorer_error(ExplorerError::GetBlockDetail(QueryError::NotFound.into()));
6092 assert!(matches!(
6093 err.downcast_ref::<AvailabilityError>(),
6094 Some(AvailabilityError::NotFound(_))
6095 ));
6096 }
6097
6098 #[test]
6099 fn explorer_error_preserves_bad_request() {
6100 let err = explorer_error(ExplorerError::GetSearchResults(
6101 GetSearchResultsError::InvalidQuery(BadQuery {}),
6102 ));
6103 assert!(matches!(
6104 err.downcast_ref::<AvailabilityError>(),
6105 Some(AvailabilityError::BadRequest(_))
6106 ));
6107 }
6108
6109 #[rstest::rstest]
6110 #[case::epochs_not_started(StateCertFetchError::EpochsNotStarted { epoch: 1 })]
6111 #[case::before_first_epoch(StateCertFetchError::BeforeFirstEpoch { epoch: 0, first_epoch: 1 })]
6112 #[case::root_not_decided(StateCertFetchError::RootNotDecided { epoch: 3, decided_height: 100 })]
6113 fn state_cert_that_cannot_exist_is_not_found(#[case] err: StateCertFetchError) {
6114 let err = state_cert_error(err);
6115 assert!(
6116 matches!(
6117 err.downcast_ref::<AvailabilityError>(),
6118 Some(AvailabilityError::NotFound(_))
6119 ),
6120 "{err:#}"
6121 );
6122 }
6123
6124 #[rstest::rstest]
6125 #[case::fetch(StateCertFetchError::FetchError(anyhow::anyhow!("timeout")))]
6126 #[case::validation(StateCertFetchError::ValidationError(anyhow::anyhow!("bad")))]
6127 #[case::other(StateCertFetchError::Other(anyhow::anyhow!("catchup")))]
6128 fn state_cert_fetch_failure_is_internal(#[case] err: StateCertFetchError) {
6129 let err = state_cert_error(err);
6130 assert!(err.downcast_ref::<AvailabilityError>().is_none(), "{err:#}");
6131 }
6132
6133 #[tokio::test(flavor = "multi_thread")]
6134 async fn advz_transaction_proof_mirrors_its_v1_rendering() {
6135 use hotshot_query_service::availability::QueryablePayload;
6136 use hotshot_types::{
6137 data::VidCommon,
6138 traits::{EncodeBytes, block_contents::BlockPayload},
6139 vid::advz::advz_scheme,
6140 };
6141 use jf_advz::VidScheme;
6142 use proto::tx_proof::Proof;
6143
6144 let namespace = espresso_types::NamespaceId::from(7_u32);
6149 let transactions: Vec<_> = (1_u8..=3)
6150 .map(|byte| espresso_types::Transaction::new(namespace, vec![byte; 8]))
6151 .collect();
6152 let (payload, _) = espresso_types::Payload::from_transactions(
6153 &transactions,
6154 &Default::default(),
6155 &Default::default(),
6156 )
6157 .await
6158 .unwrap();
6159 let common = VidCommon::V0(advz_scheme(10).disperse(payload.encode()).unwrap().common);
6160 let index = payload.iter(payload.ns_table()).next().unwrap();
6161 let (_, proof) = espresso_types::TxProof::new(&index, &payload, &common).unwrap();
6162 let rendered = serde_json::to_value(&proof).unwrap();
6163 let rendered = &rendered["V0"];
6164
6165 let Some(Proof::V0(converted)) = proto::TxProof::try_from(&proof).unwrap().proof else {
6166 panic!("an ADVZ common yields the V0 arm");
6167 };
6168 assert_eq!(converted.tx_index, json_bytes(&rendered["tx_index"]));
6169 assert_eq!(
6170 converted.payload_num_txs,
6171 json_bytes(&rendered["payload_num_txs"])
6172 );
6173 assert_eq!(
6174 converted.payload_tx_table_entries,
6175 json_bytes(&rendered["payload_tx_table_entries"])
6176 );
6177 for (proof, key) in [
6178 (converted.payload_proof_num_txs, "payload_proof_num_txs"),
6179 (
6180 converted.payload_proof_tx_table_entries,
6181 "payload_proof_tx_table_entries",
6182 ),
6183 (converted.payload_proof_tx, "payload_proof_tx"),
6184 ] {
6185 let Some(proof) = proof else {
6186 assert!(
6187 rendered[key].is_null(),
6188 "{key} is present in v1's rendering"
6189 );
6190 continue;
6191 };
6192 assert_eq!(
6193 proof.proofs,
6194 rendered[key]["proofs"].as_str().unwrap(),
6195 "{key}"
6196 );
6197 assert_eq!(
6198 proof.prefix_bytes,
6199 json_bytes(&rendered[key]["prefix_bytes"]),
6200 "{key}"
6201 );
6202 assert_eq!(
6203 proof.suffix_bytes,
6204 json_bytes(&rendered[key]["suffix_bytes"]),
6205 "{key}"
6206 );
6207 }
6208 }
6209
6210 #[tokio::test]
6215 async fn runtime_config_mirrors_public_node_config() {
6216 use proto::config_service_server::ConfigService as _;
6217
6218 use crate::options::{
6219 FollowerConfig, Identity, PublicNodeConfig,
6220 tests::{parse_options_with, test_genesis},
6221 };
6222
6223 struct UnusedDataSource;
6224
6225 impl HotShotConfigDataSource for UnusedDataSource {
6226 async fn get_config(&self) -> espresso_types::config::PublicNetworkConfig {
6227 unreachable!("the runtime config is served from the state, not the data source")
6228 }
6229 }
6230
6231 let opt = parse_options_with(&[
6232 "--config-peers",
6233 "https://peer1.test,https://peer2.test",
6234 "--cliquenet-bind-address",
6235 "[2001:db8::1]:9999",
6236 "--",
6237 "http",
6238 "--port",
6239 "24000",
6240 "--",
6241 "query",
6242 "--peers",
6243 "https://query1.test,https://query2.test",
6244 "--light-client-db-num-connections",
6245 "7",
6246 "--light-client-db-num-leaves",
6247 "11",
6248 "--",
6249 "config",
6250 ]);
6251 let mut cfg = PublicNodeConfig::new(&opt, &opt.modules(), &test_genesis());
6252 cfg.follower = Some(FollowerConfig {
6254 poll_interval: Duration::from_millis(2500),
6255 max_blocks_per_poll: 17,
6256 });
6257 cfg.identity = Identity {
6258 node_name: Some("node-name".into()),
6259 node_description: Some("node-description".into()),
6260 company_name: Some("company-name".into()),
6261 company_website: Some("https://company.test/".parse().unwrap()),
6262 country_code: Some("DE".into()),
6263 latitude: Some(1.5),
6264 longitude: Some(-2.5),
6265 operating_system: Some("operating-system".into()),
6266 node_type: Some("node-type".into()),
6267 network_type: Some("network-type".into()),
6268 icon_14x14_1x: Some("https://icons.test/14/1".parse().unwrap()),
6269 icon_14x14_2x: Some("https://icons.test/14/2".parse().unwrap()),
6270 icon_14x14_3x: Some("https://icons.test/14/3".parse().unwrap()),
6271 icon_24x24_1x: Some("https://icons.test/24/1".parse().unwrap()),
6272 icon_24x24_2x: Some("https://icons.test/24/2".parse().unwrap()),
6273 icon_24x24_3x: Some("https://icons.test/24/3".parse().unwrap()),
6274 };
6275
6276 let state = NodeApiStateImpl::new(std::sync::Arc::new(UnusedDataSource))
6277 .with_public_node_config(Some(cfg.clone()));
6278 let runtime = state
6279 .get_runtime_config(tonic::Request::new(proto::GetRuntimeConfigRequest {}))
6280 .await
6281 .unwrap()
6282 .into_inner();
6283
6284 fn strings<T: ToString>(values: &[T]) -> Vec<String> {
6285 values.iter().map(ToString::to_string).collect()
6286 }
6287
6288 assert_eq!(
6289 runtime,
6290 proto::RuntimeConfigResponse {
6291 is_da: cfg.is_da,
6292 identity: Some(proto::NodeIdentity {
6293 node_name: Some("node-name".into()),
6294 node_description: Some("node-description".into()),
6295 company_name: Some("company-name".into()),
6296 company_website: Some("https://company.test/".into()),
6297 country_code: Some("DE".into()),
6298 latitude: Some(1.5),
6299 longitude: Some(-2.5),
6300 operating_system: Some("operating-system".into()),
6301 node_type: Some("node-type".into()),
6302 network_type: Some("network-type".into()),
6303 icon_14x14_1x: Some("https://icons.test/14/1".into()),
6304 icon_14x14_2x: Some("https://icons.test/14/2".into()),
6305 icon_14x14_3x: Some("https://icons.test/14/3".into()),
6306 icon_24x24_1x: Some("https://icons.test/24/1".into()),
6307 icon_24x24_2x: Some("https://icons.test/24/2".into()),
6308 icon_24x24_3x: Some("https://icons.test/24/3".into()),
6309 }),
6310 storage: Some(proto::NodeStorage {
6311 backend: proto::StorageBackend::FsDefault as i32,
6312 fs: cfg.storage.fs.as_ref().map(|fs| proto::FsStorage {
6315 path: fs.path.display().to_string(),
6316 consensus_view_retention: fs.consensus_view_retention,
6317 }),
6318 sql: None,
6319 }),
6320 genesis_file: cfg.genesis_file.to_string(),
6321 public_api_url: cfg.public_api_url.as_ref().map(ToString::to_string),
6322 builder_urls: strings(&cfg.builder_urls),
6323 state_relay_server_url: cfg.state_relay_server_url.to_string(),
6324 state_peers: strings(&cfg.state_peers),
6325 config_peers: strings(cfg.config_peers.as_deref().unwrap()),
6326 orchestrator_url: cfg.orchestrator_url.to_string(),
6327 cliquenet_bind_address: cfg.cliquenet_bind_address.unbracketed_string(),
6328 cliquenet_advertise_address: cfg
6329 .cliquenet_advertise_address
6330 .as_ref()
6331 .map(|addr| addr.unbracketed_string()),
6332 l1_provider_count: cfg.l1_provider_count as u64,
6333 l1_ws_provider_count: cfg.l1_ws_provider_count as u64,
6334 follower: Some(proto::FollowerModule {
6335 poll_interval_ms: 2500,
6336 max_blocks_per_poll: 17,
6337 }),
6338 modules: Some(proto::ApiModules {
6339 http: Some(proto::HttpModule {
6340 port: 24000,
6341 max_connections: None,
6342 tonic_port: None,
6343 }),
6344 query: Some(proto::QueryModule {
6345 peers: vec![
6346 "https://query1.test/".to_string(),
6347 "https://query2.test/".to_string(),
6348 ],
6349 light_client: Some(proto::LightClientModuleOptions {
6350 num_stake_tables_in_memory: cfg
6351 .modules
6352 .query
6353 .as_ref()
6354 .unwrap()
6355 .light_client
6356 .num_stake_tables_in_memory
6357 as u64,
6358 }),
6359 light_client_db: Some(proto::LightClientDbOptions {
6362 num_connections: 7,
6363 num_leaves: 11,
6364 lc_path: cfg
6366 .modules
6367 .query
6368 .as_ref()
6369 .unwrap()
6370 .light_client_db
6371 .lc_path
6372 .as_ref()
6373 .map(|path| path.display().to_string()),
6374 }),
6375 }),
6376 submit: false,
6377 status: false,
6378 catchup: false,
6379 config: true,
6380 hotshot_events: false,
6381 explorer: false,
6382 light_client: false,
6383 }),
6384 ..Default::default()
6385 }
6386 );
6387 assert_eq!(runtime.config_peers.len(), 2);
6390 assert_eq!(runtime.cliquenet_bind_address, "2001:db8::1:9999");
6391 }
6392
6393 #[test]
6396 fn hotshot_config_renders_the_fields_a_test_network_leaves_empty() {
6397 use espresso_types::config::PublicNetworkConfig;
6398 use hotshot_types::{
6399 PeerConfig, VersionedDaCommittee,
6400 network::{BuilderType, NetworkConfig},
6401 };
6402
6403 let committee_member = PeerConfig::<SeqTypes>::test_default();
6404
6405 let mut network_config = NetworkConfig::<SeqTypes> {
6406 commit_sha: "deadbeef".to_string(),
6407 builder: BuilderType::Random,
6408 ..Default::default()
6409 };
6410 network_config.config.da_committees = vec![VersionedDaCommittee {
6411 start_version: vbs::version::Version { major: 0, minor: 6 },
6412 start_epoch: 10,
6413 committee: vec![committee_member.clone()],
6414 }];
6415
6416 let served: proto::HotshotConfigResponse = PublicNetworkConfig::from(network_config).into();
6417
6418 assert_eq!(served.commit_sha, "deadbeef");
6419 assert_eq!(served.builder, proto::BuilderType::Random as i32);
6420 let da_committee = &served.da_committees[0];
6422 assert_eq!(served.da_committees.len(), 1);
6423 assert_eq!(da_committee.start_version, "0.6");
6424 assert_eq!(da_committee.start_epoch, 10);
6425 assert_eq!(
6426 da_committee.committee,
6427 vec![proto::PeerConfig::from(committee_member)]
6428 );
6429 }
6430
6431 #[tokio::test]
6432 async fn sql_storage_settings_are_served_in_full() {
6433 use proto::config_service_server::ConfigService as _;
6434
6435 use crate::options::{
6436 PublicNodeConfig,
6437 tests::{parse_options_with, test_genesis},
6438 };
6439
6440 struct UnusedDataSource;
6441
6442 impl HotShotConfigDataSource for UnusedDataSource {
6443 async fn get_config(&self) -> espresso_types::config::PublicNetworkConfig {
6444 unreachable!("the runtime config is served from the state, not the data source")
6445 }
6446 }
6447
6448 let opt = parse_options_with(&[
6449 "--cliquenet-bind-address",
6450 "127.0.0.1:9999",
6451 "--",
6452 "storage-sql",
6453 "--prune",
6454 "--pruning-threshold",
6455 "1000000000000",
6456 ]);
6457 let cfg = PublicNodeConfig::new(&opt, &opt.modules(), &test_genesis());
6458 let sql = cfg.storage.sql.clone().expect("storage-sql was configured");
6459
6460 let state = NodeApiStateImpl::new(std::sync::Arc::new(UnusedDataSource))
6461 .with_public_node_config(Some(cfg));
6462 let storage = state
6463 .get_runtime_config(tonic::Request::new(proto::GetRuntimeConfigRequest {}))
6464 .await
6465 .unwrap()
6466 .into_inner()
6467 .storage
6468 .expect("the runtime config always reports a backend");
6469
6470 assert_eq!(storage.backend, proto::StorageBackend::Sql as i32);
6471 assert_eq!(storage.fs, None);
6472 assert_eq!(
6475 storage.sql,
6476 Some(proto::SqlStorage {
6477 prune: true,
6478 archive: sql.archive,
6479 lightweight: sql.lightweight,
6480 disable_proactive_fetching: sql.disable_proactive_fetching,
6481 fetch_rate_limit: sql.fetch_rate_limit.map(|limit| limit as u64),
6482 active_fetch_delay_ms: sql.active_fetch_delay.map(|delay| delay.as_millis() as u64),
6483 chunk_fetch_delay_ms: sql.chunk_fetch_delay.map(|delay| delay.as_millis() as u64),
6484 sync_status_chunk_size: sql.sync_status_chunk_size.map(|size| size as u64),
6485 sync_status_ttl_ms: sql.sync_status_ttl.map(|ttl| ttl.as_millis() as u64),
6486 proactive_scan_chunk_size: sql.proactive_scan_chunk_size.map(|size| size as u64),
6487 proactive_scan_interval_ms: sql
6488 .proactive_scan_interval
6489 .map(|interval| interval.as_millis() as u64),
6490 idle_connection_timeout_ms: sql.idle_connection_timeout.as_millis() as u64,
6491 connection_timeout_ms: sql.connection_timeout.as_millis() as u64,
6492 slow_statement_threshold_ms: sql.slow_statement_threshold.as_millis() as u64,
6493 statement_timeout_ms: sql.statement_timeout.as_millis() as u64,
6494 min_connections: sql.min_connections,
6495 max_connections: sql.max_connections,
6496 query_min_connections: sql.query_min_connections,
6497 query_max_connections: sql.query_max_connections,
6498 pruning: Some(proto::PruningConfig {
6499 pruning_threshold: Some(1000000000000),
6500 minimum_retention_ms: sql
6501 .pruning
6502 .minimum_retention
6503 .map(|retention| retention.as_millis() as u64),
6504 target_retention_ms: sql
6505 .pruning
6506 .target_retention
6507 .map(|retention| retention.as_millis() as u64),
6508 batch_size: sql.pruning.batch_size,
6509 max_usage: sql.pruning.max_usage.map(u32::from),
6510 interval_ms: sql
6511 .pruning
6512 .interval
6513 .map(|interval| interval.as_millis() as u64),
6514 pages: sql.pruning.pages,
6515 }),
6516 consensus_pruning: Some(proto::ConsensusPruningConfig {
6517 target_retention: sql.consensus_pruning.target_retention,
6518 minimum_retention: sql.consensus_pruning.minimum_retention,
6519 target_usage: sql.consensus_pruning.target_usage,
6520 }),
6521 })
6522 );
6523 let served = storage.sql.unwrap();
6525 assert!(served.statement_timeout_ms > 0);
6526 assert!(served.consensus_pruning.unwrap().target_retention > 0);
6527 }
6528
6529 fn assert_matches_v1_rendering<E, I, T, const ARITY: usize>(
6530 proof: &jf_merkle_tree_compat::prelude::MerkleProof<E, I, T, ARITY>,
6531 ) where
6532 E: jf_merkle_tree_compat::Element
6533 + ark_serialize::CanonicalSerialize
6534 + ark_serialize::CanonicalDeserialize,
6535 I: jf_merkle_tree_compat::Index
6536 + ark_serialize::CanonicalSerialize
6537 + ark_serialize::CanonicalDeserialize,
6538 T: jf_merkle_tree_compat::NodeValue,
6539 {
6540 let expected = serde_json::to_value(proof).unwrap();
6541 let converted = proto::MerklePathResponse::from(proof);
6542
6543 assert_eq!(converted.pos, expected["pos"].as_str().unwrap());
6544 let expected_path = expected["proof"].as_array().unwrap();
6545 assert_eq!(converted.proof.len(), expected_path.len());
6546 assert!(
6547 !expected_path.is_empty(),
6548 "a path of no nodes would assert nothing"
6549 );
6550 for (node, expected) in converted.proof.iter().zip(expected_path) {
6551 assert_merkle_node(node, expected);
6552 }
6553 }
6554
6555 #[test]
6556 fn block_state_path_mirrors_its_v1_rendering() {
6557 use committable::Committable as _;
6558 use jf_merkle_tree_compat::MerkleTreeScheme as _;
6559
6560 let commitment = reference_header("v3").0.commit();
6561 let tree = espresso_types::BlockMerkleTree::from_elems(Some(32), [commitment, commitment])
6562 .unwrap();
6563 let (_, proof) = tree.lookup(1).expect_ok().unwrap();
6566 assert_matches_v1_rendering(&proof);
6567 }
6568
6569 #[test]
6573 fn fee_state_path_mirrors_its_v1_rendering() {
6574 use jf_merkle_tree_compat::MerkleTreeScheme as _;
6575
6576 let account = espresso_types::FeeAccount::default();
6577 let tree = espresso_types::FeeMerkleTree::from_kv_set(
6578 20,
6579 [(account, espresso_types::FeeAmount::from(123u64))],
6580 )
6581 .unwrap();
6582 let (_, proof) = tree.lookup(account).expect_ok().unwrap();
6583 assert_matches_v1_rendering(&proof);
6584 }
6585
6586 fn assert_merkle_node(node: &proto::AdvzMerkleNode, expected: &serde_json::Value) {
6587 use proto::advz_merkle_node::Node;
6588
6589 match node.node.as_ref().unwrap() {
6590 Node::Empty(_) => assert_eq!(expected, "Empty"),
6591 Node::Branch(branch) => {
6592 let expected = &expected["Branch"];
6593 assert_eq!(branch.value, expected["value"].as_str().unwrap());
6594 let children = expected["children"].as_array().unwrap();
6595 assert_eq!(branch.children.len(), children.len());
6596 for (child, expected) in branch.children.iter().zip(children) {
6597 assert_merkle_node(child, expected);
6598 }
6599 },
6600 Node::Leaf(leaf) => {
6601 let expected = &expected["Leaf"];
6602 assert_eq!(leaf.value, expected["value"].as_str().unwrap());
6603 assert_eq!(leaf.pos, expected["pos"].as_str().unwrap());
6604 assert_eq!(leaf.elem, expected["elem"].as_str().unwrap());
6605 },
6606 Node::ForgottenSubtree(forgotten) => {
6607 let expected = &expected["ForgettenSubtree"];
6608 assert_eq!(forgotten.value, expected["value"].as_str().unwrap());
6609 },
6610 }
6611 }
6612
6613 #[test]
6614 fn snapshot_query_takes_exactly_one_selector() {
6615 assert!(matches!(
6616 snapshot_from_query(Some(7), None).unwrap(),
6617 v1::Snapshot::Height(7)
6618 ));
6619 assert!(matches!(
6620 snapshot_from_query(None, Some("MERKLE_COMM~x".to_owned())).unwrap(),
6621 v1::Snapshot::Commit(_)
6622 ));
6623 for (height, commit) in [(None, None), (Some(7), Some("MERKLE_COMM~x".to_owned()))] {
6624 assert_eq!(
6625 snapshot_from_query(height, commit).unwrap_err().code(),
6626 tonic::Code::InvalidArgument
6627 );
6628 }
6629 }
6630
6631 #[test]
6635 fn stake_table_events_carry_every_v1_field() {
6636 use espresso_types::{testing::TestValidator, v0_3::StakeTableEvent};
6637 use hotshot_contract_adapter::sol_types::StakeTableV3::{
6638 CommissionUpdated, Delegated, Undelegated, UndelegatedV2, ValidatorExitV2,
6639 };
6640
6641 let validator = TestValidator::random();
6642 let delegator = Address::repeat_byte(7);
6643 let events = [
6644 StakeTableEvent::Register((&validator).into()),
6645 StakeTableEvent::RegisterV2((&validator).into()),
6646 StakeTableEvent::RegisterV3((&validator).into()),
6647 StakeTableEvent::Deregister((&validator).into()),
6648 StakeTableEvent::DeregisterV2(ValidatorExitV2 {
6649 validator: validator.account,
6650 unlocksAt: U256::from(1_700_000_000u64),
6651 }),
6652 StakeTableEvent::Delegate(Delegated {
6653 delegator,
6654 validator: validator.account,
6655 amount: U256::from(10u64).pow(U256::from(20u64)),
6656 }),
6657 StakeTableEvent::Undelegate(Undelegated {
6658 delegator,
6659 validator: validator.account,
6660 amount: U256::from(12_345u64),
6661 }),
6662 StakeTableEvent::UndelegateV2(UndelegatedV2 {
6663 delegator,
6664 validator: validator.account,
6665 undelegationId: 7,
6666 amount: U256::from(54_321u64),
6667 unlocksAt: U256::from(1_700_000_001u64),
6668 }),
6669 StakeTableEvent::KeyUpdate((&validator).into()),
6670 StakeTableEvent::KeyUpdateV2((&validator).into()),
6671 StakeTableEvent::CommissionUpdate(CommissionUpdated {
6672 validator: validator.account,
6673 timestamp: U256::from(1_700_000_002u64),
6674 oldCommission: 100,
6675 newCommission: 250,
6676 }),
6677 validator.x25519_update([9; 32]),
6678 validator.p2p_update("10.0.0.1:9000"),
6679 ];
6680 for event in &events {
6681 let v1 = serde_json::to_value(event).unwrap();
6682 let (variant, v1) = v1.as_object().unwrap().iter().next().unwrap();
6683 let v2 = serde_json::to_value(proto::StakeTableEvent::from(event)).unwrap();
6684 let v2 = v2.as_object().unwrap().values().next().unwrap();
6685 assert_same_event_value(variant, v1, v2);
6686 }
6687 }
6688
6689 fn assert_same_event_value(path: &str, v1: &serde_json::Value, v2: &serde_json::Value) {
6691 use serde_json::Value;
6692
6693 let normalize = |key: &str| key.replace('_', "").to_lowercase();
6694 match (v1, v2) {
6695 (Value::Object(v1), Value::Object(v2)) => {
6696 let v2: HashMap<String, &Value> = v2
6697 .iter()
6698 .map(|(key, value)| (normalize(key), value))
6699 .collect();
6700 assert!(
6701 v2.keys()
6702 .all(|key| v1.keys().any(|v1_key| normalize(v1_key) == *key)),
6703 "{path}: v2 has a field v1 does not"
6704 );
6705 for (key, v1_value) in v1 {
6706 let path = format!("{path}.{key}");
6707 match v2.get(&normalize(key)) {
6708 Some(v2_value) => assert_same_event_value(&path, v1_value, v2_value),
6709 None => assert!(
6710 is_zero(v1_value),
6711 "{path}: v1 has {v1_value}, which v2 does not carry"
6712 ),
6713 }
6714 }
6715 },
6716 (Value::Number(v1), Value::Number(v2)) => assert_eq!(v1, v2, "{path}"),
6717 (Value::Number(v1), Value::String(v2)) => assert_eq!(v1.to_string(), *v2, "{path}"),
6718 (Value::String(v1), Value::String(v2)) => {
6719 let same = v1.eq_ignore_ascii_case(v2)
6720 || hex_quantity(v1).is_some_and(|v1| v2.parse::<U256>().ok() == Some(v1))
6721 || hex_bytes(v1).is_some_and(|v1| {
6722 base64::engine::general_purpose::STANDARD.decode(v2).ok() == Some(v1)
6723 });
6724 assert!(same, "{path}: v1 {v1:?}, v2 {v2:?}");
6725 },
6726 _ => panic!("{path}: v1 {v1}, v2 {v2}"),
6727 }
6728 }
6729
6730 fn is_zero(value: &serde_json::Value) -> bool {
6731 match value {
6732 serde_json::Value::Number(number) => number.as_u64() == Some(0),
6733 serde_json::Value::String(string) => {
6734 string.is_empty() || hex_quantity(string) == Some(U256::ZERO)
6735 },
6736 _ => false,
6737 }
6738 }
6739
6740 fn hex_quantity(value: &str) -> Option<U256> {
6741 U256::from_str_radix(value.strip_prefix("0x")?, 16).ok()
6742 }
6743
6744 fn hex_bytes(value: &str) -> Option<Vec<u8>> {
6745 alloy::hex::decode(value.strip_prefix("0x")?).ok()
6746 }
6747}