piecrust/
vm.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4//
5// Copyright (c) DUSK NETWORK. All rights reserved.
6
7use std::any::Any;
8use std::borrow::Cow;
9use std::collections::BTreeMap;
10use std::fmt::{self, Debug, Formatter};
11use std::path::Path;
12use std::sync::Arc;
13use std::thread;
14
15use dusk_wasmtime::{
16    Config, Engine, ModuleVersionStrategy, OperatorCost, OptLevel, Strategy,
17    WasmBacktraceDetails,
18};
19use tempfile::tempdir;
20
21use crate::config::BYTE_STORE_COST;
22use crate::session::{Session, SessionData};
23use crate::store::ContractStore;
24use crate::Error::{self, PersistenceError};
25
26fn config() -> Config {
27    let mut config = Config::new();
28
29    // Neither WASM backtrace, nor native unwind info.
30    config.wasm_backtrace(false);
31    config.wasm_backtrace_details(WasmBacktraceDetails::Disable);
32
33    config.native_unwind_info(false);
34
35    // 512KiB of max stack is the default, but we want to be explicit about it.
36    config.max_wasm_stack(0x80000);
37    config.consume_fuel(true);
38
39    config.strategy(Strategy::Cranelift);
40    config.cranelift_opt_level(OptLevel::SpeedAndSize);
41    // We need entirely deterministic computation
42    config.cranelift_nan_canonicalization(true);
43
44    // Host memory creator is set in the session.
45    // config.with_host_memory()
46
47    config.static_memory_forced(true);
48    config.static_memory_guard_size(0);
49    config.dynamic_memory_guard_size(0);
50    config.guard_before_linear_memory(false);
51    config.memory_init_cow(false);
52
53    config
54        .module_version(ModuleVersionStrategy::Custom(String::from("piecrust")))
55        .expect("Module version should be valid");
56    config.generate_address_map(false);
57    config.macos_use_mach_ports(false);
58
59    // Support 64-bit memories
60    config.wasm_memory64(true);
61
62    const BYTE4_STORE_COST: i64 = 4 * BYTE_STORE_COST;
63    const BYTE8_STORE_COST: i64 = 8 * BYTE_STORE_COST;
64    const BYTE16_STORE_COST: i64 = 16 * BYTE_STORE_COST;
65
66    config.operator_cost(OperatorCost {
67        I32Store: BYTE4_STORE_COST,
68        F32Store: BYTE4_STORE_COST,
69        I32Store8: BYTE4_STORE_COST,
70        I32Store16: BYTE4_STORE_COST,
71        I32AtomicStore: BYTE4_STORE_COST,
72        I32AtomicStore8: BYTE4_STORE_COST,
73        I32AtomicStore16: BYTE4_STORE_COST,
74
75        I64Store: BYTE8_STORE_COST,
76        F64Store: BYTE8_STORE_COST,
77        I64Store8: BYTE8_STORE_COST,
78        I64Store16: BYTE8_STORE_COST,
79        I64Store32: BYTE8_STORE_COST,
80        I64AtomicStore: BYTE8_STORE_COST,
81        I64AtomicStore8: BYTE8_STORE_COST,
82        I64AtomicStore16: BYTE8_STORE_COST,
83        I64AtomicStore32: BYTE8_STORE_COST,
84
85        V128Store: BYTE16_STORE_COST,
86        V128Store8Lane: BYTE16_STORE_COST,
87        V128Store16Lane: BYTE16_STORE_COST,
88        V128Store32Lane: BYTE16_STORE_COST,
89        V128Store64Lane: BYTE16_STORE_COST,
90
91        ..Default::default()
92    });
93
94    config
95}
96
97/// A handle to the piecrust virtual machine.
98///
99/// It is instantiated using [`new`] or [`ephemeral`], and can be used to spawn
100/// multiple [`Session`]s using [`session`].
101///
102/// These sessions are synchronized with the help of a sync loop. [`Deletions`]
103/// are assured to not delete any commits used as a base for sessions until
104/// these are dropped. A handle to this loop is available at [`sync_thread`].
105///
106/// Users are encouraged to instantiate a `VM` once during the lifetime of their
107/// program and spawn sessions as needed.
108///
109/// [`new`]: VM::new
110/// [`ephemeral`]: VM::ephemeral
111/// [`Session`]: Session
112/// [`session`]: VM::session
113/// [`Deletions`]: VM::delete_commit
114/// [`sync_thread`]: VM::sync_thread
115pub struct VM {
116    engine: Engine,
117    host_queries: HostQueries,
118    store: ContractStore,
119}
120
121impl Debug for VM {
122    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
123        f.debug_struct("VM")
124            .field("config", self.engine.config())
125            .field("host_queries", &self.host_queries)
126            .field("store", &self.store)
127            .finish()
128    }
129}
130
131impl VM {
132    /// Creates a new `VM`, reading the given `dir`ectory for existing commits
133    /// and bytecode.
134    ///
135    /// The directory will be used to save any future session commits made by
136    /// this `VM` instance.
137    ///
138    /// # Errors
139    /// If the directory contains unparseable or inconsistent data.
140    pub fn new<P: AsRef<Path>>(root_dir: P) -> Result<Self, Error> {
141        tracing::trace!("vm::new");
142        let config = config();
143
144        let engine = Engine::new(&config).expect(
145            "Configuration should be valid since its set at compile time",
146        );
147
148        tracing::trace!("before ContractStore::new");
149        let mut store = ContractStore::new(engine.clone(), root_dir)
150            .map_err(|err| PersistenceError(Arc::new(err)))?;
151        tracing::trace!("before ContractStore::finish_new");
152        store
153            .finish_new()
154            .map_err(|err| PersistenceError(Arc::new(err)))?;
155        tracing::trace!("after ContractStore::finish_new");
156
157        Ok(Self {
158            engine,
159            host_queries: HostQueries::default(),
160            store,
161        })
162    }
163
164    /// Creates a new `VM` using a new temporary directory.
165    ///
166    /// Any session commits made by this machine should be considered discarded
167    /// once this `VM` instance drops.
168    ///
169    /// # Errors
170    /// If creating a temporary directory fails.
171    pub fn ephemeral() -> Result<Self, Error> {
172        let tmp = tempdir().map_err(|err| PersistenceError(Arc::new(err)))?;
173        let tmp = tmp.path().to_path_buf();
174
175        let config = config();
176
177        let engine = Engine::new(&config).expect(
178            "Configuration should be valid since its set at compile time",
179        );
180
181        let mut store = ContractStore::new(engine.clone(), tmp)
182            .map_err(|err| PersistenceError(Arc::new(err)))?;
183        store
184            .finish_new()
185            .map_err(|err| PersistenceError(Arc::new(err)))?;
186
187        Ok(Self {
188            engine,
189            host_queries: HostQueries::default(),
190            store,
191        })
192    }
193
194    /// Registers a [host `query`] with the given `name`.
195    ///
196    /// The query will be available to any session spawned *after* this was
197    /// called.
198    ///
199    /// [host `query`]: HostQuery
200    pub fn register_host_query<Q, S>(&mut self, name: S, query: Q)
201    where
202        Q: 'static + HostQuery,
203        S: Into<Cow<'static, str>>,
204    {
205        self.host_queries.insert(name, query);
206    }
207
208    /// Spawn a [`Session`].
209    ///
210    /// # Errors
211    /// If base commit is provided but does not exist.
212    ///
213    /// [`Session`]: Session
214    pub fn session(
215        &self,
216        data: impl Into<SessionData>,
217    ) -> Result<Session, Error> {
218        let data = data.into();
219        let contract_session = match data.base {
220            Some(base) => self
221                .store
222                .session(base.into())
223                .map_err(|err| PersistenceError(Arc::new(err)))?,
224            _ => self.store.genesis_session(),
225        };
226        Ok(Session::new(
227            self.engine.clone(),
228            contract_session,
229            self.host_queries.clone(),
230            data,
231        ))
232    }
233
234    /// Return all existing commits.
235    pub fn commits(&self) -> Vec<[u8; 32]> {
236        self.store.commits().into_iter().map(Into::into).collect()
237    }
238
239    /// Deletes the given commit from disk.
240    pub fn delete_commit(&self, root: [u8; 32]) -> Result<(), Error> {
241        self.store
242            .delete_commit(root.into())
243            .map_err(|err| PersistenceError(Arc::new(err)))
244    }
245
246    /// Finalizes the given commit on disk.
247    pub fn finalize_commit(&self, root: [u8; 32]) -> Result<(), Error> {
248        self.store
249            .finalize_commit(root.into())
250            .map_err(|err| PersistenceError(Arc::new(err)))
251    }
252
253    /// Return the root directory of the virtual machine.
254    ///
255    /// This is either the directory passed in by using [`new`], or the
256    /// temporary directory created using [`ephemeral`].
257    ///
258    /// [`new`]: VM::new
259    /// [`ephemeral`]: VM::ephemeral
260    pub fn root_dir(&self) -> &Path {
261        self.store.root_dir()
262    }
263
264    /// Returns a reference to the synchronization thread.
265    pub fn sync_thread(&self) -> &thread::Thread {
266        self.store.sync_loop()
267    }
268}
269
270#[derive(Default, Clone)]
271pub struct HostQueries {
272    map: BTreeMap<Cow<'static, str>, Arc<dyn HostQuery>>,
273}
274
275impl Debug for HostQueries {
276    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
277        f.debug_list().entries(self.map.keys()).finish()
278    }
279}
280
281impl HostQueries {
282    pub fn insert<Q, S>(&mut self, name: S, query: Q)
283    where
284        Q: 'static + HostQuery,
285        S: Into<Cow<'static, str>>,
286    {
287        self.map.insert(name.into(), Arc::new(query));
288    }
289
290    pub fn get(&self, name: &str) -> Option<&dyn HostQuery> {
291        self.map.get(name).map(|q| q.as_ref())
292    }
293}
294
295/// A query executable on the host.
296///
297/// The buffer containing the argument the contract used to call the query,
298/// together with the argument's length, are passed as arguments to the
299/// function, and should be processed first. Once this is done, the implementor
300/// should emplace the return of the query in the same buffer, and return the
301/// length written.
302///
303/// Implementers of `Fn(&mut [u8], u32) -> u32` can be used as a `HostQuery`,
304/// but the cost will be 0.
305pub trait HostQuery: Send + Sync {
306    /// Deserialize the argument buffer and return the price of the query.
307    ///
308    /// The buffer passed will be of the length of the argument the contract
309    /// used to call the query.
310    ///
311    /// Any information needed to perform the query after deserializing the
312    /// argument should be stored in `arg`, and will be passed to [`execute`],
313    /// if there's enough gas to execute the query.
314    ///
315    /// [`execute`]: HostQuery::execute
316    fn deserialize_and_price(
317        &self,
318        arg_buf: &[u8],
319        arg: &mut Box<dyn Any>,
320    ) -> u64;
321
322    /// Perform the query and return the length of the result written to the
323    /// argument buffer.
324    ///
325    /// The whole argument buffer is passed, together with any information
326    /// stored in `arg` previously, during [`deserialize_and_price`].
327    ///
328    /// [`deserialize_and_price`]: HostQuery::deserialize_and_price
329    fn execute(&self, arg: &Box<dyn Any>, arg_buf: &mut [u8]) -> u32;
330}
331
332/// An implementer of `Fn(&mut [u8], u32) -> u32` can be used as a `HostQuery`,
333/// and the cost will be 0.
334impl<F> HostQuery for F
335where
336    F: Send + Sync + Fn(&mut [u8], u32) -> u32,
337{
338    fn deserialize_and_price(
339        &self,
340        arg_buf: &[u8],
341        arg: &mut Box<dyn Any>,
342    ) -> u64 {
343        let len = Box::new(arg_buf.len() as u32);
344        *arg = len;
345        0
346    }
347
348    fn execute(&self, arg: &Box<dyn Any>, arg_buf: &mut [u8]) -> u32 {
349        let arg_len = *arg.downcast_ref::<u32>().unwrap();
350        self(arg_buf, arg_len)
351    }
352}