penumbra_sdk_sct/component/
rpc.rs1use 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
17pub 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 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}