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