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