penumbra_sdk_sct/component/
rpc.rs

1use cnidarium::Storage;
2use pbjson_types::Timestamp;
3use penumbra_sdk_proto::core::component::sct::v1::query_service_server::QueryService;
4use penumbra_sdk_proto::core::component::sct::v1::{
5    AnchorByHeightRequest, AnchorByHeightResponse, EpochByHeightRequest, EpochByHeightResponse,
6    SctFrontierRequest, SctFrontierResponse, TimestampByHeightRequest, TimestampByHeightResponse,
7};
8use penumbra_sdk_proto::crypto::tct::v1 as pb_tct;
9use tonic::Status;
10use tracing::instrument;
11
12use crate::state_key;
13
14use super::clock::EpochRead;
15use super::tree::SctRead;
16
17// TODO: Hide this and only expose a Router?
18pub struct Server {
19    storage: Storage,
20}
21
22impl Server {
23    pub fn new(storage: Storage) -> Self {
24        Self { storage }
25    }
26}
27
28#[tonic::async_trait]
29impl QueryService for Server {
30    #[instrument(skip(self, request))]
31    async fn epoch_by_height(
32        &self,
33        request: tonic::Request<EpochByHeightRequest>,
34    ) -> Result<tonic::Response<EpochByHeightResponse>, Status> {
35        let state = self.storage.latest_snapshot();
36
37        let epoch = state
38            .get_epoch_by_height(request.get_ref().height)
39            .await
40            .map_err(|e| tonic::Status::unknown(format!("could not get epoch for height: {e}")))?;
41
42        Ok(tonic::Response::new(EpochByHeightResponse {
43            epoch: Some(epoch.into()),
44        }))
45    }
46
47    #[instrument(skip(self, request))]
48    async fn anchor_by_height(
49        &self,
50        request: tonic::Request<AnchorByHeightRequest>,
51    ) -> Result<tonic::Response<AnchorByHeightResponse>, Status> {
52        let state = self.storage.latest_snapshot();
53
54        let height = request.get_ref().height;
55        let anchor = state.get_anchor_by_height(height).await.map_err(|e| {
56            tonic::Status::unknown(format!("could not get anchor for height {height}: {e}"))
57        })?;
58
59        Ok(tonic::Response::new(AnchorByHeightResponse {
60            anchor: anchor.map(Into::into),
61        }))
62    }
63
64    #[instrument(skip(self, request))]
65    async fn timestamp_by_height(
66        &self,
67        request: tonic::Request<TimestampByHeightRequest>,
68    ) -> Result<tonic::Response<TimestampByHeightResponse>, Status> {
69        let state = self.storage.latest_snapshot();
70
71        let height = request.get_ref().height;
72        let block_time = state.get_block_timestamp(height).await.map_err(|e| {
73            tonic::Status::unknown(format!("could not get timestamp for height {height}: {e}"))
74        })?;
75        let timestamp = chrono::DateTime::parse_from_rfc3339(block_time.to_rfc3339().as_str())
76            .expect("timestamp should roundtrip to string");
77
78        Ok(tonic::Response::new(TimestampByHeightResponse {
79            timestamp: Some(Timestamp {
80                seconds: timestamp.timestamp(),
81                nanos: timestamp.timestamp_subsec_nanos() as i32,
82            }),
83        }))
84    }
85
86    #[instrument(skip(self, request))]
87    async fn sct_frontier(
88        &self,
89        request: tonic::Request<SctFrontierRequest>,
90    ) -> Result<tonic::Response<SctFrontierResponse>, Status> {
91        let state = self.storage.latest_snapshot();
92
93        let with_proof = request.get_ref().with_proof;
94
95        let frontier = state.get_sct().await;
96        let current_height = state
97            .get_block_height()
98            .await
99            .map_err(|e| tonic::Status::unknown(format!("could not get current height: {e}")))?;
100
101        let (anchor, maybe_proof) = if !with_proof {
102            (frontier.root(), None)
103        } else {
104            let anchor_key = state_key::tree::anchor_by_height(current_height)
105                .as_bytes()
106                .to_vec();
107            let (maybe_raw_anchor, proof) =
108                state.get_with_proof(anchor_key).await.map_err(|e| {
109                    tonic::Status::unknown(format!(
110                        "could not get w/ proof anchor for height {current_height}: {e}"
111                    ))
112                })?;
113
114            let Some(raw_anchor) = maybe_raw_anchor else {
115                return Err(tonic::Status::not_found(format!(
116                    "anchor not found for height {current_height}"
117                )));
118            };
119
120            let proto_anchor: pb_tct::MerkleRoot = pb_tct::MerkleRoot { inner: raw_anchor };
121            let anchor: penumbra_sdk_tct::Root = proto_anchor
122                .try_into()
123                .map_err(|_| tonic::Status::internal("failed to parse anchor"))?;
124            (anchor, Some(proof.into()))
125        };
126
127        // Sanity check we got the right anchor - redundant if no proof was requested
128        let locked_anchor = frontier.root();
129        if anchor != locked_anchor {
130            return Err(tonic::Status::internal(format!(
131                "anchor mismatch: {anchor} != {locked_anchor}"
132            )));
133        }
134
135        let raw_frontier = bincode::serialize(&frontier)
136            .map_err(|e| tonic::Status::internal(format!("failed to serialize SCT: {e}")))?;
137
138        Ok(tonic::Response::new(SctFrontierResponse {
139            height: current_height,
140            anchor: Some(anchor.into()),
141            compact_frontier: raw_frontier,
142            proof: maybe_proof,
143        }))
144    }
145}