ibc_testkit/testapp/ibc/clients/mock/
client_state.rs

1use core::str::FromStr;
2use core::time::Duration;
3
4use ibc::clients::tendermint::client_state::consensus_state_status;
5use ibc::core::client::context::prelude::*;
6use ibc::core::client::types::error::{ClientError, UpgradeClientError};
7use ibc::core::client::types::{Height, Status};
8use ibc::core::commitment_types::commitment::{
9    CommitmentPrefix, CommitmentProofBytes, CommitmentRoot,
10};
11use ibc::core::host::types::error::{DecodingError, HostError};
12use ibc::core::host::types::identifiers::{ClientId, ClientType};
13use ibc::core::host::types::path::{ClientConsensusStatePath, ClientStatePath, Path, PathBytes};
14use ibc::core::primitives::prelude::*;
15use ibc::core::primitives::Timestamp;
16use ibc::primitives::proto::{Any, Protobuf};
17
18use crate::testapp::ibc::clients::mock::client_state::client_type as mock_client_type;
19use crate::testapp::ibc::clients::mock::consensus_state::MockConsensusState;
20use crate::testapp::ibc::clients::mock::header::{MockHeader, MOCK_HEADER_TYPE_URL};
21use crate::testapp::ibc::clients::mock::misbehaviour::{Misbehaviour, MOCK_MISBEHAVIOUR_TYPE_URL};
22use crate::testapp::ibc::clients::mock::proto::ClientState as RawMockClientState;
23
24pub const MOCK_CLIENT_STATE_TYPE_URL: &str = "/ibc.mock.ClientState";
25pub const MOCK_CLIENT_TYPE: &str = "9999-mock";
26
27pub fn client_type() -> ClientType {
28    ClientType::from_str(MOCK_CLIENT_TYPE).expect("never fails because it's valid client type")
29}
30
31/// A mock of a client state. For an example of a real structure that this mocks, you can see
32/// `ClientState` of ics07_tendermint/client_state.rs.
33#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
34#[derive(Copy, Clone, Debug, PartialEq, Eq)]
35pub struct MockClientState {
36    pub header: MockHeader,
37    pub trusting_period: Duration,
38    pub frozen: bool,
39}
40
41impl MockClientState {
42    /// Initializes a new `MockClientState` with the given `MockHeader` and a
43    /// trusting period of 10 seconds as a default. If the trusting period
44    /// needs to be changed, use the `with_trusting_period` method to override it.
45    pub fn new(header: MockHeader) -> Self {
46        Self {
47            header,
48            trusting_period: Duration::from_secs(64000),
49            frozen: false,
50        }
51    }
52
53    pub fn latest_height(&self) -> Height {
54        self.header.height()
55    }
56
57    pub fn refresh_time(&self) -> Option<Duration> {
58        None
59    }
60
61    pub fn with_trusting_period(self, trusting_period: Duration) -> Self {
62        Self {
63            trusting_period,
64            ..self
65        }
66    }
67
68    pub fn frozen(self) -> Self {
69        Self {
70            frozen: true,
71            ..self
72        }
73    }
74
75    pub fn unfrozen(self) -> Self {
76        Self {
77            frozen: false,
78            ..self
79        }
80    }
81
82    pub fn is_frozen(&self) -> bool {
83        self.frozen
84    }
85
86    fn expired(&self, elapsed: Duration) -> bool {
87        elapsed > self.trusting_period
88    }
89}
90
91impl Protobuf<RawMockClientState> for MockClientState {}
92
93impl TryFrom<RawMockClientState> for MockClientState {
94    type Error = DecodingError;
95
96    fn try_from(raw: RawMockClientState) -> Result<Self, Self::Error> {
97        Ok(Self {
98            header: raw
99                .header
100                .ok_or(DecodingError::missing_raw_data("mock client state header"))?
101                .try_into()?,
102            trusting_period: Duration::from_nanos(raw.trusting_period),
103            frozen: raw.frozen,
104        })
105    }
106}
107
108impl From<MockClientState> for RawMockClientState {
109    fn from(value: MockClientState) -> Self {
110        Self {
111            header: Some(value.header.into()),
112            trusting_period: value
113                .trusting_period
114                .as_nanos()
115                .try_into()
116                .expect("no overflow"),
117            frozen: value.frozen,
118        }
119    }
120}
121
122impl Protobuf<Any> for MockClientState {}
123
124impl TryFrom<Any> for MockClientState {
125    type Error = DecodingError;
126
127    fn try_from(raw: Any) -> Result<Self, Self::Error> {
128        if let MOCK_CLIENT_STATE_TYPE_URL = raw.type_url.as_str() {
129            Protobuf::<RawMockClientState>::decode(raw.value.as_ref()).map_err(Into::into)
130        } else {
131            Err(DecodingError::MismatchedResourceName {
132                expected: MOCK_CLIENT_STATE_TYPE_URL.to_string(),
133                actual: raw.type_url,
134            })
135        }
136    }
137}
138
139impl From<MockClientState> for Any {
140    fn from(client_state: MockClientState) -> Self {
141        Self {
142            type_url: MOCK_CLIENT_STATE_TYPE_URL.to_string(),
143            value: Protobuf::<RawMockClientState>::encode_vec(client_state),
144        }
145    }
146}
147
148pub trait MockClientContext {
149    /// Returns the current timestamp of the local chain.
150    fn host_timestamp(&self) -> Result<Timestamp, HostError>;
151
152    /// Returns the current height of the local chain.
153    fn host_height(&self) -> Result<Height, HostError>;
154}
155
156impl ClientStateCommon for MockClientState {
157    fn verify_consensus_state(
158        &self,
159        consensus_state: Any,
160        host_timestamp: &Timestamp,
161    ) -> Result<(), ClientError> {
162        let mock_consensus_state = MockConsensusState::try_from(consensus_state)?;
163
164        if consensus_state_status(&mock_consensus_state, host_timestamp, self.trusting_period)?
165            .is_expired()
166        {
167            return Err(ClientError::InvalidStatus(Status::Expired));
168        }
169
170        Ok(())
171    }
172
173    fn client_type(&self) -> ClientType {
174        mock_client_type()
175    }
176
177    fn latest_height(&self) -> Height {
178        self.header.height()
179    }
180
181    fn validate_proof_height(&self, proof_height: Height) -> Result<(), ClientError> {
182        if self.latest_height() < proof_height {
183            return Err(ClientError::InsufficientProofHeight {
184                actual: self.latest_height(),
185                expected: proof_height,
186            });
187        }
188        Ok(())
189    }
190
191    fn serialize_path(&self, path: Path) -> Result<PathBytes, ClientError> {
192        Ok(path.to_string().into_bytes().into())
193    }
194
195    fn verify_upgrade_client(
196        &self,
197        upgraded_client_state: Any,
198        upgraded_consensus_state: Any,
199        _proof_upgrade_client: CommitmentProofBytes,
200        _proof_upgrade_consensus_state: CommitmentProofBytes,
201        _root: &CommitmentRoot,
202    ) -> Result<(), ClientError> {
203        let upgraded_mock_client_state = Self::try_from(upgraded_client_state)?;
204        MockConsensusState::try_from(upgraded_consensus_state)?;
205        if self.latest_height() >= upgraded_mock_client_state.latest_height() {
206            return Err(UpgradeClientError::InsufficientUpgradeHeight {
207                upgraded_height: self.latest_height(),
208                client_height: upgraded_mock_client_state.latest_height(),
209            })?;
210        }
211        Ok(())
212    }
213
214    fn verify_membership_raw(
215        &self,
216        _prefix: &CommitmentPrefix,
217        _proof: &CommitmentProofBytes,
218        _root: &CommitmentRoot,
219        _path: PathBytes,
220        _value: Vec<u8>,
221    ) -> Result<(), ClientError> {
222        Ok(())
223    }
224
225    fn verify_non_membership_raw(
226        &self,
227        _prefix: &CommitmentPrefix,
228        _proof: &CommitmentProofBytes,
229        _root: &CommitmentRoot,
230        _path: PathBytes,
231    ) -> Result<(), ClientError> {
232        Ok(())
233    }
234}
235
236impl<V> ClientStateValidation<V> for MockClientState
237where
238    V: ClientValidationContext + MockClientContext,
239    MockConsensusState: Convertible<V::ConsensusStateRef>,
240    <MockConsensusState as TryFrom<V::ConsensusStateRef>>::Error: Into<ClientError>,
241{
242    fn verify_client_message(
243        &self,
244        _ctx: &V,
245        _client_id: &ClientId,
246        client_message: Any,
247    ) -> Result<(), ClientError> {
248        match client_message.type_url.as_str() {
249            MOCK_HEADER_TYPE_URL => {
250                let _header = MockHeader::try_from(client_message)?;
251            }
252            MOCK_MISBEHAVIOUR_TYPE_URL => {
253                let _misbehaviour = Misbehaviour::try_from(client_message)?;
254            }
255            _ => {}
256        }
257
258        Ok(())
259    }
260
261    fn check_for_misbehaviour(
262        &self,
263        _ctx: &V,
264        _client_id: &ClientId,
265        client_message: Any,
266    ) -> Result<bool, ClientError> {
267        match client_message.type_url.as_str() {
268            MOCK_HEADER_TYPE_URL => Ok(false),
269            MOCK_MISBEHAVIOUR_TYPE_URL => {
270                let misbehaviour = Misbehaviour::try_from(client_message)?;
271                let header_1 = misbehaviour.header1;
272                let header_2 = misbehaviour.header2;
273
274                let header_heights_equal = header_1.height() == header_2.height();
275                let headers_are_in_future = self.latest_height() < header_1.height();
276
277                Ok(header_heights_equal && headers_are_in_future)
278            }
279            header_type => Err(ClientError::InvalidHeaderType(header_type.to_owned())),
280        }
281    }
282
283    fn status(&self, ctx: &V, client_id: &ClientId) -> Result<Status, ClientError> {
284        if self.is_frozen() {
285            return Ok(Status::Frozen);
286        }
287
288        let latest_consensus_state: MockConsensusState = {
289            match ctx.consensus_state(&ClientConsensusStatePath::new(
290                client_id.clone(),
291                self.latest_height().revision_number(),
292                self.latest_height().revision_height(),
293            )) {
294                Ok(cs) => cs.try_into().map_err(Into::into)?,
295                // if the client state does not have an associated consensus state for its latest height
296                // then it must be expired
297                Err(_) => return Ok(Status::Expired),
298            }
299        };
300
301        let now = ctx.host_timestamp()?;
302        let elapsed_since_latest_consensus_state = now
303            .duration_since(&latest_consensus_state.timestamp())
304            .ok_or(ClientError::InvalidConsensusStateTimestamp(
305                latest_consensus_state.timestamp(),
306            ))?;
307
308        if self.expired(elapsed_since_latest_consensus_state) {
309            return Ok(Status::Expired);
310        }
311
312        Ok(Status::Active)
313    }
314
315    fn check_substitute(&self, _ctx: &V, _substitute_client_state: Any) -> Result<(), ClientError> {
316        Ok(())
317    }
318}
319
320impl<E> ClientStateExecution<E> for MockClientState
321where
322    E: ClientExecutionContext + MockClientContext,
323    E::ClientStateRef: From<Self>,
324    MockConsensusState: Convertible<E::ConsensusStateRef>,
325    <MockConsensusState as TryFrom<E::ConsensusStateRef>>::Error: Into<ClientError>,
326{
327    fn initialise(
328        &self,
329        ctx: &mut E,
330        client_id: &ClientId,
331        consensus_state: Any,
332    ) -> Result<(), ClientError> {
333        let mock_consensus_state: MockConsensusState = consensus_state.try_into()?;
334
335        ctx.store_client_state(ClientStatePath::new(client_id.clone()), (*self).into())?;
336        ctx.store_consensus_state(
337            ClientConsensusStatePath::new(
338                client_id.clone(),
339                self.latest_height().revision_number(),
340                self.latest_height().revision_height(),
341            ),
342            mock_consensus_state.into(),
343        )?;
344        ctx.store_update_meta(
345            client_id.clone(),
346            self.latest_height(),
347            ctx.host_timestamp()?,
348            ctx.host_height()?,
349        )?;
350
351        Ok(())
352    }
353
354    fn update_state(
355        &self,
356        ctx: &mut E,
357        client_id: &ClientId,
358        header: Any,
359    ) -> Result<Vec<Height>, ClientError> {
360        let header = MockHeader::try_from(header)?;
361        let header_height = header.height;
362
363        let new_client_state = Self::new(header);
364        let new_consensus_state = MockConsensusState::new(header);
365
366        ctx.store_consensus_state(
367            ClientConsensusStatePath::new(
368                client_id.clone(),
369                new_client_state.latest_height().revision_number(),
370                new_client_state.latest_height().revision_height(),
371            ),
372            new_consensus_state.into(),
373        )?;
374        ctx.store_client_state(
375            ClientStatePath::new(client_id.clone()),
376            new_client_state.into(),
377        )?;
378        ctx.store_update_meta(
379            client_id.clone(),
380            header_height,
381            ctx.host_timestamp()?,
382            ctx.host_height()?,
383        )?;
384
385        Ok(vec![header_height])
386    }
387
388    fn update_state_on_misbehaviour(
389        &self,
390        ctx: &mut E,
391        client_id: &ClientId,
392        _client_message: Any,
393    ) -> Result<(), ClientError> {
394        let frozen_client_state = self.frozen();
395
396        ctx.store_client_state(
397            ClientStatePath::new(client_id.clone()),
398            frozen_client_state.into(),
399        )?;
400
401        Ok(())
402    }
403
404    fn update_state_on_upgrade(
405        &self,
406        ctx: &mut E,
407        client_id: &ClientId,
408        upgraded_client_state: Any,
409        upgraded_consensus_state: Any,
410    ) -> Result<Height, ClientError> {
411        let new_client_state = Self::try_from(upgraded_client_state)?;
412        let new_consensus_state: MockConsensusState = upgraded_consensus_state.try_into()?;
413
414        let latest_height = new_client_state.latest_height();
415
416        ctx.store_consensus_state(
417            ClientConsensusStatePath::new(
418                client_id.clone(),
419                latest_height.revision_number(),
420                latest_height.revision_height(),
421            ),
422            new_consensus_state.into(),
423        )?;
424        ctx.store_client_state(
425            ClientStatePath::new(client_id.clone()),
426            new_client_state.into(),
427        )?;
428
429        let host_timestamp = ctx.host_timestamp()?;
430        let host_height = ctx.host_height()?;
431
432        ctx.store_update_meta(
433            client_id.clone(),
434            latest_height,
435            host_timestamp,
436            host_height,
437        )?;
438
439        Ok(latest_height)
440    }
441
442    fn update_on_recovery(
443        &self,
444        ctx: &mut E,
445        subject_client_id: &ClientId,
446        substitute_client_state: Any,
447        substitute_consensus_state: Any,
448    ) -> Result<(), ClientError> {
449        let substitute_client_state = MockClientState::try_from(substitute_client_state)?;
450
451        let latest_height = substitute_client_state.latest_height();
452
453        let new_mock_client_state = MockClientState {
454            frozen: false,
455            ..substitute_client_state
456        };
457
458        let host_timestamp = ctx.host_timestamp()?;
459        let host_height = ctx.host_height()?;
460
461        let mock_consensus_state: MockConsensusState = substitute_consensus_state.try_into()?;
462
463        ctx.store_consensus_state(
464            ClientConsensusStatePath::new(
465                subject_client_id.clone(),
466                new_mock_client_state.latest_height().revision_number(),
467                new_mock_client_state.latest_height().revision_height(),
468            ),
469            mock_consensus_state.into(),
470        )?;
471
472        ctx.store_client_state(
473            ClientStatePath::new(subject_client_id.clone()),
474            new_mock_client_state.into(),
475        )?;
476
477        ctx.store_update_meta(
478            subject_client_id.clone(),
479            latest_height,
480            host_timestamp,
481            host_height,
482        )?;
483
484        Ok(())
485    }
486}
487
488impl From<MockConsensusState> for MockClientState {
489    fn from(cs: MockConsensusState) -> Self {
490        Self::new(cs.header)
491    }
492}
493
494#[cfg(test)]
495mod test {
496    #[cfg(feature = "serde")]
497    #[test]
498    fn test_any_client_state_to_json() {
499        use ibc::primitives::proto::Any;
500
501        use super::{MockClientState, MockHeader};
502
503        let client_state = MockClientState::new(MockHeader::default());
504        let expected =
505            r#"{"typeUrl":"/ibc.mock.ClientState","value":"Cg4KAhABEICAiJ69yIGbFxCAgJDK0sYO"}"#;
506        let json = serde_json::to_string(&Any::from(client_state)).unwrap();
507        assert_eq!(json, expected);
508
509        let proto_any = serde_json::from_str::<Any>(expected).unwrap();
510        assert_eq!(proto_any, Any::from(client_state));
511    }
512}