Skip to main content

espresso_node/api/
state.rs

1//! Implementations of the v1 API traits and the v2 tonic service traits, both reading the one
2//! data source this type wraps.
3
4use 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
79/// Timeout for failing requests due to missing data.
80///
81/// If data needed to respond to a request is missing, it can (in some cases) be fetched from an
82/// external provider. This parameter controls how long the request handler will wait for
83/// missing data to be fetched before giving up and failing the request.
84///
85/// Matches the `hotshot_query_service` availability API default.
86const FETCH_TIMEOUT: Duration = Duration::from_millis(500);
87
88/// Bounds the namespace list of a v2 multi-namespace light-client request, as the URL length
89/// bounds v1's, since every namespace costs a lookup in every block of the range.
90const MAX_NAMESPACES_PER_REQUEST: usize = 100;
91
92/// Node API state implementation
93///
94/// This struct implements the v1 API traits (internal types) and the v2 tonic service traits
95/// (proto types).
96#[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        // `last_merklized_state_height` is the same row for every merklized-state module in
169        // this file (reward V1/V2, block-state, fee-state), not just these two.
170        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        // Parse the Ethereum address
199        let addr: alloy::primitives::Address = address
200            .parse()
201            .map_err(|_| bad_request(format!("invalid ethereum address: {}", address)))?;
202
203        // Load the reward account proof from the data source
204        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        // Convert the proof to reward claim input (internal type)
216        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        // Parse the Ethereum address
240        let addr: alloy::primitives::Address = address
241            .parse()
242            .map_err(|_| bad_request(format!("invalid ethereum address: {}", address)))?;
243
244        // Load the reward account proof from the data source
245        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        // Parse the Ethereum address
287        let addr: alloy::primitives::Address = address
288            .parse()
289            .map_err(|_| bad_request(format!("invalid ethereum address: {}", address)))?;
290
291        // Load and return the reward account proof directly (internal type)
292        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        // Parse the Ethereum address
311        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    // No `RewardMerkleTreeDataSource` bound here: unlike `v1::RewardApi`, none of these methods
443    // touch the reward merkle tree, so filesystem storage (which doesn't implement it) can serve
444    // this module too.
445    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        // Convert v1 BlockId to hotshot BlockId
461        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        // Fetch block and VID common data
478        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        // Namespace absent from the block: an empty result, not an error.
497        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        // Generate namespace proof
506        let Some(proof) = NsProof::new(block.payload(), &ns_index, vid_common.common()) else {
507            // Failed to generate proof - namespace exists but proof generation failed
508            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        // Validate range
531        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        // Fetch blocks and VID common data for the range
547        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        // Generate proofs for each block
581        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            // Check if namespace exists in this block
587            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                    // Failed to generate proof - return empty result for this block
596                    proofs.push(espresso_types::NamespaceProofQueryData {
597                        transactions: vec![],
598                        proof: None,
599                    });
600                }
601            } else {
602                // Namespace not present in this block
603                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        // Block has incorrect encoding: fetch VID shares to construct the proof.
697        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        // Try to get from local storage first
747        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                // Not found locally, try to fetch from peers
753                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                // Store the fetched certificate
761                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
794/// Check a ranges request against the same per-request object limit the range endpoints enforce,
795/// and convert it for the data source.
796///
797/// Bounding the total heights also bounds how many ranges a request may carry, since every range
798/// covers at least one height.
799fn 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        // Heights are bound into the query as i64, so anything past that range cannot be queried
809        // and would only be a way to overflow the accounting below.
810        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    // The light client pairs leaves with proofs positionally, so a height must appear once, in
825    // order.
826    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
833// Range limits for list endpoints, read from `hotshot_query_service`'s `Options` (their only
834// remaining declaration) so a dependency bump that changes the defaults changes enforcement too.
835const 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        // One read when every height is present. A miss falls through to the range endpoints,
1048        // for their window per height and 404 at the first one missing.
1049        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        // Standard prometheus text exposition of the registry.
1560        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        // A fresh node computes 0/0 and a restarted one height/0 until its first view tick
1597        // (the view gauge is in-memory, the height persisted). protoJSON cannot encode a
1598        // non-finite double and the generated deserializer rejects `null`, so clamp to zero.
1599        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        // v1 serves a map, so the order is its own; the paged route reports account order.
2106        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
2155// These stay here rather than in the api crate's `render` for the same reason as the stake table
2156// below: the source types are this crate's, and the api crate cannot depend on this one.
2157
2158impl From<crate::options::PublicNodeConfig> for proto::RuntimeConfigResponse {
2159    fn from(config: crate::options::PublicNodeConfig) -> Self {
2160        // Destructured without `..` so that a new field fails to compile here instead of becoming
2161        // a setting v2 silently never serves; the `_` bindings are the deliberate drops. The guard
2162        // stops at this level, as the storage and module structs below are read field by field.
2163        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            // `unbracketed_string` rather than `to_string`: NetAddr's Display brackets an IPv6
2240            // literal and its serde impl does not, so v1 serves the unbracketed form.
2241            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            // The deprecated CDN and libp2p fields stay at their empty defaults.
2252            ..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            // The module is accepted for compatibility but no longer served.
2344            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        // The wire format is the raw Vec<u8> from `get_reward_merkle_tree_v2` encoded as the
2497        // JSON body; keep it that way for existing clients.
2498        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        // v1 wraps these bytes in a `serde_json::Value`, so take them from the source instead.
2665        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
2689/// A tree travels as bincode, the encoding peers already decode from v1 and from the v2
2690/// reward tree route.
2691fn 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
2696/// The `(height, view)` the catchup routes over a merkle tree are asked for.
2697fn 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            // Keep rejections typed for `classify`.
2714            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/// Network-agnostic submit hook used by the axum wrapper. The original
2747/// `SubmitDataSource<N, P>` trait is parameterized by the network type; this
2748/// erased trait lets `NodeApiStateImpl` avoid carrying those parameters.
2749#[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// Bare mode (no query/status API) has no `ExtensibleDataSource` wrapper: the app state is
2772// `ApiState` directly, so it needs its own erased forwarding impl.
2773#[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// Bare mode (no query/status API) has no `ExtensibleDataSource` wrapper: the app state is
2841// `ApiState` directly, so it needs its own erased forwarding impl.
2842#[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                // The query binds `limit + offset` as an i64 row count, which a larger offset
2942                // would overflow into a negative or wrapped limit.
2943                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
3114/// v1 spells the three ways of naming a block as separate routes, so v2's optional parameters
3115/// carry the same choice, with neither given meaning the latest block.
3116fn 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
3127/// As [`block_ident`], except that naming a transaction by position takes both `height` and
3128/// `offset`, so half of that pair is a bad request rather than a fallback to the latest.
3129fn 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            // By height, not by position: a proof built from one height's payload and another
3386            // height's VID common cannot verify, and the two are read separately.
3387            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        // v1's trait method takes the namespaces as its TaggedBase64 path segment, which this
3665        // would only build to have it parsed straight back.
3666        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
3732/// Convert a query-service error to an [`AvailabilityError`]-carrying anyhow error so the HTTP
3733/// layer returns the status carried by the error (400/404) instead of 500.
3734pub(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
3743/// Like [`lc_error`], for explorer data source errors.
3744fn 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
3759/// Bounds the leaves in a single leaf proof, and so the memory to build and serialize it.
3760///
3761/// Tracks the `hotshot_query_service` small-object range limit, so a dependency bump that
3762/// changes that default changes this bound too.
3763fn 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                // v1 serializes these through chrono's serde impl, which ends in `Z`, where plain
3953                // `to_rfc3339` would write `+00:00` and disagree with it and with protoJSON.
3954                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
3990/// Namespace ids are 32 bits on chain but travel as uint64 so they round-trip with the
3991/// responses' `namespace` fields. Anything wider is a client error, not a truncation.
3992fn 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
4005/// A conversion error is sent as an `event: error` frame. Ending the stream there means a
4006/// subscriber that skips the frame cannot miss a height without noticing.
4007fn 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    // Delegates to both v1 availability traits, so it needs the bounds of both.
4035    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        // v1 takes the key as a string because its route carried it as a path segment.
4606        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        // v1 reverses each page. v2 serves the tree's own order, so a client paging forward reads
4733        // the tree in one direction.
4734        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    /// v1 renders its byte-encoded fields as JSON integer arrays.
4800    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    /// The reference vectors are the canonical v1 encoding, so comparing the converted header
4814    /// against them is what makes "the v2 header mirrors v1" a checked claim rather than a
4815    /// reviewed one. Every representation the conversion picks by hand is pinned here: `0x`
4816    /// addresses, the hex L1 timestamp, decimal fee and reward amounts, TaggedBase64
4817    /// commitments, and the base64 namespace table.
4818    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        // v1 headers are stored flat; later versions wrap their fields alongside the version.
4824        let fields = json.get("fields").cloned().unwrap_or(json);
4825        (header, fields)
4826    }
4827
4828    /// Fails when the proto message and the reference vector disagree about which fields exist,
4829    /// which value-by-value assertions cannot catch: they only check the fields already declared.
4830    /// The declared side is read from the descriptor, so a field the proto lacks fails too.
4831    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                // v1 writes a tagged union as one key naming the union, not one per arm, so an
4846                // arm is compared under its oneof's name. A `proto3_optional` field is a
4847                // synthetic one-arm oneof and keeps its own name.
4848                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            // `_pd` is how v1 serializes a certificate's `PhantomData`, and carries nothing a
4860            // client can read, so no proto message mirrors it.
4861            .filter(|name| *name != "_pd")
4862            .collect();
4863        assert_eq!(
4864            declared, referenced,
4865            "{message} fields drifted from the reference vector"
4866        );
4867    }
4868
4869    /// Covers the four shapes and all seven arms: every version's vector must select the arm named
4870    /// after it, and the proto message must carry exactly the fields v1 serializes, so neither a
4871    /// new protocol version nor a proto edit can add or drop a header field without failing here.
4872    /// A version may drop a field its shape still declares, which the shape then renders empty;
4873    /// the table names that field so the check stays exact for the rest.
4874    #[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            // Every shape repeats these assignments in its own struct literal, so each one is
4895            // compared against the vector: the reference heights, timestamps and l1_head are
4896            // distinct, so a field wired to its neighbour fails here.
4897            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            // Every shape before 0.7 carries the builder commitment; 0.7 dropped the field.
4942            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        // protoJSON base64s the bytes, which is how v1 renders the table too.
5076        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    /// No reference vector carries a commitment-only chain config, so the `Right` arm is checked
5120    /// here on its own. `resolve` must report absence rather than `commit` hashing an empty config.
5121    #[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    // A test network only disperses with AvidmGf2, so the ADVZ and AvidM arms, which mainnet
5137    // still serves for blocks from before 0.6, run only here, against the namespaced wrappers
5138    // the node stores rather than the inner per-namespace shares.
5139    #[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(&param, &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(&param, &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    // No test network registers a validator, so this mapping is only exercised here.
5233    #[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        // serde renders this key in x25519's own base58, so the tagged form is worth pinning.
5259        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        // v1 serializes the pre-bracketing form, so `to_string` would give `[::1]:9977`.
5267        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        // v1 serializes a U256 as a hex quantity, where `to_string` would give it in decimal.
5278        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_node_api_v2_agrees_with_v1` compares a fresh node's sync status, which the query
5300    // service caches at startup with no ranges, so this match is only exercised here.
5301    #[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    // The only tests of the range limits since the query service's own API (and its
5348    // `test_range_limit`) was deleted: an in-limit range passes, one past the limit is a
5349    // RangeExceeded, which the HTTP layer serves as a 400.
5350    #[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    /// No vector carries view-change evidence, an upgrade certificate, a phase-2 certificate or a
5382    /// signed QC, so those are built here. The aggregate signature must print as the TaggedBase64
5383    /// form v1's serde emits, so a v2 client can hand it back to v1.
5384    #[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    /// No test can build this proof, since it needs a malicious dispersal and the vid crate keeps
5576    /// the items for one private. Deserializing the JSON v1 would serve pins the two field names
5577    /// the conversion reads, so an upstream rename fails here rather than as a 500.
5578    #[test]
5579    fn bad_encoding_namespace_proof_mirrors_its_v1_rendering() {
5580        // ark-serialize writes a `Vec` as a little-endian u64 length followed by its elements, so
5581        // eight zero bytes is the empty vector both fields hold here.
5582        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    /// Neither vector carries signatures, so the signature mapping is pinned only by its types.
5619    #[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    /// Every proof in the vector is AvidM. The ADVZ arm is covered by
5658    /// `advz_transaction_proof_mirrors_its_v1_rendering`.
5659    #[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    /// The payload bytes and namespace table are compared through base64, which is how v1 renders
5803    /// them and how protoJSON renders `bytes`.
5804    #[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    /// The per-namespace map is the one summary field that comes from the payload, so no header
5859    /// vector covers it.
5860    #[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        // protoJSON writes a map key as a string whatever its proto type, which is also what
5880        // serde_json does with v1's `NamespaceId` keys.
5881        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        // The v3-era vector embeds a 0.1-shaped header: twelve flat fields, no version wrapper,
5923        // no reward root. Header versions and leaf versions moved independently.
5924        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        // `LeafQueryData` deserializes through `new`, which drops the payload the JSON carries,
5936        // so the conversion must report none.
5937        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        // The vector's certificates are unsigned, so absence must map to absence.
5963        assert!(qc.signatures.is_none());
5964    }
5965
5966    /// A 0.1 header has no reward tree, no millisecond timestamp and no leader counts, so the V1
5967    /// message must not carry them. This is the case where the `reward_merkle_tree_root`
5968    /// accessor would have reported the commitment of an empty tree instead of nothing.
5969    #[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        // More heights than the object limit.
6000        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        // Many single-height ranges are bounded by the object limit like anything else.
6007        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        // An empty range would otherwise reach the query builder as a contradictory bound.
6015        #[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        // A range wide enough to overflow the running total must not wrap past the limit.
6023        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        // Touching is fine: a run split at a chunk boundary arrives this way.
6033        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    // Tripwire: the enforced and advertised limits come from `hotshot_query_service`'s
6044    // `Options` defaults. If a dependency change moves them, this fails so the new bound is
6045    // adopted deliberately rather than silently.
6046    #[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    // Regression: the light-client trait methods used to map query-service errors through
6054    // `anyhow::anyhow!("{err}")`, erasing the status; every 400/404 became a 500.
6055    #[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    // Regression: the explorer methods also erased the status, so a missing block was a 500.
6089    #[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        // No reference vector carries an ADVZ transaction proof, so build one the way a V0 node
6145        // did and compare against v1's JSON. The accessor-backed fields are the real check. The
6146        // range proofs are read from that same JSON, so their loop only pins that the keys exist
6147        // and that an absent proof stays absent.
6148        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    /// A node under test registers no runtime config, so `test_v2_api_agrees_with_v1` only ever
6211    /// reaches the 404. This covers the mapping itself: every field the proto promises comes from
6212    /// the matching `PublicNodeConfig` field, with identity values distinct enough that a mapping
6213    /// crossing two of them fails.
6214    #[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        // Set directly: the follower module needs storage-sql.
6253        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                    // Not pinned to `None`: the default backend parses an empty argv, which
6313                    // still reads ESPRESSO_NODE_STORAGE_PATH.
6314                    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                        // Two same-typed fields whose defaults are 5/100, so the flags above give
6360                        // each a distinct value: a crossed pair would pass otherwise.
6361                        light_client_db: Some(proto::LightClientDbOptions {
6362                            num_connections: 7,
6363                            num_leaves: 11,
6364                            // Not pinned to `None`: it defaults under ESPRESSO_NODE_STORAGE_PATH.
6365                            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        // IPv6 because that is the only case where NetAddr's Display and its serde impl
6388        // disagree, and v1 goes through serde.
6389        assert_eq!(runtime.config_peers.len(), 2);
6390        assert_eq!(runtime.cliquenet_bind_address, "2001:db8::1:9999");
6391    }
6392
6393    /// A TestNetwork leaves all of these at their defaults, so the live parity test compares them
6394    /// `None` to `None` and empty to empty. Exercised here with values instead.
6395    #[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        // The version renders as v1's `version_ser` writes it, not as `Debug`.
6421        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        // Compared against the source, not against `sql.clone().into()`, which would assert the
6473        // mapping against itself. v1 serves the durations as `{secs, nanos}`.
6474        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        // The defaults these come from are non-zero, so the comparisons above are not vacuous.
6524        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        // The block tree is light-weight: every leaf but the frontier is forgotten, so only the
6564        // last index can be looked up here.
6565        let (_, proof) = tree.lookup(1).expect_ok().unwrap();
6566        assert_matches_v1_rendering(&proof);
6567    }
6568
6569    /// The fee tree indexes by account and branches 256 ways where the block tree indexes by
6570    /// height and branches 3, so it exercises the conversion over a different `Index` and a
6571    /// different arity.
6572    #[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    /// Every stake table event, compared field by field with the JSON v1 serves for it. The two
6632    /// spell a field differently (`blsVK` against `blsVk`) and encode amounts, timestamps and
6633    /// bytes differently, so each pair of values is compared as the quantity or bytes it holds.
6634    #[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    /// protoJSON omits a zero value, so a field v2 leaves out must be zero on v1.
6690    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}