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