penumbra_sdk_ibc/component/rpc/
client_query.rs

1use async_trait::async_trait;
2
3use ibc_proto::ibc::core::client::v1::query_server::Query as ClientQuery;
4use ibc_proto::ibc::core::client::v1::{
5    ConsensusStateWithHeight, IdentifiedClientState, QueryClientParamsRequest,
6    QueryClientParamsResponse, QueryClientStateRequest, QueryClientStateResponse,
7    QueryClientStatesRequest, QueryClientStatesResponse, QueryClientStatusRequest,
8    QueryClientStatusResponse, QueryConsensusStateHeightsRequest,
9    QueryConsensusStateHeightsResponse, QueryConsensusStateRequest, QueryConsensusStateResponse,
10    QueryConsensusStatesRequest, QueryConsensusStatesResponse, QueryUpgradedClientStateRequest,
11    QueryUpgradedClientStateResponse, QueryUpgradedConsensusStateRequest,
12    QueryUpgradedConsensusStateResponse,
13};
14use prost::Message;
15
16use ibc_types::core::client::ClientId;
17use ibc_types::core::client::Height;
18use ibc_types::path::ClientConsensusStatePath;
19use ibc_types::path::ClientStatePath;
20use ibc_types::DomainType;
21
22use std::str::FromStr;
23use tonic::{Response, Status};
24
25use crate::component::{ClientStateReadExt, HostInterface};
26use crate::prefix::MerklePrefixExt;
27use crate::IBC_COMMITMENT_PREFIX;
28
29use super::utils::determine_snapshot_from_metadata;
30use super::IbcQuery;
31
32#[async_trait]
33impl<HI: HostInterface + Send + Sync + 'static> ClientQuery for IbcQuery<HI> {
34    async fn client_state(
35        &self,
36        request: tonic::Request<QueryClientStateRequest>,
37    ) -> std::result::Result<Response<QueryClientStateResponse>, Status> {
38        let snapshot = match determine_snapshot_from_metadata(self.storage.clone(), request.metadata()) {
39            Err(err) => return Err(tonic::Status::aborted(
40                format!("could not determine the correct snapshot to open given the `\"height\"` header of the request: {err:#}")
41            )),
42            Ok(snapshot) => snapshot,
43        };
44
45        let client_id = ClientId::from_str(&request.get_ref().client_id)
46            .map_err(|e| tonic::Status::invalid_argument(format!("invalid client id: {e}")))?;
47
48        // Query for client_state and associated proof.
49        let (cs_opt, proof) = snapshot
50            .get_with_proof(
51                IBC_COMMITMENT_PREFIX
52                    .apply_string(ClientStatePath(client_id.clone()).to_string())
53                    .as_bytes()
54                    .to_vec(),
55            )
56            .await
57            .map_err(|e| tonic::Status::aborted(format!("couldn't get client: {e}")))?;
58
59        let client_state = cs_opt
60            .map(|cs_opt| ibc_proto::google::protobuf::Any::decode(cs_opt.as_ref()))
61            .transpose()
62            .map_err(|e| tonic::Status::aborted(format!("couldn't decode client state: {e}")))?;
63
64        let res = QueryClientStateResponse {
65            client_state,
66            proof: proof.encode_to_vec(),
67            proof_height: Some(ibc_proto::ibc::core::client::v1::Height {
68                revision_height: HI::get_block_height(&snapshot)
69                    .await
70                    .map_err(|e| tonic::Status::aborted(format!("couldn't decode height: {e}")))?
71                    + 1,
72                revision_number: HI::get_revision_number(&snapshot)
73                    .await
74                    .map_err(|e| tonic::Status::aborted(format!("couldn't decode height: {e}")))?,
75            }),
76        };
77
78        Ok(tonic::Response::new(res))
79    }
80
81    /// ClientStates queries all the IBC light clients of a chain.
82    async fn client_states(
83        &self,
84        _request: tonic::Request<QueryClientStatesRequest>,
85    ) -> std::result::Result<tonic::Response<QueryClientStatesResponse>, tonic::Status> {
86        let snapshot = self.storage.latest_snapshot();
87
88        let client_counter = snapshot
89            .client_counter()
90            .await
91            .map_err(|e| tonic::Status::aborted(format!("couldn't get client counter: {e}")))?
92            .0;
93
94        let mut client_states = vec![];
95        for client_idx in 0..client_counter {
96            // NOTE: currently, we only look up tendermint clients, because we only support tendermint clients.
97            let client_id = ClientId::from_str(format!("07-tendermint-{}", client_idx).as_str())
98                .map_err(|e| tonic::Status::aborted(format!("invalid client id: {e}")))?;
99            let client_state = snapshot.get_client_state(&client_id).await;
100            let id_client = IdentifiedClientState {
101                client_id: client_id.to_string(),
102                client_state: client_state.ok().map(|state| state.into()), // send None if we couldn't find the client state
103            };
104            client_states.push(id_client);
105        }
106
107        let res = QueryClientStatesResponse {
108            client_states,
109            pagination: None,
110        };
111
112        Ok(tonic::Response::new(res))
113    }
114
115    /// ConsensusState queries a consensus state associated with a client state at
116    /// a given height.
117    async fn consensus_state(
118        &self,
119        request: tonic::Request<QueryConsensusStateRequest>,
120    ) -> std::result::Result<tonic::Response<QueryConsensusStateResponse>, tonic::Status> {
121        let snapshot = match determine_snapshot_from_metadata(self.storage.clone(), request.metadata()) {
122            Err(err) => return Err(tonic::Status::aborted(
123                format!("could not determine the correct snapshot to open given the `\"height\"` header of the request: {err:#}")
124            )),
125            Ok(snapshot) => snapshot,
126        };
127
128        let client_id = ClientId::from_str(&request.get_ref().client_id)
129            .map_err(|e| tonic::Status::invalid_argument(format!("invalid client id: {e}")))?;
130        let height = if request.get_ref().latest_height {
131            get_latest_verified_height(&snapshot, &client_id).await?
132        } else {
133            Height {
134                revision_number: request.get_ref().revision_number,
135                revision_height: request.get_ref().revision_height,
136            }
137        };
138
139        let (cs_opt, proof) = snapshot
140            .get_with_proof(
141                IBC_COMMITMENT_PREFIX
142                    .apply_string(ClientConsensusStatePath::new(&client_id, &height).to_string())
143                    .as_bytes()
144                    .to_vec(),
145            )
146            .await
147            .map_err(|e| tonic::Status::aborted(format!("couldn't get consensus state: {e}")))?;
148
149        let consensus_state = cs_opt
150            .map(|cs_opt| ibc_proto::google::protobuf::Any::decode(cs_opt.as_ref()))
151            .transpose()
152            .map_err(|e| tonic::Status::aborted(format!("couldn't decode consensus state: {e}")))?;
153
154        let res = QueryConsensusStateResponse {
155            consensus_state,
156            proof: proof.encode_to_vec(),
157            proof_height: Some(ibc_proto::ibc::core::client::v1::Height {
158                revision_height: HI::get_block_height(&snapshot)
159                    .await
160                    .map_err(|e| tonic::Status::aborted(format!("couldn't decode height: {e}")))?
161                    + 1,
162                revision_number: HI::get_revision_number(&snapshot)
163                    .await
164                    .map_err(|e| tonic::Status::aborted(format!("couldn't decode height: {e}")))?,
165            }),
166        };
167
168        Ok(tonic::Response::new(res))
169    }
170
171    /// ConsensusStates queries all the consensus state associated with a given
172    /// client.
173    async fn consensus_states(
174        &self,
175        request: tonic::Request<QueryConsensusStatesRequest>,
176    ) -> std::result::Result<tonic::Response<QueryConsensusStatesResponse>, tonic::Status> {
177        let snapshot = self.storage.latest_snapshot();
178        let client_id = ClientId::from_str(&request.get_ref().client_id)
179            .map_err(|e| tonic::Status::invalid_argument(format!("invalid client id: {e}")))?;
180
181        let verified_heights = snapshot
182            .get_verified_heights(&client_id)
183            .await
184            .map_err(|e| tonic::Status::aborted(format!("couldn't get verified heights: {e}")))?;
185        let resp = if let Some(verified_heights) = verified_heights {
186            let mut consensus_states = Vec::with_capacity(verified_heights.heights.len());
187            for height in verified_heights.heights {
188                let consensus_state = snapshot
189                    .get_verified_consensus_state(&height, &client_id)
190                    .await
191                    .map_err(|e| {
192                        tonic::Status::aborted(format!("couldn't get consensus state: {e}"))
193                    })?;
194                consensus_states.push(ConsensusStateWithHeight {
195                    height: Some(height.into()),
196                    consensus_state: Some(consensus_state.into()),
197                });
198            }
199            QueryConsensusStatesResponse {
200                consensus_states,
201                pagination: None,
202            }
203        } else {
204            QueryConsensusStatesResponse {
205                consensus_states: vec![],
206                pagination: None,
207            }
208        };
209
210        Ok(tonic::Response::new(resp))
211    }
212
213    /// ConsensusStateHeights queries the height of every consensus states associated with a given client.
214    async fn consensus_state_heights(
215        &self,
216        request: tonic::Request<QueryConsensusStateHeightsRequest>,
217    ) -> std::result::Result<tonic::Response<QueryConsensusStateHeightsResponse>, tonic::Status>
218    {
219        let snapshot = self.storage.latest_snapshot();
220        let client_id = ClientId::from_str(&request.get_ref().client_id)
221            .map_err(|e| tonic::Status::invalid_argument(format!("invalid client id: {e}")))?;
222
223        let verified_heights = snapshot
224            .get_verified_heights(&client_id)
225            .await
226            .map_err(|e| tonic::Status::aborted(format!("couldn't get verified heights: {e}")))?;
227        let resp = if let Some(verified_heights) = verified_heights {
228            QueryConsensusStateHeightsResponse {
229                consensus_state_heights: verified_heights
230                    .heights
231                    .into_iter()
232                    .map(|h| h.into())
233                    .collect(),
234                pagination: None,
235            }
236        } else {
237            QueryConsensusStateHeightsResponse {
238                consensus_state_heights: vec![],
239                pagination: None,
240            }
241        };
242
243        Ok(tonic::Response::new(resp))
244    }
245
246    /// Status queries the status of an IBC client.
247    async fn client_status(
248        &self,
249        request: tonic::Request<QueryClientStatusRequest>,
250    ) -> std::result::Result<tonic::Response<QueryClientStatusResponse>, tonic::Status> {
251        let snapshot = self.storage.latest_snapshot();
252        let client_id = ClientId::from_str(&request.get_ref().client_id)
253            .map_err(|e| tonic::Status::invalid_argument(format!("invalid client id: {e}")))?;
254        let timestamp = HI::get_block_timestamp(snapshot.clone())
255            .await
256            .map_err(|e| tonic::Status::aborted(format!("couldn't get block timestamp: {e}")))?;
257        let client_status = snapshot.get_client_status(&client_id, timestamp).await;
258        let resp = QueryClientStatusResponse {
259            status: client_status.to_string(),
260        };
261
262        Ok(tonic::Response::new(resp))
263    }
264    /// ClientParams queries all parameters of the ibc client.
265    async fn client_params(
266        &self,
267        _request: tonic::Request<QueryClientParamsRequest>,
268    ) -> std::result::Result<tonic::Response<QueryClientParamsResponse>, tonic::Status> {
269        Err(tonic::Status::unimplemented("not implemented"))
270    }
271    /// UpgradedClientState queries an Upgraded IBC light client.
272    async fn upgraded_client_state(
273        &self,
274        _request: tonic::Request<QueryUpgradedClientStateRequest>,
275    ) -> std::result::Result<tonic::Response<QueryUpgradedClientStateResponse>, tonic::Status> {
276        Err(tonic::Status::unimplemented("not implemented"))
277    }
278    /// UpgradedConsensusState queries an Upgraded IBC consensus state.
279    async fn upgraded_consensus_state(
280        &self,
281        _request: tonic::Request<QueryUpgradedConsensusStateRequest>,
282    ) -> std::result::Result<tonic::Response<QueryUpgradedConsensusStateResponse>, tonic::Status>
283    {
284        Err(tonic::Status::unimplemented("not implemented"))
285    }
286}
287
288async fn get_latest_verified_height(
289    snapshot: &cnidarium::Snapshot,
290    client_id: &ClientId,
291) -> Result<Height, tonic::Status> {
292    let verified_heights = snapshot
293        .get_verified_heights(&client_id)
294        .await
295        .map_err(|e| tonic::Status::aborted(format!("couldn't get verified heights: {e}")))?;
296
297    let Some(mut verified_heights) = verified_heights else {
298        return Err(tonic::Status::not_found(
299            "couldn't find verified heights for client: {client_id}",
300        ));
301    };
302
303    verified_heights.heights.sort();
304    if verified_heights.heights.is_empty() {
305        return Err(tonic::Status::not_found(
306            "verified heights for client were empty: {client_id}",
307        ));
308    }
309
310    Ok(verified_heights.heights.pop().expect("must be non-empty"))
311}