ibc_testkit/
context.rs

1use core::fmt::Debug;
2use core::time::Duration;
3
4use basecoin_store::context::ProvableStore;
5use basecoin_store::impls::InMemoryStore;
6use ibc::core::channel::types::channel::ChannelEnd;
7use ibc::core::channel::types::commitment::PacketCommitment;
8use ibc::core::client::context::client_state::ClientStateValidation;
9use ibc::core::client::context::{ClientExecutionContext, ClientValidationContext};
10use ibc::core::client::types::Height;
11use ibc::core::connection::types::ConnectionEnd;
12use ibc::core::entrypoint::{dispatch, execute, validate};
13use ibc::core::handler::types::error::HandlerError;
14use ibc::core::handler::types::events::IbcEvent;
15use ibc::core::handler::types::msgs::MsgEnvelope;
16use ibc::core::host::types::identifiers::{ChannelId, ClientId, ConnectionId, PortId, Sequence};
17use ibc::core::host::types::path::{
18    ChannelEndPath, ClientConsensusStatePath, ClientStatePath, CommitmentPath, ConnectionPath,
19    SeqAckPath, SeqRecvPath, SeqSendPath,
20};
21use ibc::core::host::{ExecutionContext, ValidationContext};
22use ibc::primitives::prelude::*;
23use ibc::primitives::Timestamp;
24
25use super::testapp::ibc::core::types::{LightClientState, MockIbcStore};
26use crate::fixtures::core::context::dummy_store_generic_test_context;
27use crate::hosts::{HostClientState, MockHost, TendermintHost, TestBlock, TestHeader, TestHost};
28use crate::testapp::ibc::clients::{AnyClientState, AnyConsensusState};
29use crate::testapp::ibc::core::router::MockRouter;
30use crate::testapp::ibc::core::types::DEFAULT_BLOCK_TIME_SECS;
31
32/// A context implementing the dependencies necessary for testing any IBC module.
33#[derive(Debug)]
34pub struct StoreGenericTestContext<S, H>
35where
36    S: ProvableStore + Debug,
37    H: TestHost,
38    HostClientState<H>: ClientStateValidation<MockIbcStore<S>>,
39{
40    /// The multi store of the context.
41    /// This is where the IBC store root is stored at IBC commitment prefix.
42    pub multi_store: S,
43
44    /// The type of host chain underlying this mock context.
45    pub host: H,
46
47    /// An object that stores all IBC related data.
48    pub ibc_store: MockIbcStore<S>,
49
50    /// A router that can route messages to the appropriate IBC application.
51    pub ibc_router: MockRouter,
52}
53
54/// A mock store type using basecoin-storage implementations.
55pub type MockStore = InMemoryStore;
56/// A [`StoreGenericTestContext`] using [`MockStore`].
57pub type TestContext<H> = StoreGenericTestContext<MockStore, H>;
58/// A [`StoreGenericTestContext`] using [`MockStore`] and [`MockHost`].
59pub type MockContext = TestContext<MockHost>;
60/// A [`StoreGenericTestContext`] using [`MockStore`] and [`TendermintHost`].
61pub type TendermintContext = TestContext<TendermintHost>;
62
63/// Returns a [`StoreGenericTestContext`] with bare minimum initialization: no clients, no connections, and no channels are
64/// present, and the chain has Height(5). This should be used sparingly, mostly for testing the
65/// creation of new domain objects.
66impl<S, H> Default for StoreGenericTestContext<S, H>
67where
68    S: ProvableStore + Debug + Default,
69    H: TestHost,
70    HostClientState<H>: ClientStateValidation<MockIbcStore<S>>,
71{
72    fn default() -> Self {
73        dummy_store_generic_test_context().call()
74    }
75}
76
77/// Implementation of internal interface for use in testing. The methods in this interface should
78/// _not_ be accessible to any ICS handler.
79impl<S, H> StoreGenericTestContext<S, H>
80where
81    S: ProvableStore + Debug,
82    H: TestHost,
83    HostClientState<H>: ClientStateValidation<MockIbcStore<S>>,
84{
85    /// Returns an immutable reference to the IBC store.
86    pub fn ibc_store(&self) -> &MockIbcStore<S> {
87        &self.ibc_store
88    }
89
90    /// Returns a mutable reference to the IBC store.
91    pub fn ibc_store_mut(&mut self) -> &mut MockIbcStore<S> {
92        &mut self.ibc_store
93    }
94
95    /// Returns an immutable reference to the IBC router.
96    pub fn ibc_router(&self) -> &MockRouter {
97        &self.ibc_router
98    }
99
100    /// Returns a mutable reference to the IBC router.
101    pub fn ibc_router_mut(&mut self) -> &mut MockRouter {
102        &mut self.ibc_router
103    }
104
105    /// Returns the block at the given height from the host chain, if exists.
106    pub fn host_block(&self, target_height: &Height) -> Option<H::Block> {
107        self.host.get_block(target_height)
108    }
109
110    /// Returns the latest block from the host chain.
111    pub fn query_latest_block(&self) -> Option<H::Block> {
112        self.host.get_block(&self.latest_height())
113    }
114
115    /// Returns the latest height of client state for the given [`ClientId`].
116    pub fn light_client_latest_height(&self, client_id: &ClientId) -> Height {
117        self.ibc_store
118            .client_state(client_id)
119            .expect("client state exists")
120            .latest_height()
121    }
122
123    /// Advances the host chain height to the given target height.
124    pub fn advance_block_up_to_height(mut self, target_height: Height) -> Self {
125        let latest_height = self.host.latest_height();
126        if target_height.revision_number() != latest_height.revision_number() {
127            panic!("Cannot advance history of the chain to a different revision number!")
128        } else if target_height.revision_height() < latest_height.revision_height() {
129            panic!("Cannot rewind history of the chain to a smaller revision height!")
130        } else {
131            // Repeatedly advance the host chain height till we hit the desired height
132            while self.host.latest_height().revision_height() < target_height.revision_height() {
133                self.advance_block_height()
134            }
135        }
136        self
137    }
138
139    /// Advance the first height of the host chain by generating a genesis block.
140    ///
141    /// This method is same as [`Self::advance_genesis_height`].
142    /// But it bootstraps the genesis block by height 1 and `genesis_time`.
143    ///
144    /// The method starts and ends with [`Self::end_block`] and [`Self::begin_block`], just
145    /// like the [`Self::advance_block_height_with_params`], so that it can advance to next height
146    /// i.e. height 2 - just by calling [`Self::advance_block_height_with_params`].
147    pub fn advance_genesis_height(&mut self, genesis_time: Timestamp, params: &H::BlockParams) {
148        self.end_block();
149
150        // commit multi store
151        let multi_store_commitment = self.multi_store.commit().expect("no error");
152
153        // generate a genesis block
154        // this is basically self.host.produce_block() but with
155        // block height 1 and block timestamp `genesis_time`.
156        let genesis_block =
157            self.host
158                .generate_block(multi_store_commitment, 1, genesis_time, params);
159
160        // push the genesis block to the host
161        self.host.push_block(genesis_block);
162
163        self.begin_block();
164    }
165
166    /// Begin a new block on the context.
167    ///
168    /// This method commits the required metadata from the last block generation
169    /// and consensus, and prepares the context for the next block. This includes
170    /// the latest consensus state and the latest IBC commitment proof.
171    pub fn begin_block(&mut self) {
172        let consensus_state = self
173            .host
174            .latest_block()
175            .into_header()
176            .into_consensus_state()
177            .into();
178
179        let ibc_commitment_proof = self
180            .multi_store
181            .get_proof(
182                self.host.latest_height().revision_height().into(),
183                &self
184                    .ibc_store
185                    .commitment_prefix()
186                    .as_bytes()
187                    .try_into()
188                    .expect("valid utf8 prefix"),
189            )
190            .expect("no error");
191
192        self.ibc_store.begin_block(
193            self.host.latest_height().revision_height(),
194            consensus_state,
195            ibc_commitment_proof,
196        );
197    }
198
199    /// End the current block on the context.
200    ///
201    /// This method commits the state of the IBC store and the host's multi store.
202    pub fn end_block(&mut self) {
203        // commit ibc store
204        let ibc_store_commitment = self.ibc_store.end_block().expect("no error");
205
206        // commit ibc store commitment in multi store
207        self.multi_store
208            .set(
209                self.ibc_store
210                    .commitment_prefix()
211                    .as_bytes()
212                    .try_into()
213                    .expect("valid utf8 prefix"),
214                ibc_store_commitment,
215            )
216            .expect("no error");
217    }
218
219    /// Commit store state to the current block of the host chain by:
220    /// - Committing the state to the context's multi store.
221    /// - Generating a new block with the commitment.
222    /// - Adding the generated block to the host's block history.
223    pub fn commit_state_to_host(&mut self, block_time: Duration, params: &H::BlockParams) {
224        // commit the multi store
225        let multi_store_commitment = self.multi_store.commit().expect("no error");
226        // generate a new block and add it to the block history
227        self.host
228            .commit_block(multi_store_commitment, block_time, params);
229    }
230
231    /// Advances the host chain height by ending the current block, producing a new block, and
232    /// beginning the next block.
233    pub fn advance_block_height_with_params(
234        &mut self,
235        block_time: Duration,
236        params: &H::BlockParams,
237    ) {
238        self.end_block();
239        self.commit_state_to_host(block_time, params);
240        self.begin_block();
241    }
242
243    /// Convenience method to advance the host chain height using default parameters.
244    pub fn advance_block_height(&mut self) {
245        self.advance_block_height_with_params(
246            Duration::from_secs(DEFAULT_BLOCK_TIME_SECS),
247            &Default::default(),
248        )
249    }
250
251    /// Returns the latest height of the host chain.
252    pub fn latest_height(&self) -> Height {
253        let latest_ibc_height = self.ibc_store.host_height().expect("Never fails");
254        let latest_host_height = self.host.latest_height();
255        assert_eq!(
256            latest_ibc_height, latest_host_height,
257            "The IBC store and the host chain must have the same height"
258        );
259        latest_ibc_height
260    }
261
262    /// Returns the latest timestamp of the host chain.
263    pub fn latest_timestamp(&self) -> Timestamp {
264        self.host.latest_block().timestamp()
265    }
266
267    /// Returns the timestamp at the given height.
268    pub fn timestamp_at(&self, height: Height) -> Timestamp {
269        self.host
270            .get_block(&height)
271            .expect("block exists")
272            .timestamp()
273    }
274
275    /// Bootstraps the context with a client state and its corresponding [`ClientId`].
276    pub fn with_client_state(mut self, client_id: &ClientId, client_state: AnyClientState) -> Self {
277        let client_state_path = ClientStatePath::new(client_id.clone());
278        self.ibc_store
279            .store_client_state(client_state_path, client_state)
280            .expect("error writing to store");
281        self
282    }
283
284    /// Bootstraps the context with a consensus state and its corresponding [`ClientId`] and [`Height`].
285    pub fn with_consensus_state(
286        mut self,
287        client_id: &ClientId,
288        height: Height,
289        consensus_state: AnyConsensusState,
290    ) -> Self {
291        let consensus_state_path = ClientConsensusStatePath::new(
292            client_id.clone(),
293            height.revision_number(),
294            height.revision_height(),
295        );
296        self.ibc_store
297            .store_consensus_state(consensus_state_path, consensus_state)
298            .expect("error writing to store");
299
300        self
301    }
302
303    /// Generates a light client for the host by generating a client
304    /// state, as well as generating consensus states for each
305    /// consensus height.
306    pub fn generate_light_client(
307        &self,
308        mut consensus_heights: Vec<Height>,
309        client_params: &H::LightClientParams,
310    ) -> LightClientState<H> {
311        let client_height = if let Some(&height) = consensus_heights.last() {
312            height
313        } else {
314            consensus_heights.push(self.latest_height());
315            self.latest_height()
316        };
317
318        let client_state = self
319            .host
320            .generate_client_state(&client_height, client_params);
321
322        let consensus_states = consensus_heights
323            .into_iter()
324            .map(|height| {
325                (
326                    height,
327                    self.host_block(&height)
328                        .expect("block exists")
329                        .into_header()
330                        .into_consensus_state(),
331                )
332            })
333            .collect();
334
335        LightClientState {
336            client_state,
337            consensus_states,
338        }
339    }
340
341    /// Bootstrap a light client with ClientState and its ConsensusState(s) to this context.
342    pub fn with_light_client<RH>(
343        mut self,
344        client_id: &ClientId,
345        light_client: LightClientState<RH>,
346    ) -> Self
347    where
348        RH: TestHost,
349    {
350        self = self.with_client_state(client_id, light_client.client_state.into());
351
352        for (height, consensus_state) in light_client.consensus_states {
353            self = self.with_consensus_state(client_id, height, consensus_state.into());
354
355            self.ibc_store
356                .store_update_meta(
357                    client_id.clone(),
358                    height,
359                    self.latest_timestamp(),
360                    self.latest_height(),
361                )
362                .expect("error writing to store");
363        }
364
365        self
366    }
367
368    /// Bootstraps an IBC connection to this context.
369    ///
370    /// This does not bootstrap any light client.
371    pub fn with_connection(
372        mut self,
373        connection_id: ConnectionId,
374        connection_end: ConnectionEnd,
375    ) -> Self {
376        let connection_path = ConnectionPath::new(&connection_id);
377        self.ibc_store
378            .store_connection(&connection_path, connection_end)
379            .expect("error writing to store");
380        self
381    }
382
383    /// Bootstraps an IBC channel to this context.
384    ///
385    /// This does not bootstrap any corresponding IBC connection or light client.
386    pub fn with_channel(
387        mut self,
388        port_id: PortId,
389        chan_id: ChannelId,
390        channel_end: ChannelEnd,
391    ) -> Self {
392        let channel_end_path = ChannelEndPath::new(&port_id, &chan_id);
393        self.ibc_store
394            .store_channel(&channel_end_path, channel_end)
395            .expect("error writing to store");
396        self
397    }
398
399    /// Bootstraps a send sequence to this context.
400    ///
401    /// This does not bootstrap any corresponding IBC channel, connection or light client.
402    pub fn with_send_sequence(
403        mut self,
404        port_id: PortId,
405        chan_id: ChannelId,
406        seq_number: Sequence,
407    ) -> Self {
408        let seq_send_path = SeqSendPath::new(&port_id, &chan_id);
409        self.ibc_store
410            .store_next_sequence_send(&seq_send_path, seq_number)
411            .expect("error writing to store");
412        self
413    }
414
415    /// Bootstraps a receive sequence to this context.
416    ///
417    /// This does not bootstrap any corresponding IBC channel, connection or light client.
418    pub fn with_recv_sequence(
419        mut self,
420        port_id: PortId,
421        chan_id: ChannelId,
422        seq_number: Sequence,
423    ) -> Self {
424        let seq_recv_path = SeqRecvPath::new(&port_id, &chan_id);
425        self.ibc_store
426            .store_next_sequence_recv(&seq_recv_path, seq_number)
427            .expect("error writing to store");
428        self
429    }
430
431    /// Bootstraps an ack sequence to this context.
432    ///
433    /// This does not bootstrap any corresponding IBC channel, connection or light client.
434    pub fn with_ack_sequence(
435        mut self,
436        port_id: PortId,
437        chan_id: ChannelId,
438        seq_number: Sequence,
439    ) -> Self {
440        let seq_ack_path = SeqAckPath::new(&port_id, &chan_id);
441        self.ibc_store
442            .store_next_sequence_ack(&seq_ack_path, seq_number)
443            .expect("error writing to store");
444        self
445    }
446
447    /// Bootstraps a packet commitment to this context.
448    ///
449    /// This does not bootstrap any corresponding IBC channel, connection or light client.
450    pub fn with_packet_commitment(
451        mut self,
452        port_id: PortId,
453        chan_id: ChannelId,
454        seq: Sequence,
455        data: PacketCommitment,
456    ) -> Self {
457        let commitment_path = CommitmentPath::new(&port_id, &chan_id, seq);
458        self.ibc_store
459            .store_packet_commitment(&commitment_path, data)
460            .expect("error writing to store");
461        self
462    }
463
464    /// Calls [`validate`] function on [`MsgEnvelope`] using the context's IBC store and router.
465    pub fn validate(&mut self, msg: MsgEnvelope) -> Result<(), HandlerError> {
466        validate(&self.ibc_store, &self.ibc_router, msg)
467    }
468
469    /// Calls [`execute`] function on [`MsgEnvelope`] using the context's IBC store and router.
470    pub fn execute(&mut self, msg: MsgEnvelope) -> Result<(), HandlerError> {
471        execute(&mut self.ibc_store, &mut self.ibc_router, msg)
472    }
473
474    /// Calls [`dispatch`] function on [`MsgEnvelope`] using the context's IBC store and router.
475    pub fn dispatch(&mut self, msg: MsgEnvelope) -> Result<(), HandlerError> {
476        dispatch(&mut self.ibc_store, &mut self.ibc_router, msg)
477    }
478
479    /// A datagram passes from the relayer to the IBC module (on host chain).
480    /// Alternative method to `Ics18Context::send` that does not exercise any serialization.
481    /// Used in testing the Ics18 algorithms, hence this may return an Ics18Error.
482    pub fn deliver(&mut self, msg: MsgEnvelope) -> Result<(), HandlerError> {
483        self.dispatch(msg)?;
484
485        // Create a new block.
486        self.advance_block_height();
487        Ok(())
488    }
489
490    /// Returns all the events that have been emitted by the context's IBC store.
491    pub fn get_events(&self) -> Vec<IbcEvent> {
492        self.ibc_store.events.lock().clone()
493    }
494
495    /// Returns all the logs that have been emitted by the context's IBC store.
496    pub fn get_logs(&self) -> Vec<String> {
497        self.ibc_store.logs.lock().clone()
498    }
499}
500
501#[cfg(test)]
502mod tests {
503    use ibc::core::client::context::consensus_state::ConsensusState;
504
505    use super::*;
506    use crate::hosts::{HostConsensusState, MockHost, TendermintHost};
507    use crate::testapp::ibc::core::types::DefaultIbcStore;
508
509    #[test]
510    fn test_mock_history_validation() {
511        pub struct Test<H: TestHost>
512        where
513            H: TestHost,
514            HostConsensusState<H>: ConsensusState,
515            HostClientState<H>: ClientStateValidation<DefaultIbcStore>,
516        {
517            name: String,
518            ctx: TestContext<H>,
519        }
520
521        fn run_tests<H>(sub_title: &str)
522        where
523            H: TestHost,
524            HostConsensusState<H>: ConsensusState,
525            HostClientState<H>: ClientStateValidation<DefaultIbcStore>,
526        {
527            let cv = 0; // The version to use for all chains.
528
529            let tests: Vec<Test<H>> = vec![
530                Test {
531                    name: "Empty history, small pruning window".to_string(),
532                    ctx: dummy_store_generic_test_context()
533                        .latest_height(Height::new(cv, 1).expect("Never fails"))
534                        .call(),
535                },
536                Test {
537                    name: "Large pruning window".to_string(),
538                    ctx: dummy_store_generic_test_context()
539                        .latest_height(Height::new(cv, 2).expect("Never fails"))
540                        .call(),
541                },
542                Test {
543                    name: "Small pruning window".to_string(),
544                    ctx: dummy_store_generic_test_context()
545                        .latest_height(Height::new(cv, 30).expect("Never fails"))
546                        .call(),
547                },
548                Test {
549                    name: "Small pruning window, small starting height".to_string(),
550                    ctx: dummy_store_generic_test_context()
551                        .latest_height(Height::new(cv, 2).expect("Never fails"))
552                        .call(),
553                },
554                // This is disabled, as now we generate all the blocks till latest_height
555                // Generating 2000 Tendermint blocks is slow.
556                // Test {
557                //     name: "Large pruning window, large starting height".to_string(),
558                //     ctx: dummy_store_generic_test_context()
559                //         .latest_height(Height::new(cv, 2000).expect("Never fails"))
560                //         .call(),
561                // },
562            ];
563
564            for mut test in tests {
565                // All tests should yield a valid context after initialization.
566                assert!(
567                    test.ctx.host.validate().is_ok(),
568                    "failed in test [{}] {} while validating context {:?}",
569                    sub_title,
570                    test.name,
571                    test.ctx
572                );
573
574                let current_height = test.ctx.latest_height();
575
576                // After advancing the chain's height, the context should still be valid.
577                test.ctx.advance_block_height();
578                assert!(
579                    test.ctx.host.validate().is_ok(),
580                    "failed in test [{}] {} while validating context {:?}",
581                    sub_title,
582                    test.name,
583                    test.ctx
584                );
585
586                let next_height = current_height.increment();
587                assert_eq!(
588                    test.ctx.latest_height(),
589                    next_height,
590                    "failed while increasing height for context {:?}",
591                    test.ctx
592                );
593
594                assert_eq!(
595                    test.ctx
596                        .host
597                        .get_block(&current_height)
598                        .expect("Never fails")
599                        .height(),
600                    current_height,
601                    "failed while fetching height {:?} of context {:?}",
602                    current_height,
603                    test.ctx
604                );
605            }
606        }
607
608        run_tests::<MockHost>("Mock Host");
609        run_tests::<TendermintHost>("Synthetic TM Host");
610    }
611}