ibc_testkit/testapp/ibc/core/
client_ctx.rs

1use core::fmt::Debug;
2
3use basecoin_store::context::{ProvableStore, Store};
4use basecoin_store::types::Height as StoreHeight;
5use ibc::core::client::context::{
6    ClientExecutionContext, ClientValidationContext, ExtClientValidationContext,
7};
8use ibc::core::client::types::Height;
9use ibc::core::host::types::error::HostError;
10use ibc::core::host::types::identifiers::{ChannelId, ClientId, PortId};
11use ibc::core::host::types::path::{
12    ClientConsensusStatePath, ClientStatePath, ClientUpdateHeightPath, ClientUpdateTimePath, Path,
13};
14use ibc::core::host::ValidationContext;
15use ibc::core::primitives::Timestamp;
16use ibc::primitives::prelude::*;
17
18use super::types::MockIbcStore;
19use crate::testapp::ibc::clients::mock::client_state::MockClientContext;
20use crate::testapp::ibc::clients::{AnyClientState, AnyConsensusState};
21
22pub type PortChannelIdMap<V> = BTreeMap<PortId, BTreeMap<ChannelId, V>>;
23
24/// A mock of an IBC client record as it is stored in a mock context.
25/// For testing ICS02 handlers mostly, cf. `MockClientContext`.
26#[derive(Clone, Debug)]
27pub struct MockClientRecord {
28    /// The client state (representing only the latest height at the moment).
29    pub client_state: Option<AnyClientState>,
30
31    /// Mapping of heights to consensus states for this client.
32    pub consensus_states: BTreeMap<Height, AnyConsensusState>,
33}
34
35impl<S> MockClientContext for MockIbcStore<S>
36where
37    S: ProvableStore + Debug,
38{
39    fn host_timestamp(&self) -> Result<Timestamp, HostError> {
40        ValidationContext::host_timestamp(self)
41    }
42
43    fn host_height(&self) -> Result<Height, HostError> {
44        ValidationContext::host_height(self)
45    }
46}
47
48impl<S> ExtClientValidationContext for MockIbcStore<S>
49where
50    S: ProvableStore + Debug,
51{
52    fn host_timestamp(&self) -> Result<Timestamp, HostError> {
53        ValidationContext::host_timestamp(self)
54    }
55
56    fn host_height(&self) -> Result<Height, HostError> {
57        ValidationContext::host_height(self)
58    }
59
60    /// Returns the list of heights at which the consensus state of the given client was updated.
61    fn consensus_state_heights(&self, client_id: &ClientId) -> Result<Vec<Height>, HostError> {
62        let path = format!("clients/{}/consensusStates", client_id).into();
63
64        self.consensus_state_store
65            .get_keys(&path)
66            .into_iter()
67            .filter_map(|path| {
68                if let Ok(Path::ClientConsensusState(consensus_path)) = path.try_into() {
69                    Some(consensus_path)
70                } else {
71                    None
72                }
73            })
74            .map(|consensus_path| {
75                Height::new(
76                    consensus_path.revision_number,
77                    consensus_path.revision_height,
78                )
79                .map_err(HostError::invalid_state)
80            })
81            .collect::<Result<Vec<_>, _>>()
82    }
83
84    fn next_consensus_state(
85        &self,
86        client_id: &ClientId,
87        height: &Height,
88    ) -> Result<Option<Self::ConsensusStateRef>, HostError> {
89        let path = format!("clients/{client_id}/consensusStates").into();
90
91        let keys = self.store.get_keys(&path);
92        let found_path = keys.into_iter().find_map(|path| {
93            if let Ok(Path::ClientConsensusState(path)) = path.try_into() {
94                if height
95                    < &Height::new(path.revision_number, path.revision_height).expect("no error")
96                {
97                    return Some(path);
98                }
99            }
100            None
101        });
102
103        let consensus_state = found_path
104            .map(|path| {
105                self.consensus_state_store
106                    .get(StoreHeight::Pending, &path)
107                    .ok_or_else(|| {
108                        HostError::failed_to_retrieve(format!(
109                            "consensus state for client `{}` at height `{}`",
110                            client_id.clone(),
111                            *height
112                        ))
113                    })
114            })
115            .transpose()
116            .map_err(HostError::missing_state)?;
117
118        Ok(consensus_state)
119    }
120
121    fn prev_consensus_state(
122        &self,
123        client_id: &ClientId,
124        height: &Height,
125    ) -> Result<Option<Self::ConsensusStateRef>, HostError> {
126        let path = format!("clients/{client_id}/consensusStates").into();
127
128        let keys = self.store.get_keys(&path);
129        let found_path = keys.into_iter().rev().find_map(|path| {
130            if let Ok(Path::ClientConsensusState(path)) = path.try_into() {
131                if height
132                    > &Height::new(path.revision_number, path.revision_height).expect("no error")
133                {
134                    return Some(path);
135                }
136            }
137            None
138        });
139
140        let consensus_state = found_path
141            .map(|path| {
142                self.consensus_state_store
143                    .get(StoreHeight::Pending, &path)
144                    .ok_or_else(|| {
145                        HostError::failed_to_retrieve(format!(
146                            "consensus state for client `{}` at height `{}`",
147                            client_id.clone(),
148                            *height
149                        ))
150                    })
151            })
152            .transpose()
153            .map_err(HostError::missing_state)?;
154
155        Ok(consensus_state)
156    }
157}
158
159impl<S> ClientValidationContext for MockIbcStore<S>
160where
161    S: ProvableStore + Debug,
162{
163    type ClientStateRef = AnyClientState;
164    type ConsensusStateRef = AnyConsensusState;
165
166    fn client_state(&self, client_id: &ClientId) -> Result<Self::ClientStateRef, HostError> {
167        self.client_state_store
168            .get(StoreHeight::Pending, &ClientStatePath(client_id.clone()))
169            .ok_or(HostError::failed_to_retrieve(format!(
170                "missing client state for client {}",
171                client_id.clone()
172            )))
173    }
174
175    fn consensus_state(
176        &self,
177        client_cons_state_path: &ClientConsensusStatePath,
178    ) -> Result<AnyConsensusState, HostError> {
179        let height = Height::new(
180            client_cons_state_path.revision_number,
181            client_cons_state_path.revision_height,
182        )
183        .map_err(HostError::invalid_state)?;
184        let consensus_state = self
185            .consensus_state_store
186            .get(StoreHeight::Pending, client_cons_state_path)
187            .ok_or(HostError::failed_to_retrieve(format!(
188                "consensus state for client `{}` at height `{}`",
189                client_cons_state_path.client_id.clone(),
190                height
191            )))?;
192
193        Ok(consensus_state)
194    }
195
196    /// Returns the time and height when the client state for the given
197    /// [`ClientId`] was updated with a header for the given [`Height`]
198    fn client_update_meta(
199        &self,
200        client_id: &ClientId,
201        height: &Height,
202    ) -> Result<(Timestamp, Height), HostError> {
203        let client_update_time_path = ClientUpdateTimePath::new(
204            client_id.clone(),
205            height.revision_number(),
206            height.revision_height(),
207        );
208        let processed_timestamp = self
209            .client_processed_times
210            .get(StoreHeight::Pending, &client_update_time_path)
211            .ok_or(HostError::failed_to_retrieve(format!(
212                "missing client update metadata for client {} at height {}",
213                client_id.clone(),
214                *height,
215            )))?;
216        let client_update_height_path = ClientUpdateHeightPath::new(
217            client_id.clone(),
218            height.revision_number(),
219            height.revision_height(),
220        );
221        let processed_height = self
222            .client_processed_heights
223            .get(StoreHeight::Pending, &client_update_height_path)
224            .ok_or(HostError::failed_to_retrieve(format!(
225                "missing client update metadata for client {} at height {}",
226                client_id.clone(),
227                *height,
228            )))?;
229
230        Ok((processed_timestamp, processed_height))
231    }
232}
233
234impl<S> ClientExecutionContext for MockIbcStore<S>
235where
236    S: ProvableStore + Debug,
237{
238    type ClientStateMut = AnyClientState;
239
240    /// Called upon successful client creation and update
241    fn store_client_state(
242        &mut self,
243        client_state_path: ClientStatePath,
244        client_state: Self::ClientStateRef,
245    ) -> Result<(), HostError> {
246        self.client_state_store
247            .set(client_state_path, client_state)
248            .map_err(|e| HostError::failed_to_store(format!("{e:?}")))?;
249
250        Ok(())
251    }
252
253    /// Called upon successful client creation and update
254    fn store_consensus_state(
255        &mut self,
256        consensus_state_path: ClientConsensusStatePath,
257        consensus_state: Self::ConsensusStateRef,
258    ) -> Result<(), HostError> {
259        self.consensus_state_store
260            .set(consensus_state_path, consensus_state)
261            .map_err(|e| HostError::failed_to_store(format!("{e:?}")))?;
262        Ok(())
263    }
264
265    fn delete_consensus_state(
266        &mut self,
267        consensus_state_path: ClientConsensusStatePath,
268    ) -> Result<(), HostError> {
269        self.consensus_state_store.delete(consensus_state_path);
270        Ok(())
271    }
272
273    /// Delete the update metadata associated with the client at the specified
274    /// height.
275    fn delete_update_meta(&mut self, client_id: ClientId, height: Height) -> Result<(), HostError> {
276        let client_update_time_path = ClientUpdateTimePath::new(
277            client_id.clone(),
278            height.revision_number(),
279            height.revision_height(),
280        );
281        self.client_processed_times.delete(client_update_time_path);
282        let client_update_height_path = ClientUpdateHeightPath::new(
283            client_id,
284            height.revision_number(),
285            height.revision_height(),
286        );
287        self.client_processed_heights
288            .delete(client_update_height_path);
289        Ok(())
290    }
291
292    /// Called upon successful client update. Implementations are expected to
293    /// use this to record the time and height at which this update (or header)
294    /// was processed.
295    fn store_update_meta(
296        &mut self,
297        client_id: ClientId,
298        height: Height,
299        host_timestamp: Timestamp,
300        host_height: Height,
301    ) -> Result<(), HostError> {
302        let client_update_time_path = ClientUpdateTimePath::new(
303            client_id.clone(),
304            height.revision_number(),
305            height.revision_height(),
306        );
307        self.client_processed_times
308            .set(client_update_time_path, host_timestamp)
309            .map_err(|e| HostError::failed_to_store(format!("{e:?}")))?;
310        let client_update_height_path = ClientUpdateHeightPath::new(
311            client_id,
312            height.revision_number(),
313            height.revision_height(),
314        );
315        self.client_processed_heights
316            .set(client_update_height_path, host_height)
317            .map_err(|e| HostError::failed_to_store(format!("{e:?}")))?;
318        Ok(())
319    }
320}