oasis_runtime_sdk/testing/
mock.rs

1//! Mock dispatch context for use in tests.
2use std::collections::BTreeMap;
3
4use oasis_core_runtime::{
5    common::{crypto::mrae::deoxysii, namespace::Namespace, version::Version},
6    consensus::{beacon, roothash, state::ConsensusState, Event},
7    protocol::HostInfo,
8    storage::mkvs,
9    types::EventKind,
10};
11
12use crate::{
13    callformat,
14    context::{Context, RuntimeBatchContext},
15    dispatcher,
16    error::RuntimeError,
17    history,
18    keymanager::KeyManager,
19    module::MigrationHandler,
20    modules,
21    runtime::Runtime,
22    state::{self, CurrentState, TransactionResult},
23    storage::MKVSStore,
24    testing::{configmap, keymanager::MockKeyManagerClient},
25    types::{self, address::SignatureAddressSpec, transaction},
26};
27
28pub struct Config;
29
30impl modules::core::Config for Config {}
31
32/// A mock runtime that only has the core module.
33pub struct EmptyRuntime;
34
35impl Runtime for EmptyRuntime {
36    const VERSION: Version = Version::new(0, 0, 0);
37
38    type Core = modules::core::Module<Config>;
39
40    type Accounts = modules::accounts::Module;
41
42    type Modules = modules::core::Module<Config>;
43
44    fn genesis_state() -> <Self::Modules as MigrationHandler>::Genesis {
45        Default::default()
46    }
47}
48
49struct EmptyHistory;
50
51impl history::HistoryHost for EmptyHistory {
52    fn consensus_state_at(&self, _height: u64) -> Result<ConsensusState, history::Error> {
53        Err(history::Error::FailedToFetchBlock)
54    }
55
56    fn consensus_events_at(
57        &self,
58        _height: u64,
59        _kind: EventKind,
60    ) -> Result<Vec<Event>, history::Error> {
61        Err(history::Error::FailedToFetchEvents)
62    }
63}
64
65/// Mock dispatch context factory.
66pub struct Mock {
67    pub host_info: HostInfo,
68    pub runtime_header: roothash::Header,
69    pub runtime_round_results: roothash::RoundResults,
70    pub consensus_state: ConsensusState,
71    pub history: Box<dyn history::HistoryHost>,
72    pub epoch: beacon::EpochTime,
73
74    pub max_messages: u32,
75}
76
77impl Mock {
78    /// Create a new mock dispatch context.
79    pub fn create_ctx(&mut self) -> RuntimeBatchContext<'_, EmptyRuntime> {
80        self.create_ctx_for_runtime(false)
81    }
82
83    /// Create a new mock dispatch context.
84    pub fn create_ctx_for_runtime<R: Runtime>(
85        &mut self,
86        confidential: bool,
87    ) -> RuntimeBatchContext<'_, R> {
88        RuntimeBatchContext::new(
89            &self.host_info,
90            if confidential {
91                Some(Box::new(MockKeyManagerClient::new()) as Box<dyn KeyManager>)
92            } else {
93                None
94            },
95            &self.runtime_header,
96            &self.runtime_round_results,
97            &self.consensus_state,
98            &self.history,
99            self.epoch,
100            self.max_messages,
101        )
102    }
103
104    /// Create an instance with the given local configuration.
105    pub fn with_local_config(local_config: BTreeMap<String, cbor::Value>) -> Self {
106        // Ensure a current state is always available during tests. Note that one can always use a
107        // different store by calling `CurrentState::enter` explicitly.
108        CurrentState::init_local_fallback();
109
110        let consensus_tree = mkvs::Tree::builder()
111            .with_root_type(mkvs::RootType::State)
112            .build(Box::new(mkvs::sync::NoopReadSyncer));
113
114        Self {
115            host_info: HostInfo {
116                runtime_id: Namespace::default(),
117                consensus_backend: "mock".to_string(),
118                consensus_protocol_version: Version::default(),
119                consensus_chain_context: "test".to_string(),
120                local_config,
121            },
122            runtime_header: roothash::Header::default(),
123            runtime_round_results: roothash::RoundResults::default(),
124            consensus_state: ConsensusState::new(1, consensus_tree),
125            history: Box::new(EmptyHistory),
126            epoch: 1,
127            max_messages: 32,
128        }
129    }
130}
131
132impl Default for Mock {
133    fn default() -> Self {
134        let local_config_for_tests = configmap! {
135            // Allow expensive gas estimation and expensive queries so they can be tested.
136            "estimate_gas_by_simulating_contracts" => true,
137            "allowed_queries" => vec![
138                configmap! {"all_expensive" => true}
139            ],
140        };
141        Self::with_local_config(local_config_for_tests)
142    }
143}
144
145/// Create an empty MKVS store.
146pub fn empty_store() -> MKVSStore<mkvs::OverlayTree<mkvs::Tree>> {
147    let root = mkvs::OverlayTree::new(
148        mkvs::Tree::builder()
149            .with_root_type(mkvs::RootType::State)
150            .build(Box::new(mkvs::sync::NoopReadSyncer)),
151    );
152    MKVSStore::new(root)
153}
154
155/// Create a new mock transaction.
156pub fn transaction() -> transaction::Transaction {
157    transaction::Transaction {
158        version: 1,
159        call: transaction::Call {
160            format: transaction::CallFormat::Plain,
161            method: "mock".to_owned(),
162            body: cbor::Value::Simple(cbor::SimpleValue::NullValue),
163            ..Default::default()
164        },
165        auth_info: transaction::AuthInfo {
166            signer_info: vec![],
167            fee: transaction::Fee {
168                amount: Default::default(),
169                gas: 1_000_000,
170                consensus_messages: 32,
171                ..Default::default()
172            },
173            ..Default::default()
174        },
175    }
176}
177
178/// Options that can be used during mock signer calls.
179#[derive(Clone, Debug)]
180pub struct CallOptions {
181    /// Transaction fee.
182    pub fee: transaction::Fee,
183    /// Should the call be encrypted.
184    pub encrypted: bool,
185}
186
187impl Default for CallOptions {
188    fn default() -> Self {
189        Self {
190            fee: transaction::Fee {
191                amount: Default::default(),
192                gas: 1_000_000,
193                consensus_messages: 0,
194                ..Default::default()
195            },
196            encrypted: false,
197        }
198    }
199}
200
201/// A mock signer for use during tests.
202pub struct Signer {
203    nonce: u64,
204    sigspec: SignatureAddressSpec,
205}
206
207impl Signer {
208    /// Create a new mock signer using the given nonce and signature spec.
209    pub fn new(nonce: u64, sigspec: SignatureAddressSpec) -> Self {
210        Self { nonce, sigspec }
211    }
212
213    /// Address specification for this signer.
214    pub fn sigspec(&self) -> &SignatureAddressSpec {
215        &self.sigspec
216    }
217
218    /// Dispatch a call to the given method.
219    pub fn call<C, B>(&mut self, ctx: &C, method: &str, body: B) -> dispatcher::DispatchResult
220    where
221        C: Context,
222        B: cbor::Encode,
223    {
224        self.call_opts(ctx, method, body, Default::default())
225    }
226
227    /// Dispatch a call to the given method with the given options.
228    pub fn call_opts<C, B>(
229        &mut self,
230        ctx: &C,
231        method: &str,
232        body: B,
233        opts: CallOptions,
234    ) -> dispatcher::DispatchResult
235    where
236        C: Context,
237        B: cbor::Encode,
238    {
239        let mut call = transaction::Call {
240            format: transaction::CallFormat::Plain,
241            method: method.to_owned(),
242            body: cbor::to_value(body),
243            ..Default::default()
244        };
245        if opts.encrypted {
246            let key_pair = deoxysii::generate_key_pair();
247            let nonce = [0u8; deoxysii::NONCE_SIZE];
248            let km = ctx.key_manager().unwrap();
249            let epoch = ctx.epoch();
250            let runtime_keypair = km
251                .get_or_create_ephemeral_keys(callformat::get_key_pair_id(epoch), epoch)
252                .unwrap();
253            let runtime_pk = runtime_keypair.input_keypair.pk;
254            call = transaction::Call {
255                format: transaction::CallFormat::EncryptedX25519DeoxysII,
256                method: "".to_owned(),
257                body: cbor::to_value(types::callformat::CallEnvelopeX25519DeoxysII {
258                    pk: key_pair.0.into(),
259                    nonce,
260                    epoch,
261                    data: deoxysii::box_seal(
262                        &nonce,
263                        cbor::to_vec(call),
264                        vec![],
265                        &runtime_pk.0,
266                        &key_pair.1,
267                    )
268                    .unwrap(),
269                }),
270                ..Default::default()
271            }
272        };
273        let tx = transaction::Transaction {
274            version: 1,
275            call,
276            auth_info: transaction::AuthInfo {
277                signer_info: vec![transaction::SignerInfo::new_sigspec(
278                    self.sigspec.clone(),
279                    self.nonce,
280                )],
281                fee: opts.fee,
282                ..Default::default()
283            },
284        };
285
286        let result = dispatcher::Dispatcher::<C::Runtime>::dispatch_tx(ctx, 1024, tx, 0)
287            .expect("dispatch should work");
288
289        // Increment the nonce.
290        self.nonce += 1;
291
292        result
293    }
294
295    /// Dispatch a query to the given method.
296    pub fn query<C, A, R>(&self, ctx: &C, method: &str, args: A) -> Result<R, RuntimeError>
297    where
298        C: Context,
299        A: cbor::Encode,
300        R: cbor::Decode,
301    {
302        let result = CurrentState::with_transaction_opts(
303            state::Options::new().with_mode(state::Mode::Check),
304            || {
305                let result = dispatcher::Dispatcher::<C::Runtime>::dispatch_query(
306                    ctx,
307                    method,
308                    cbor::to_vec(args),
309                );
310
311                TransactionResult::Rollback(result)
312            },
313        )?;
314        Ok(cbor::from_slice(&result).expect("result should decode correctly"))
315    }
316}