linera_execution/wasm/
mod.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Support for user applications compiled as WebAssembly (Wasm) modules.
5//!
6//! Requires a WebAssembly runtime to be selected and enabled using one of the following features:
7//!
8//! - `wasmer` enables the [Wasmer](https://wasmer.io/) runtime
9//! - `wasmtime` enables the [Wasmtime](https://wasmtime.dev/) runtime
10
11#![cfg(with_wasm_runtime)]
12
13mod entrypoints;
14mod module_cache;
15#[macro_use]
16mod runtime_api;
17#[cfg(with_wasmer)]
18mod wasmer;
19#[cfg(with_wasmtime)]
20mod wasmtime;
21
22use linera_base::data_types::Bytecode;
23#[cfg(with_metrics)]
24use linera_base::prometheus_util::MeasureLatency as _;
25use thiserror::Error;
26use wasm_instrument::{gas_metering, parity_wasm};
27#[cfg(with_wasmer)]
28use wasmer::{WasmerContractInstance, WasmerServiceInstance};
29#[cfg(with_wasmtime)]
30use wasmtime::{WasmtimeContractInstance, WasmtimeServiceInstance};
31
32pub use self::{
33    entrypoints::{ContractEntrypoints, ServiceEntrypoints},
34    runtime_api::{BaseRuntimeApi, ContractRuntimeApi, RuntimeApiData, ServiceRuntimeApi},
35};
36use crate::{
37    ContractSyncRuntimeHandle, ExecutionError, ServiceSyncRuntimeHandle, UserContractInstance,
38    UserContractModule, UserServiceInstance, UserServiceModule, WasmRuntime,
39};
40
41#[cfg(with_metrics)]
42mod metrics {
43    use std::sync::LazyLock;
44
45    use linera_base::prometheus_util::{exponential_bucket_latencies, register_histogram_vec};
46    use prometheus::HistogramVec;
47
48    pub static CONTRACT_INSTANTIATION_LATENCY: LazyLock<HistogramVec> = LazyLock::new(|| {
49        register_histogram_vec(
50            "wasm_contract_instantiation_latency",
51            "Wasm contract instantiation latency",
52            &[],
53            exponential_bucket_latencies(1.0),
54        )
55    });
56
57    pub static SERVICE_INSTANTIATION_LATENCY: LazyLock<HistogramVec> = LazyLock::new(|| {
58        register_histogram_vec(
59            "wasm_service_instantiation_latency",
60            "Wasm service instantiation latency",
61            &[],
62            exponential_bucket_latencies(1.0),
63        )
64    });
65}
66
67/// A user contract in a compiled WebAssembly module.
68#[derive(Clone)]
69pub enum WasmContractModule {
70    #[cfg(with_wasmer)]
71    Wasmer {
72        engine: ::wasmer::Engine,
73        module: ::wasmer::Module,
74    },
75    #[cfg(with_wasmtime)]
76    Wasmtime { module: ::wasmtime::Module },
77}
78
79impl WasmContractModule {
80    /// Creates a new [`WasmContractModule`] using the WebAssembly module with the provided bytecode.
81    pub async fn new(
82        contract_bytecode: Bytecode,
83        runtime: WasmRuntime,
84    ) -> Result<Self, WasmExecutionError> {
85        let contract_bytecode = add_metering(contract_bytecode)?;
86        match runtime {
87            #[cfg(with_wasmer)]
88            WasmRuntime::Wasmer => Self::from_wasmer(contract_bytecode).await,
89            #[cfg(with_wasmtime)]
90            WasmRuntime::Wasmtime => Self::from_wasmtime(contract_bytecode).await,
91        }
92    }
93
94    /// Creates a new [`WasmContractModule`] using the WebAssembly module in `contract_bytecode_file`.
95    #[cfg(with_fs)]
96    pub async fn from_file(
97        contract_bytecode_file: impl AsRef<std::path::Path>,
98        runtime: WasmRuntime,
99    ) -> Result<Self, WasmExecutionError> {
100        Self::new(
101            Bytecode::load_from_file(contract_bytecode_file)
102                .map_err(anyhow::Error::from)
103                .map_err(WasmExecutionError::LoadContractModule)?,
104            runtime,
105        )
106        .await
107    }
108}
109
110impl UserContractModule for WasmContractModule {
111    fn instantiate(
112        &self,
113        runtime: ContractSyncRuntimeHandle,
114    ) -> Result<UserContractInstance, ExecutionError> {
115        #[cfg(with_metrics)]
116        let _instantiation_latency = metrics::CONTRACT_INSTANTIATION_LATENCY.measure_latency();
117
118        let instance: UserContractInstance = match self {
119            #[cfg(with_wasmtime)]
120            WasmContractModule::Wasmtime { module } => {
121                Box::new(WasmtimeContractInstance::prepare(module, runtime)?)
122            }
123            #[cfg(with_wasmer)]
124            WasmContractModule::Wasmer { engine, module } => Box::new(
125                WasmerContractInstance::prepare(engine.clone(), module, runtime)?,
126            ),
127        };
128
129        Ok(instance)
130    }
131}
132
133/// A user service in a compiled WebAssembly module.
134#[derive(Clone)]
135pub enum WasmServiceModule {
136    #[cfg(with_wasmer)]
137    Wasmer { module: ::wasmer::Module },
138    #[cfg(with_wasmtime)]
139    Wasmtime { module: ::wasmtime::Module },
140}
141
142impl WasmServiceModule {
143    /// Creates a new [`WasmServiceModule`] using the WebAssembly module with the provided bytecode.
144    pub async fn new(
145        service_bytecode: Bytecode,
146        runtime: WasmRuntime,
147    ) -> Result<Self, WasmExecutionError> {
148        match runtime {
149            #[cfg(with_wasmer)]
150            WasmRuntime::Wasmer => Self::from_wasmer(service_bytecode).await,
151            #[cfg(with_wasmtime)]
152            WasmRuntime::Wasmtime => Self::from_wasmtime(service_bytecode).await,
153        }
154    }
155
156    /// Creates a new [`WasmServiceModule`] using the WebAssembly module in `service_bytecode_file`.
157    #[cfg(with_fs)]
158    pub async fn from_file(
159        service_bytecode_file: impl AsRef<std::path::Path>,
160        runtime: WasmRuntime,
161    ) -> Result<Self, WasmExecutionError> {
162        Self::new(
163            Bytecode::load_from_file(service_bytecode_file)
164                .map_err(anyhow::Error::from)
165                .map_err(WasmExecutionError::LoadServiceModule)?,
166            runtime,
167        )
168        .await
169    }
170}
171
172impl UserServiceModule for WasmServiceModule {
173    fn instantiate(
174        &self,
175        runtime: ServiceSyncRuntimeHandle,
176    ) -> Result<UserServiceInstance, ExecutionError> {
177        #[cfg(with_metrics)]
178        let _instantiation_latency = metrics::SERVICE_INSTANTIATION_LATENCY.measure_latency();
179
180        let instance: UserServiceInstance = match self {
181            #[cfg(with_wasmtime)]
182            WasmServiceModule::Wasmtime { module } => {
183                Box::new(WasmtimeServiceInstance::prepare(module, runtime)?)
184            }
185            #[cfg(with_wasmer)]
186            WasmServiceModule::Wasmer { module } => {
187                Box::new(WasmerServiceInstance::prepare(module, runtime)?)
188            }
189        };
190
191        Ok(instance)
192    }
193}
194
195/// Instrument the [`Bytecode`] to add fuel metering.
196pub fn add_metering(bytecode: Bytecode) -> Result<Bytecode, WasmExecutionError> {
197    struct WasmtimeRules;
198
199    impl gas_metering::Rules for WasmtimeRules {
200        /// Calculates the fuel cost of a WebAssembly [`Operator`].
201        ///
202        /// The rules try to follow the hardcoded [rules in the Wasmtime runtime
203        /// engine](https://docs.rs/wasmtime/5.0.0/wasmtime/struct.Store.html#method.add_fuel).
204        fn instruction_cost(
205            &self,
206            instruction: &parity_wasm::elements::Instruction,
207        ) -> Option<u32> {
208            use parity_wasm::elements::Instruction::*;
209
210            Some(match instruction {
211                Nop | Drop | Block(_) | Loop(_) | Unreachable | Else | End => 0,
212                _ => 1,
213            })
214        }
215
216        fn memory_grow_cost(&self) -> gas_metering::MemoryGrowCost {
217            gas_metering::MemoryGrowCost::Free
218        }
219
220        fn call_per_local_cost(&self) -> u32 {
221            0
222        }
223    }
224
225    let instrumented_module = gas_metering::inject(
226        parity_wasm::deserialize_buffer(&bytecode.bytes)?,
227        gas_metering::host_function::Injector::new(
228            "linera:app/contract-runtime-api",
229            "consume-fuel",
230        ),
231        &WasmtimeRules,
232    )
233    .map_err(|_| WasmExecutionError::InstrumentModule)?;
234
235    Ok(Bytecode::new(instrumented_module.into_bytes()?))
236}
237
238#[cfg(web)]
239const _: () = {
240    use js_sys::wasm_bindgen::JsValue;
241
242    impl TryFrom<JsValue> for WasmServiceModule {
243        type Error = JsValue;
244
245        fn try_from(value: JsValue) -> Result<Self, JsValue> {
246            // TODO(#2775): be generic over possible implementations
247
248            cfg_if::cfg_if! {
249                if #[cfg(with_wasmer)] {
250                    Ok(Self::Wasmer {
251                        module: value.try_into()?,
252                    })
253                } else {
254                    Err(value)
255                }
256            }
257        }
258    }
259
260    impl From<WasmServiceModule> for JsValue {
261        fn from(module: WasmServiceModule) -> JsValue {
262            match module {
263                #[cfg(with_wasmer)]
264                WasmServiceModule::Wasmer { module } => ::wasmer::Module::clone(&module).into(),
265            }
266        }
267    }
268
269    impl TryFrom<JsValue> for WasmContractModule {
270        type Error = JsValue;
271
272        fn try_from(value: JsValue) -> Result<Self, JsValue> {
273            // TODO(#2775): be generic over possible implementations
274            cfg_if::cfg_if! {
275                if #[cfg(with_wasmer)] {
276                    Ok(Self::Wasmer {
277                        module: value.try_into()?,
278                        engine: Default::default(),
279                    })
280                } else {
281                    Err(value)
282                }
283            }
284        }
285    }
286
287    impl From<WasmContractModule> for JsValue {
288        fn from(module: WasmContractModule) -> JsValue {
289            match module {
290                #[cfg(with_wasmer)]
291                WasmContractModule::Wasmer { module, engine: _ } => {
292                    ::wasmer::Module::clone(&module).into()
293                }
294            }
295        }
296    }
297};
298
299/// Errors that can occur when executing a user application in a WebAssembly module.
300#[derive(Debug, Error)]
301pub enum WasmExecutionError {
302    #[error("Failed to load contract Wasm module: {_0}")]
303    LoadContractModule(#[source] anyhow::Error),
304    #[error("Failed to load service Wasm module: {_0}")]
305    LoadServiceModule(#[source] anyhow::Error),
306    #[error("Failed to instrument Wasm module to add fuel metering")]
307    InstrumentModule,
308    #[error("Invalid Wasm module: {0}")]
309    InvalidBytecode(#[from] wasm_instrument::parity_wasm::SerializationError),
310    #[cfg(with_wasmer)]
311    #[error("Failed to instantiate Wasm module: {_0}")]
312    InstantiateModuleWithWasmer(#[from] Box<::wasmer::InstantiationError>),
313    #[cfg(with_wasmtime)]
314    #[error("Failed to create and configure Wasmtime runtime: {_0}")]
315    CreateWasmtimeEngine(#[source] anyhow::Error),
316    #[cfg(with_wasmer)]
317    #[error(
318        "Failed to execute Wasm module in Wasmer. This may be caused by panics or insufficient fuel. {0}"
319    )]
320    ExecuteModuleInWasmer(#[from] ::wasmer::RuntimeError),
321    #[cfg(with_wasmtime)]
322    #[error("Failed to execute Wasm module in Wasmtime: {0}")]
323    ExecuteModuleInWasmtime(#[from] ::wasmtime::Trap),
324    #[error("Failed to execute Wasm module: {0}")]
325    ExecuteModule(#[from] linera_witty::RuntimeError),
326    #[error("Attempt to wait for an unknown promise")]
327    UnknownPromise,
328    #[error("Attempt to call incorrect `wait` function for a promise")]
329    IncorrectPromise,
330}
331
332#[cfg(with_wasmer)]
333impl From<::wasmer::InstantiationError> for WasmExecutionError {
334    fn from(instantiation_error: ::wasmer::InstantiationError) -> Self {
335        WasmExecutionError::InstantiateModuleWithWasmer(Box::new(instantiation_error))
336    }
337}
338
339/// This assumes that the current directory is one of the crates.
340#[cfg(with_testing)]
341pub mod test {
342    use std::sync::LazyLock;
343
344    #[cfg(with_fs)]
345    use super::{WasmContractModule, WasmRuntime, WasmServiceModule};
346
347    fn build_applications() -> Result<(), std::io::Error> {
348        tracing::info!("Building example applications with cargo");
349        let output = std::process::Command::new("cargo")
350            .current_dir("../examples")
351            .args(["build", "--release", "--target", "wasm32-unknown-unknown"])
352            .output()?;
353        assert!(
354            output.status.success(),
355            "Failed to build example applications.\n\n\
356                stdout:\n-------\n{}\n\n\
357                stderr:\n-------\n{}",
358            String::from_utf8_lossy(&output.stdout),
359            String::from_utf8_lossy(&output.stderr),
360        );
361        Ok(())
362    }
363
364    pub fn get_example_bytecode_paths(name: &str) -> Result<(String, String), std::io::Error> {
365        let name = name.replace('-', "_");
366        static INSTANCE: LazyLock<()> = LazyLock::new(|| build_applications().unwrap());
367        LazyLock::force(&INSTANCE);
368        Ok((
369            format!("../examples/target/wasm32-unknown-unknown/release/{name}_contract.wasm"),
370            format!("../examples/target/wasm32-unknown-unknown/release/{name}_service.wasm"),
371        ))
372    }
373
374    #[cfg(with_fs)]
375    pub async fn build_example_application(
376        name: &str,
377        wasm_runtime: impl Into<Option<WasmRuntime>>,
378    ) -> Result<(WasmContractModule, WasmServiceModule), anyhow::Error> {
379        let (contract_path, service_path) = get_example_bytecode_paths(name)?;
380        let wasm_runtime = wasm_runtime.into().unwrap_or_default();
381        let contract = WasmContractModule::from_file(&contract_path, wasm_runtime).await?;
382        let service = WasmServiceModule::from_file(&service_path, wasm_runtime).await?;
383        Ok((contract, service))
384    }
385}