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                .await
103                .map_err(anyhow::Error::from)
104                .map_err(WasmExecutionError::LoadContractModule)?,
105            runtime,
106        )
107        .await
108    }
109}
110
111impl UserContractModule for WasmContractModule {
112    fn instantiate(
113        &self,
114        runtime: ContractSyncRuntimeHandle,
115    ) -> Result<UserContractInstance, ExecutionError> {
116        #[cfg(with_metrics)]
117        let _instantiation_latency = metrics::CONTRACT_INSTANTIATION_LATENCY.measure_latency();
118
119        let instance: UserContractInstance = match self {
120            #[cfg(with_wasmtime)]
121            WasmContractModule::Wasmtime { module } => {
122                Box::new(WasmtimeContractInstance::prepare(module, runtime)?)
123            }
124            #[cfg(with_wasmer)]
125            WasmContractModule::Wasmer { engine, module } => Box::new(
126                WasmerContractInstance::prepare(engine.clone(), module, runtime)?,
127            ),
128        };
129
130        Ok(instance)
131    }
132}
133
134/// A user service in a compiled WebAssembly module.
135#[derive(Clone)]
136pub enum WasmServiceModule {
137    #[cfg(with_wasmer)]
138    Wasmer { module: ::wasmer::Module },
139    #[cfg(with_wasmtime)]
140    Wasmtime { module: ::wasmtime::Module },
141}
142
143impl WasmServiceModule {
144    /// Creates a new [`WasmServiceModule`] using the WebAssembly module with the provided bytecode.
145    pub async fn new(
146        service_bytecode: Bytecode,
147        runtime: WasmRuntime,
148    ) -> Result<Self, WasmExecutionError> {
149        match runtime {
150            #[cfg(with_wasmer)]
151            WasmRuntime::Wasmer => Self::from_wasmer(service_bytecode).await,
152            #[cfg(with_wasmtime)]
153            WasmRuntime::Wasmtime => Self::from_wasmtime(service_bytecode).await,
154        }
155    }
156
157    /// Creates a new [`WasmServiceModule`] using the WebAssembly module in `service_bytecode_file`.
158    #[cfg(with_fs)]
159    pub async fn from_file(
160        service_bytecode_file: impl AsRef<std::path::Path>,
161        runtime: WasmRuntime,
162    ) -> Result<Self, WasmExecutionError> {
163        Self::new(
164            Bytecode::load_from_file(service_bytecode_file)
165                .await
166                .map_err(anyhow::Error::from)
167                .map_err(WasmExecutionError::LoadServiceModule)?,
168            runtime,
169        )
170        .await
171    }
172}
173
174impl UserServiceModule for WasmServiceModule {
175    fn instantiate(
176        &self,
177        runtime: ServiceSyncRuntimeHandle,
178    ) -> Result<UserServiceInstance, ExecutionError> {
179        #[cfg(with_metrics)]
180        let _instantiation_latency = metrics::SERVICE_INSTANTIATION_LATENCY.measure_latency();
181
182        let instance: UserServiceInstance = match self {
183            #[cfg(with_wasmtime)]
184            WasmServiceModule::Wasmtime { module } => {
185                Box::new(WasmtimeServiceInstance::prepare(module, runtime)?)
186            }
187            #[cfg(with_wasmer)]
188            WasmServiceModule::Wasmer { module } => {
189                Box::new(WasmerServiceInstance::prepare(module, runtime)?)
190            }
191        };
192
193        Ok(instance)
194    }
195}
196
197/// Instrument the [`Bytecode`] to add fuel metering.
198pub fn add_metering(bytecode: Bytecode) -> Result<Bytecode, WasmExecutionError> {
199    struct WasmtimeRules;
200
201    impl gas_metering::Rules for WasmtimeRules {
202        /// Calculates the fuel cost of a WebAssembly [`Operator`].
203        ///
204        /// The rules try to follow the hardcoded [rules in the Wasmtime runtime
205        /// engine](https://docs.rs/wasmtime/5.0.0/wasmtime/struct.Store.html#method.add_fuel).
206        fn instruction_cost(
207            &self,
208            instruction: &parity_wasm::elements::Instruction,
209        ) -> Option<u32> {
210            use parity_wasm::elements::Instruction::*;
211
212            Some(match instruction {
213                Nop | Drop | Block(_) | Loop(_) | Unreachable | Else | End => 0,
214                _ => 1,
215            })
216        }
217
218        fn memory_grow_cost(&self) -> gas_metering::MemoryGrowCost {
219            gas_metering::MemoryGrowCost::Free
220        }
221
222        fn call_per_local_cost(&self) -> u32 {
223            0
224        }
225    }
226
227    let instrumented_module = gas_metering::inject(
228        parity_wasm::deserialize_buffer(&bytecode.bytes)?,
229        gas_metering::host_function::Injector::new(
230            "linera:app/contract-runtime-api",
231            "consume-fuel",
232        ),
233        &WasmtimeRules,
234    )
235    .map_err(|_| WasmExecutionError::InstrumentModule)?;
236
237    Ok(Bytecode::new(instrumented_module.into_bytes()?))
238}
239
240#[cfg(web)]
241const _: () = {
242    use js_sys::wasm_bindgen::JsValue;
243
244    impl TryFrom<JsValue> for WasmServiceModule {
245        type Error = JsValue;
246
247        fn try_from(value: JsValue) -> Result<Self, JsValue> {
248            // TODO(#2775): be generic over possible implementations
249
250            cfg_if::cfg_if! {
251                if #[cfg(with_wasmer)] {
252                    Ok(Self::Wasmer {
253                        module: value.try_into()?,
254                    })
255                } else {
256                    Err(value)
257                }
258            }
259        }
260    }
261
262    impl From<WasmServiceModule> for JsValue {
263        fn from(module: WasmServiceModule) -> JsValue {
264            match module {
265                #[cfg(with_wasmer)]
266                WasmServiceModule::Wasmer { module } => ::wasmer::Module::clone(&module).into(),
267            }
268        }
269    }
270
271    impl TryFrom<JsValue> for WasmContractModule {
272        type Error = JsValue;
273
274        fn try_from(value: JsValue) -> Result<Self, JsValue> {
275            // TODO(#2775): be generic over possible implementations
276            cfg_if::cfg_if! {
277                if #[cfg(with_wasmer)] {
278                    Ok(Self::Wasmer {
279                        module: value.try_into()?,
280                        engine: Default::default(),
281                    })
282                } else {
283                    Err(value)
284                }
285            }
286        }
287    }
288
289    impl From<WasmContractModule> for JsValue {
290        fn from(module: WasmContractModule) -> JsValue {
291            match module {
292                #[cfg(with_wasmer)]
293                WasmContractModule::Wasmer { module, engine: _ } => {
294                    ::wasmer::Module::clone(&module).into()
295                }
296            }
297        }
298    }
299};
300
301/// Errors that can occur when executing a user application in a WebAssembly module.
302#[derive(Debug, Error)]
303pub enum WasmExecutionError {
304    #[error("Failed to load contract Wasm module: {_0}")]
305    LoadContractModule(#[source] anyhow::Error),
306    #[error("Failed to load service Wasm module: {_0}")]
307    LoadServiceModule(#[source] anyhow::Error),
308    #[error("Failed to instrument Wasm module to add fuel metering")]
309    InstrumentModule,
310    #[error("Invalid Wasm module: {0}")]
311    InvalidBytecode(#[from] wasm_instrument::parity_wasm::SerializationError),
312    #[cfg(with_wasmer)]
313    #[error("Failed to instantiate Wasm module: {_0}")]
314    InstantiateModuleWithWasmer(#[from] Box<::wasmer::InstantiationError>),
315    #[cfg(with_wasmtime)]
316    #[error("Failed to create and configure Wasmtime runtime: {_0}")]
317    CreateWasmtimeEngine(#[source] anyhow::Error),
318    #[cfg(with_wasmer)]
319    #[error(
320        "Failed to execute Wasm module in Wasmer. This may be caused by panics or insufficient fuel. {0}"
321    )]
322    ExecuteModuleInWasmer(#[from] ::wasmer::RuntimeError),
323    #[cfg(with_wasmtime)]
324    #[error("Failed to execute Wasm module in Wasmtime: {0}")]
325    ExecuteModuleInWasmtime(#[from] ::wasmtime::Trap),
326    #[error("Failed to execute Wasm module: {0}")]
327    ExecuteModule(#[from] linera_witty::RuntimeError),
328    #[error("Attempt to wait for an unknown promise")]
329    UnknownPromise,
330    #[error("Attempt to call incorrect `wait` function for a promise")]
331    IncorrectPromise,
332}
333
334#[cfg(with_wasmer)]
335impl From<::wasmer::InstantiationError> for WasmExecutionError {
336    fn from(instantiation_error: ::wasmer::InstantiationError) -> Self {
337        WasmExecutionError::InstantiateModuleWithWasmer(Box::new(instantiation_error))
338    }
339}
340
341/// This assumes that the current directory is one of the crates.
342#[cfg(with_testing)]
343pub mod test {
344    use std::sync::LazyLock;
345
346    #[cfg(with_fs)]
347    use super::{WasmContractModule, WasmRuntime, WasmServiceModule};
348
349    fn build_applications() -> Result<(), std::io::Error> {
350        tracing::info!("Building example applications with cargo");
351        let output = std::process::Command::new("cargo")
352            .current_dir("../examples")
353            .args(["build", "--release", "--target", "wasm32-unknown-unknown"])
354            .output()?;
355        if !output.status.success() {
356            panic!(
357                "Failed to build example applications.\n\n\
358                stdout:\n-------\n{}\n\n\
359                stderr:\n-------\n{}",
360                String::from_utf8_lossy(&output.stdout),
361                String::from_utf8_lossy(&output.stderr),
362            );
363        }
364        Ok(())
365    }
366
367    pub fn get_example_bytecode_paths(name: &str) -> Result<(String, String), std::io::Error> {
368        let name = name.replace('-', "_");
369        static INSTANCE: LazyLock<()> = LazyLock::new(|| build_applications().unwrap());
370        LazyLock::force(&INSTANCE);
371        Ok((
372            format!("../examples/target/wasm32-unknown-unknown/release/{name}_contract.wasm"),
373            format!("../examples/target/wasm32-unknown-unknown/release/{name}_service.wasm"),
374        ))
375    }
376
377    #[cfg(with_fs)]
378    pub async fn build_example_application(
379        name: &str,
380        wasm_runtime: impl Into<Option<WasmRuntime>>,
381    ) -> Result<(WasmContractModule, WasmServiceModule), anyhow::Error> {
382        let (contract_path, service_path) = get_example_bytecode_paths(name)?;
383        let wasm_runtime = wasm_runtime.into().unwrap_or_default();
384        let contract = WasmContractModule::from_file(&contract_path, wasm_runtime).await?;
385        let service = WasmServiceModule::from_file(&service_path, wasm_runtime).await?;
386        Ok((contract, service))
387    }
388}