diff --git a/Cargo.lock b/Cargo.lock index f2379d4ee6de1..dc8d08aace674 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22335,6 +22335,7 @@ dependencies = [ "rpassword", "sc-client-api", "sc-client-db", + "sc-executor 0.32.0", "sc-keystore", "sc-mixnet", "sc-network", @@ -22351,6 +22352,8 @@ dependencies = [ "sp-keystore 0.34.0", "sp-panic-handler 13.0.0", "sp-runtime 31.0.1", + "sp-state-machine 0.35.0", + "sp-storage 19.0.0", "sp-tracing 16.0.0", "sp-version 29.0.0", "tempfile", @@ -22880,6 +22883,7 @@ dependencies = [ name = "sc-executor-common" version = "0.29.0" dependencies = [ + "parity-scale-codec", "polkavm 0.9.3", "sc-allocator 23.0.0", "sp-maybe-compressed-blob 11.0.0", @@ -22958,7 +22962,6 @@ dependencies = [ "cargo_metadata", "cfg-if", "libc", - "log", "parity-scale-codec", "parking_lot 0.12.3", "paste", @@ -22966,10 +22969,12 @@ dependencies = [ "sc-allocator 23.0.0", "sc-executor-common 0.29.0", "sc-runtime-test", + "sp-core 28.0.0", "sp-io 30.0.0", "sp-runtime-interface 24.0.0", "sp-wasm-interface 20.0.0", "tempfile", + "tracing", "wasmtime", "wat", ] diff --git a/polkadot/node/core/pvf/common/src/executor_interface.rs b/polkadot/node/core/pvf/common/src/executor_interface.rs index 47f9ed1604e78..c02db118b7e49 100644 --- a/polkadot/node/core/pvf/common/src/executor_interface.rs +++ b/polkadot/node/core/pvf/common/src/executor_interface.rs @@ -191,7 +191,7 @@ pub fn prepare( executor_params: &ExecutorParams, ) -> Result, sc_executor_common::error::WasmError> { let (semantics, _) = params_to_wasmtime_semantics(executor_params); - sc_executor_wasmtime::prepare_runtime_artifact(blob, &semantics) + sc_executor_wasmtime::prepare_runtime_artifact(blob, Default::default(), &semantics) } /// Available host functions. We leave out: diff --git a/substrate/bin/node/cli/src/cli.rs b/substrate/bin/node/cli/src/cli.rs index 1d7001a5dccfc..d1f654c14f525 100644 --- a/substrate/bin/node/cli/src/cli.rs +++ b/substrate/bin/node/cli/src/cli.rs @@ -100,4 +100,7 @@ pub enum Subcommand { /// Db meta columns information. ChainInfo(sc_cli::ChainInfoCmd), + + /// Precompile the WASM runtime into native code + PrecompileWasm(sc_cli::PrecompileWasmCmd), } diff --git a/substrate/bin/node/cli/src/command.rs b/substrate/bin/node/cli/src/command.rs index 2910002e5b274..3ccf5e9f19d1f 100644 --- a/substrate/bin/node/cli/src/command.rs +++ b/substrate/bin/node/cli/src/command.rs @@ -228,5 +228,13 @@ pub fn run() -> Result<()> { let runner = cli.create_runner(cmd)?; runner.sync_run(|config| cmd.run::(&config)) }, + Some(Subcommand::PrecompileWasm(cmd)) => { + let runner = cli.create_runner(cmd)?; + runner.async_run(|config| { + let PartialComponents { task_manager, backend, .. } = + service::new_partial(&config, None)?; + Ok((cmd.run(backend, config.chain_spec), task_manager)) + }) + }, } } diff --git a/substrate/client/cli/Cargo.toml b/substrate/client/cli/Cargo.toml index f0b9f8f9b9051..9a050055568be 100644 --- a/substrate/client/cli/Cargo.toml +++ b/substrate/client/cli/Cargo.toml @@ -37,6 +37,7 @@ bip39 = { package = "parity-bip39", version = "2.0.1", features = ["rand"] } tokio = { features = ["parking_lot", "rt-multi-thread", "signal"], workspace = true, default-features = true } sc-client-api = { workspace = true, default-features = true } sc-client-db = { workspace = true } +sc-executor = { workspace = true, default-features = true } sc-keystore = { workspace = true, default-features = true } sc-mixnet = { workspace = true, default-features = true } sc-network = { workspace = true, default-features = true } @@ -51,6 +52,8 @@ sp-keyring = { workspace = true, default-features = true } sp-keystore = { workspace = true, default-features = true } sp-panic-handler = { workspace = true, default-features = true } sp-runtime = { workspace = true, default-features = true } +sp-state-machine = { workspace = true, default-features = true } +sp-storage = { workspace = true, default-features = true } sp-version = { workspace = true, default-features = true } [dev-dependencies] diff --git a/substrate/client/cli/src/commands/mod.rs b/substrate/client/cli/src/commands/mod.rs index 2d7a0dc72ff53..abe495f731721 100644 --- a/substrate/client/cli/src/commands/mod.rs +++ b/substrate/client/cli/src/commands/mod.rs @@ -30,6 +30,7 @@ mod insert_key; mod inspect_key; mod inspect_node_key; mod key; +mod precompile_wasm_cmd; mod purge_chain_cmd; mod revert_cmd; mod run_cmd; @@ -44,6 +45,6 @@ pub use self::{ export_blocks_cmd::ExportBlocksCmd, export_state_cmd::ExportStateCmd, generate::GenerateCmd, generate_node_key::GenerateKeyCmdCommon, import_blocks_cmd::ImportBlocksCmd, insert_key::InsertKeyCmd, inspect_key::InspectKeyCmd, inspect_node_key::InspectNodeKeyCmd, - key::KeySubcommand, purge_chain_cmd::PurgeChainCmd, revert_cmd::RevertCmd, run_cmd::RunCmd, - sign::SignCmd, vanity::VanityCmd, verify::VerifyCmd, + key::KeySubcommand, precompile_wasm_cmd::PrecompileWasmCmd, purge_chain_cmd::PurgeChainCmd, + revert_cmd::RevertCmd, run_cmd::RunCmd, sign::SignCmd, vanity::VanityCmd, verify::VerifyCmd, }; diff --git a/substrate/client/cli/src/commands/precompile_wasm_cmd.rs b/substrate/client/cli/src/commands/precompile_wasm_cmd.rs new file mode 100644 index 0000000000000..daf5bc6dd1932 --- /dev/null +++ b/substrate/client/cli/src/commands/precompile_wasm_cmd.rs @@ -0,0 +1,170 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use crate::{ + arg_enums::{ + execution_method_from_cli, WasmExecutionMethod, WasmtimeInstantiationStrategy, + DEFAULT_WASMTIME_INSTANTIATION_STRATEGY, + }, + error::{self, Error}, + params::{DatabaseParams, PruningParams, SharedParams}, + CliConfiguration, +}; + +use clap::Parser; +use sc_client_api::{Backend, HeaderBackend}; +use sc_executor::{ + precompile_and_serialize_versioned_wasm_runtime, HeapAllocStrategy, DEFAULT_HEAP_ALLOC_PAGES, +}; +use sc_service::ChainSpec; +use sp_core::traits::RuntimeCode; +use sp_runtime::traits::{Block as BlockT, Hash, Header}; +use sp_state_machine::backend::BackendRuntimeCode; +use std::{fmt::Debug, path::PathBuf, sync::Arc}; + +/// The `precompile-wasm` command used to serialize a precompiled WASM module. +/// +/// The WASM code precompiled will be the one used at the latest finalized block +/// this node is aware of, if this node has the state for that finalized block in +/// its storage. If that's not the case, it will use the WASM code from the chain +/// spec passed as parameter when running the node. +#[derive(Debug, Parser)] +pub struct PrecompileWasmCmd { + #[allow(missing_docs)] + #[clap(flatten)] + pub database_params: DatabaseParams, + + /// The default number of 64KB pages to ever allocate for Wasm execution. + /// + /// Don't alter this unless you know what you're doing. + #[arg(long, value_name = "COUNT")] + pub default_heap_pages: Option, + + /// Path to the directory where precompiled artifact will be written. + #[arg(value_parser = PrecompileWasmCmd::validate_existing_directory)] + pub output_dir: PathBuf, + + #[allow(missing_docs)] + #[clap(flatten)] + pub pruning_params: PruningParams, + + #[allow(missing_docs)] + #[clap(flatten)] + pub shared_params: SharedParams, + + /// The WASM instantiation method to use. + /// + /// Only has an effect when `wasm-execution` is set to `compiled`. + /// The copy-on-write strategies are only supported on Linux. + /// If the copy-on-write variant of a strategy is unsupported + /// the executor will fall back to the non-CoW equivalent. + /// The fastest (and the default) strategy available is `pooling-copy-on-write`. + /// The `legacy-instance-reuse` strategy is deprecated and will + /// be removed in the future. It should only be used in case of + /// issues with the default instantiation strategy. + #[arg( + long, + value_name = "STRATEGY", + default_value_t = DEFAULT_WASMTIME_INSTANTIATION_STRATEGY, + value_enum, + )] + pub wasmtime_instantiation_strategy: WasmtimeInstantiationStrategy, +} + +impl PrecompileWasmCmd { + /// Run the precompile-wasm command + pub async fn run(&self, backend: Arc, spec: Box) -> error::Result<()> + where + B: BlockT, + BA: Backend, + { + let heap_pages = self.default_heap_pages.unwrap_or(DEFAULT_HEAP_ALLOC_PAGES); + + let blockchain_info = backend.blockchain().info(); + + if backend.have_state_at(blockchain_info.finalized_hash, blockchain_info.finalized_number) { + let state = backend.state_at(backend.blockchain().info().finalized_hash)?; + + precompile_and_serialize_versioned_wasm_runtime( + HeapAllocStrategy::Static { extra_pages: heap_pages }, + &BackendRuntimeCode::new(&state).runtime_code()?, + execution_method_from_cli( + WasmExecutionMethod::Compiled, + self.wasmtime_instantiation_strategy, + ), + &self.output_dir, + ) + .map_err(|e| Error::Application(Box::new(e)))?; + } else { + let storage = spec.as_storage_builder().build_storage()?; + if let Some(wasm_bytecode) = storage.top.get(sp_storage::well_known_keys::CODE) { + let runtime_code = RuntimeCode { + code_fetcher: &sp_core::traits::WrappedRuntimeCode( + wasm_bytecode.as_slice().into(), + ), + hash: <::Hashing as Hash>::hash(&wasm_bytecode) + .as_ref() + .to_vec(), + heap_pages: Some(heap_pages as u64), + }; + precompile_and_serialize_versioned_wasm_runtime( + HeapAllocStrategy::Static { extra_pages: heap_pages }, + &runtime_code, + execution_method_from_cli( + WasmExecutionMethod::Compiled, + self.wasmtime_instantiation_strategy, + ), + &self.output_dir, + ) + .map_err(|e| Error::Application(Box::new(e)))?; + } else { + return Err(Error::Input(format!( + "The chain spec used does not contain a wasm bytecode in the {} storage key", + String::from_utf8(sp_storage::well_known_keys::CODE.to_vec()) + .expect("Well known key should be a valid UTF8 string.") + ))); + } + } + + Ok(()) + } + + /// Validate that the path is a valid directory. + fn validate_existing_directory(path: &str) -> Result { + let path_buf = PathBuf::from(path); + if path_buf.is_dir() { + Ok(path_buf) + } else { + Err(format!("The path '{}' is not a valid directory", path)) + } + } +} + +impl CliConfiguration for PrecompileWasmCmd { + fn shared_params(&self) -> &SharedParams { + &self.shared_params + } + + fn pruning_params(&self) -> Option<&PruningParams> { + Some(&self.pruning_params) + } + + fn database_params(&self) -> Option<&DatabaseParams> { + Some(&self.database_params) + } +} diff --git a/substrate/client/cli/src/config.rs b/substrate/client/cli/src/config.rs index 59238b3307cf2..9d9e00ebdb74e 100644 --- a/substrate/client/cli/src/config.rs +++ b/substrate/client/cli/src/config.rs @@ -294,6 +294,13 @@ pub trait CliConfiguration: Sized { Ok(self.import_params().map(|x| x.wasm_method()).unwrap_or_default()) } + /// Get the path where WASM precompiled artifacts live. + /// + /// By default this is `None`. + fn wasmtime_precompiled(&self) -> Option { + self.import_params().map(|x| x.wasmtime_precompiled()).unwrap_or_default() + } + /// Get the path where WASM overrides live. /// /// By default this is `None`. @@ -535,6 +542,7 @@ pub trait CliConfiguration: Sized { default_heap_pages: self.default_heap_pages()?, max_runtime_instances, runtime_cache_size, + wasmtime_precompiled: self.wasmtime_precompiled(), }, wasm_runtime_overrides: self.wasm_runtime_overrides(), rpc: RpcConfiguration { diff --git a/substrate/client/cli/src/params/import_params.rs b/substrate/client/cli/src/params/import_params.rs index e4b8b9644febc..7d28267c38ffb 100644 --- a/substrate/client/cli/src/params/import_params.rs +++ b/substrate/client/cli/src/params/import_params.rs @@ -65,10 +65,28 @@ pub struct ImportParams { )] pub wasmtime_instantiation_strategy: WasmtimeInstantiationStrategy, + /// Specify the path where local precompiled WASM runtimes are stored. + /// Only has an effect when `wasm-execution` is set to `compiled`. + /// + /// The precompiled runtimes must have been generated using the `precompile-wasm` + /// subcommand with the same version of wasmtime and the exact same configuration. + /// The file name must end with the hash of the configuration. This hash must match, otherwise + /// the runtime will be recompiled. + #[arg( + long, + value_name = "PATH", + value_parser = ImportParams::validate_existing_directory, + )] + pub wasmtime_precompiled: Option, + /// Specify the path where local WASM runtimes are stored. /// /// These runtimes will override on-chain runtimes when the version matches. - #[arg(long, value_name = "PATH")] + #[arg( + long, + value_name = "PATH", + value_parser = ImportParams::validate_existing_directory, + )] pub wasm_runtime_overrides: Option, #[allow(missing_docs)] @@ -99,11 +117,27 @@ impl ImportParams { crate::execution_method_from_cli(self.wasm_method, self.wasmtime_instantiation_strategy) } + /// Enable using precompiled WASM module with locally-stored artifacts + /// by specifying the path where artifacts are stored. + pub fn wasmtime_precompiled(&self) -> Option { + self.wasmtime_precompiled.clone() + } + /// Enable overriding on-chain WASM with locally-stored WASM /// by specifying the path where local WASM is stored. pub fn wasm_runtime_overrides(&self) -> Option { self.wasm_runtime_overrides.clone() } + + /// Validate that the path is a valid directory. + fn validate_existing_directory(path: &str) -> Result { + let path_buf = PathBuf::from(path); + if path_buf.is_dir() { + Ok(path_buf) + } else { + Err(format!("The path '{}' is not a valid directory", path)) + } + } } /// Execution strategies parameters. diff --git a/substrate/client/executor/benches/bench.rs b/substrate/client/executor/benches/bench.rs index 4cde8c2a4a646..63bb08551917b 100644 --- a/substrate/client/executor/benches/bench.rs +++ b/substrate/client/executor/benches/bench.rs @@ -70,9 +70,12 @@ fn initialize( }; if precompile { - let precompiled_blob = - sc_executor_wasmtime::prepare_runtime_artifact(blob, &config.semantics) - .unwrap(); + let precompiled_blob = sc_executor_wasmtime::prepare_runtime_artifact( + blob, + Default::default(), + &config.semantics, + ) + .unwrap(); // Create a fresh temporary directory to make absolutely sure // we'll use the right module. @@ -84,7 +87,7 @@ fn initialize( unsafe { sc_executor_wasmtime::create_runtime_from_artifact::< sp_io::SubstrateHostFunctions, - >(&path, config) + >(&path, sc_executor_wasmtime::ModuleVersionStrategy::default(), config) } } else { sc_executor_wasmtime::create_runtime::(blob, config) diff --git a/substrate/client/executor/common/Cargo.toml b/substrate/client/executor/common/Cargo.toml index 58fb0b423f242..dc69adef853b6 100644 --- a/substrate/client/executor/common/Cargo.toml +++ b/substrate/client/executor/common/Cargo.toml @@ -17,6 +17,7 @@ workspace = true targets = ["x86_64-unknown-linux-gnu"] [dependencies] +codec = { workspace = true, default-features = true } thiserror = { workspace = true } wasm-instrument = { workspace = true, default-features = true } sc-allocator = { workspace = true, default-features = true } diff --git a/substrate/client/executor/common/src/wasm_runtime.rs b/substrate/client/executor/common/src/wasm_runtime.rs index e8f429a3dbb2a..778731c964c40 100644 --- a/substrate/client/executor/common/src/wasm_runtime.rs +++ b/substrate/client/executor/common/src/wasm_runtime.rs @@ -74,7 +74,7 @@ pub trait WasmInstance: Send { /// Defines the heap pages allocation strategy the wasm runtime should use. /// /// A heap page is defined as 64KiB of memory. -#[derive(Debug, Copy, Clone, PartialEq, Hash, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Hash, Eq, codec::Encode)] pub enum HeapAllocStrategy { /// Allocate a static number of heap pages. /// diff --git a/substrate/client/executor/src/executor.rs b/substrate/client/executor/src/executor.rs index 913bcdfcfe591..9c0618d1e0eb1 100644 --- a/substrate/client/executor/src/executor.rs +++ b/substrate/client/executor/src/executor.rs @@ -92,6 +92,7 @@ pub struct WasmExecutorBuilder { max_runtime_instances: usize, cache_path: Option, allow_missing_host_functions: bool, + wasmtime_precompiled_path: Option, runtime_cache_size: u8, } @@ -110,6 +111,7 @@ impl WasmExecutorBuilder { runtime_cache_size: 4, allow_missing_host_functions: false, cache_path: None, + wasmtime_precompiled_path: None, } } @@ -193,6 +195,23 @@ impl WasmExecutorBuilder { self } + /// Create the wasm executor with the given `maybe_wasmtime_precompiled_path`, provided that it + /// is `Some`. + /// + /// The `maybe_wasmtime_precompiled_path` is an optional which (if `Some`) its inner value is a + /// path to a directory where the executor loads precompiled wasmtime modules. + /// + /// If `None`, calling this function will have no impact on the wasm executor being built. + pub fn with_optional_wasmtime_precompiled_path( + mut self, + maybe_wasmtime_precompiled_path: Option>, + ) -> Self { + if let Some(wasmtime_precompiled_path) = maybe_wasmtime_precompiled_path { + self.wasmtime_precompiled_path = Some(wasmtime_precompiled_path.into()); + } + self + } + /// Build the configured [`WasmExecutor`]. pub fn build(self) -> WasmExecutor { WasmExecutor { @@ -211,6 +230,7 @@ impl WasmExecutorBuilder { )), cache_path: self.cache_path, allow_missing_host_functions: self.allow_missing_host_functions, + wasmtime_precompiled_path: self.wasmtime_precompiled_path, phantom: PhantomData, } } @@ -234,6 +254,8 @@ pub struct WasmExecutor { cache_path: Option, /// Ignore missing function imports. allow_missing_host_functions: bool, + /// Optional path to a directory where the executor can find precompiled wasmtime modules. + wasmtime_precompiled_path: Option, phantom: PhantomData, } @@ -247,6 +269,7 @@ impl Clone for WasmExecutor { cache: self.cache.clone(), cache_path: self.cache_path.clone(), allow_missing_host_functions: self.allow_missing_host_functions, + wasmtime_precompiled_path: self.wasmtime_precompiled_path.clone(), phantom: self.phantom, } } @@ -301,6 +324,7 @@ impl WasmExecutor { )), cache_path, allow_missing_host_functions: false, + wasmtime_precompiled_path: None, phantom: PhantomData, } } @@ -353,6 +377,7 @@ where runtime_code, ext, self.method, + self.wasmtime_precompiled_path.as_deref(), heap_alloc_strategy, self.allow_missing_host_functions, |module, instance, version, ext| { @@ -430,6 +455,8 @@ where runtime_blob, allow_missing_host_functions, self.cache_path.as_deref(), + None, + &[], ) .map_err(|e| format!("Failed to create module: {}", e))?; diff --git a/substrate/client/executor/src/integration_tests/mod.rs b/substrate/client/executor/src/integration_tests/mod.rs index 5d94ec6dcd386..3928a52786878 100644 --- a/substrate/client/executor/src/integration_tests/mod.rs +++ b/substrate/client/executor/src/integration_tests/mod.rs @@ -453,6 +453,8 @@ fn mk_test_runtime( blob, true, None, + None, + &[], ) .expect("failed to instantiate wasm runtime") } @@ -679,6 +681,8 @@ fn memory_is_cleared_between_invocations(wasm_method: WasmExecutionMethod) { RuntimeBlob::uncompress_if_needed(&binary[..]).unwrap(), true, None, + None, + &[], ) .unwrap(); diff --git a/substrate/client/executor/src/lib.rs b/substrate/client/executor/src/lib.rs index 204f1ff22d74d..f4b605a760677 100644 --- a/substrate/client/executor/src/lib.rs +++ b/substrate/client/executor/src/lib.rs @@ -46,7 +46,9 @@ pub use sp_version::{NativeVersion, RuntimeVersion}; #[doc(hidden)] pub use sp_wasm_interface; pub use sp_wasm_interface::HostFunctions; -pub use wasm_runtime::{read_embedded_version, WasmExecutionMethod}; +pub use wasm_runtime::{ + precompile_and_serialize_versioned_wasm_runtime, read_embedded_version, WasmExecutionMethod, +}; pub use sc_executor_common::{ error, diff --git a/substrate/client/executor/src/wasm_runtime.rs b/substrate/client/executor/src/wasm_runtime.rs index 8f189ca92388a..fe2a3a26f65aa 100644 --- a/substrate/client/executor/src/wasm_runtime.rs +++ b/substrate/client/executor/src/wasm_runtime.rs @@ -23,7 +23,7 @@ use crate::error::{Error, WasmError}; -use codec::Decode; +use codec::{Decode, Encode}; use parking_lot::Mutex; use sc_executor_common::{ runtime_blob::RuntimeBlob, @@ -40,6 +40,8 @@ use std::{ sync::Arc, }; +const PRECOM_FILENAME_PREFIX: &str = "precompiled_wasm_"; + /// Specification of different methods of executing the runtime Wasm code. #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] pub enum WasmExecutionMethod { @@ -220,6 +222,7 @@ impl RuntimeCache { runtime_code: &'c RuntimeCode<'c>, ext: &mut dyn Externalities, wasm_method: WasmExecutionMethod, + wasmtime_precompiled: Option<&Path>, heap_alloc_strategy: HeapAllocStrategy, allow_missing_func_imports: bool, f: F, @@ -255,6 +258,8 @@ impl RuntimeCache { allow_missing_func_imports, self.max_runtime_instances, self.cache_path.as_deref(), + wasmtime_precompiled, + code_hash, ); match result { @@ -293,6 +298,8 @@ pub fn create_wasm_runtime_with_code( blob: RuntimeBlob, allow_missing_func_imports: bool, cache_path: Option<&Path>, + wasmtime_precompiled_path: Option<&Path>, + code_hash: &[u8], ) -> Result, WasmError> where H: HostFunctions, @@ -302,29 +309,176 @@ where } match wasm_method { - WasmExecutionMethod::Compiled { instantiation_strategy } => + WasmExecutionMethod::Compiled { instantiation_strategy } => { + let semantics = sc_executor_wasmtime::Semantics { + heap_alloc_strategy, + instantiation_strategy, + deterministic_stack_limit: None, + canonicalize_nans: false, + parallel_compilation: true, + wasm_multi_value: false, + wasm_bulk_memory: false, + wasm_reference_types: false, + wasm_simd: false, + }; + if let Some(wasmtime_precompiled_dir) = wasmtime_precompiled_path { + if !wasmtime_precompiled_dir.is_dir() { + return Err(WasmError::Instantiation(format!( + "Wasmtime precompiled is not a directory: {}", + wasmtime_precompiled_dir.display() + ))) + } + + let artifact_version = + compute_artifact_version(allow_missing_func_imports, code_hash, &semantics); + tracing::debug!( + target: "wasmtime-runtime", + "Searching for wasm hash: {}", + artifact_version.clone() + ); + + let file_name = format!("{}{}", PRECOM_FILENAME_PREFIX, &artifact_version); + let file_path = std::path::Path::new(wasmtime_precompiled_dir).join(&file_name); + + // If the file exists, and it hasn't been tempered with, the name is known. + // And it is in itself a check for the artifact version, wasm interface + // version and configuration. + if file_path.is_file() { + tracing::info!( + target: "wasmtime-runtime", + "🔎 Found precompiled wasm: {}", + file_name + ); + + // We change the version check strategy to make sure that the file + // content was serialized with the exact same config as well + let module_version_strategy = + sc_executor_wasmtime::ModuleVersionStrategy::Custom( + artifact_version.clone(), + ); + + // # Safety + // + // The file name of the artifact was checked before, + // so if the user has not renamed nor modified the file, + // it's certain that the file has been generated by + // `prepare_runtime_artifact` and with the same wasmtime + // version and configuration. + // + // Return early if the expected precompiled artifact exists. + return unsafe { + sc_executor_wasmtime::create_runtime_from_artifact::( + &file_path, + module_version_strategy, + sc_executor_wasmtime::Config { + allow_missing_func_imports, + cache_path: cache_path.map(ToOwned::to_owned), + semantics, + }, + ) + } + .map(|runtime| -> Box { Box::new(runtime) }) + } + + // If the expected precompiled artifact doesn't exist, we default to compiling it. + tracing::warn!( + "❗️ Precompiled wasm with name '{}' doesn't exist, compiling it.", + file_name + ); + } + sc_executor_wasmtime::create_runtime::( blob, sc_executor_wasmtime::Config { allow_missing_func_imports, cache_path: cache_path.map(ToOwned::to_owned), - semantics: sc_executor_wasmtime::Semantics { - heap_alloc_strategy, - instantiation_strategy, - deterministic_stack_limit: None, - canonicalize_nans: false, - parallel_compilation: true, - wasm_multi_value: false, - wasm_bulk_memory: false, - wasm_reference_types: false, - wasm_simd: false, - }, + semantics, }, ) - .map(|runtime| -> Box { Box::new(runtime) }), + .map(|runtime| -> Box { Box::new(runtime) }) + }, } } +/// Create and serialize a precompiled artifact of a wasm runtime with the given `code`. +pub fn precompile_and_serialize_versioned_wasm_runtime<'c>( + heap_alloc_strategy: HeapAllocStrategy, + runtime_code: &'c RuntimeCode<'c>, + wasm_method: WasmExecutionMethod, + wasmtime_precompiled_path: &Path, +) -> Result<(), WasmError> { + let semantics = match wasm_method { + WasmExecutionMethod::Compiled { instantiation_strategy } => + sc_executor_wasmtime::Semantics { + heap_alloc_strategy, + instantiation_strategy, + deterministic_stack_limit: None, + canonicalize_nans: false, + parallel_compilation: true, + wasm_multi_value: false, + wasm_bulk_memory: false, + wasm_reference_types: false, + wasm_simd: false, + }, + }; + + let code_hash = &runtime_code.hash; + + let artifact_version = compute_artifact_version(false, code_hash, &semantics); + tracing::debug!( + target: "wasmtime-runtime", + wasm_hash = %artifact_version, + "Generated precompiled wasm hash", + ); + + let code = runtime_code.fetch_runtime_code().ok_or(WasmError::CodeNotFound)?; + + // The incoming code may be actually compressed. We decompress it here and then work with + // the uncompressed code from now on. + let blob = sc_executor_common::runtime_blob::RuntimeBlob::uncompress_if_needed(code.as_ref())?; + + let serialized_precompiled_wasm = sc_executor_wasmtime::prepare_runtime_artifact( + blob, + sc_executor_wasmtime::ModuleVersionStrategy::Custom(artifact_version.clone()), + &semantics, + )?; + + // Write in a file + std::fs::write( + wasmtime_precompiled_path.join(format!("{PRECOM_FILENAME_PREFIX}{artifact_version}")), + &serialized_precompiled_wasm, + ) + .map_err(|e| { + WasmError::Other(format!("Fail to write precompiled artifact, I/O Error: {}", e)) + })?; + + Ok(()) +} + +/// Compute a hash that aggregates all the metadata relating to the artifact that must not change +/// so that it can be reused with confidence. +fn compute_artifact_version( + allow_missing_func_imports: bool, + code_hash: &[u8], + semantics: &sc_executor_wasmtime::Semantics, +) -> String { + tracing::trace!( + target: "wasmtime-runtime", + allow_missing_func_imports, + code_hash = sp_core::bytes::to_hex(&code_hash, false), + ?semantics, + "Computing wasm runtime hash", + ); + let mut buffer = Vec::new(); + buffer.extend_from_slice(code_hash); + buffer.extend_from_slice(sp_wasm_interface::VERSION.as_bytes()); + buffer.push(allow_missing_func_imports as u8); + semantics.encode_to(&mut buffer); + + let hash = sp_core::hashing::blake2_256(&buffer); + sp_core::bytes::to_hex(&hash, false) +} + fn decode_version(mut version: &[u8]) -> Result { Decode::decode(&mut version).map_err(|_| { WasmError::Instantiation( @@ -389,6 +543,8 @@ fn create_versioned_wasm_runtime( allow_missing_func_imports: bool, max_instances: usize, cache_path: Option<&Path>, + wasmtime_precompiled: Option<&Path>, + code_hash: &[u8], ) -> Result where H: HostFunctions, @@ -408,6 +564,8 @@ where blob, allow_missing_func_imports, cache_path, + wasmtime_precompiled, + code_hash, )?; // If the runtime blob doesn't embed the runtime version then use the legacy version query diff --git a/substrate/client/executor/wasmtime/Cargo.toml b/substrate/client/executor/wasmtime/Cargo.toml index ef8e5da876aa6..aff43c9b6c991 100644 --- a/substrate/client/executor/wasmtime/Cargo.toml +++ b/substrate/client/executor/wasmtime/Cargo.toml @@ -16,7 +16,7 @@ workspace = true targets = ["x86_64-unknown-linux-gnu"] [dependencies] -log = { workspace = true, default-features = true } +tracing = { workspace = true, default-features = true } cfg-if = { workspace = true } libc = { workspace = true } parking_lot = { workspace = true, default-features = true } @@ -31,8 +31,10 @@ wasmtime = { features = [ "pooling-allocator", ], workspace = true } anyhow = { workspace = true } +codec = { workspace = true, default-features = true } sc-allocator = { workspace = true, default-features = true } sc-executor-common = { workspace = true, default-features = true } +sp-core = { workspace = true, default-features = true } sp-runtime-interface = { workspace = true, default-features = true } sp-wasm-interface = { features = ["wasmtime"], workspace = true, default-features = true } @@ -50,5 +52,4 @@ sc-runtime-test = { workspace = true } sp-io = { workspace = true, default-features = true } tempfile = { workspace = true } paste = { workspace = true, default-features = true } -codec = { workspace = true, default-features = true } cargo_metadata = { workspace = true } diff --git a/substrate/client/executor/wasmtime/src/imports.rs b/substrate/client/executor/wasmtime/src/imports.rs index fccc121fa0887..d4dc16239f078 100644 --- a/substrate/client/executor/wasmtime/src/imports.rs +++ b/substrate/client/executor/wasmtime/src/imports.rs @@ -64,7 +64,7 @@ where if allow_missing_func_imports { for (name, (import_ty, func_ty)) in registry.pending_func_imports { let error = format!("call to a missing function {}:{}", import_ty.module(), name); - log::debug!("Missing import: '{}' {:?}", name, func_ty); + tracing::debug!("Missing import: '{}' {:?}", name, func_ty); linker .func_new("env", &name, func_ty.clone(), move |_, _, _| { Err(anyhow::Error::msg(error.clone())) diff --git a/substrate/client/executor/wasmtime/src/instance_wrapper.rs b/substrate/client/executor/wasmtime/src/instance_wrapper.rs index 4f67d1df9c5f3..426398bcf17da 100644 --- a/substrate/client/executor/wasmtime/src/instance_wrapper.rs +++ b/substrate/client/executor/wasmtime/src/instance_wrapper.rs @@ -89,7 +89,7 @@ impl sc_allocator::Memory for MemoryWrapper<'_, C> { self.0 .grow(&mut self.1, additional as u64) .map_err(|e| { - log::error!( + tracing::error!( target: "wasm-executor", "Failed to grow memory by {} pages: {}", additional, diff --git a/substrate/client/executor/wasmtime/src/lib.rs b/substrate/client/executor/wasmtime/src/lib.rs index 8e8e92017df91..0047c0ff8be4a 100644 --- a/substrate/client/executor/wasmtime/src/lib.rs +++ b/substrate/client/executor/wasmtime/src/lib.rs @@ -38,8 +38,8 @@ mod tests; pub use runtime::{ create_runtime, create_runtime_from_artifact, create_runtime_from_artifact_bytes, - prepare_runtime_artifact, Config, DeterministicStackLimit, InstantiationStrategy, Semantics, - WasmtimeRuntime, + prepare_runtime_artifact, Config, DeterministicStackLimit, InstantiationStrategy, + ModuleVersionStrategy, Semantics, WasmtimeRuntime, }; pub use sc_executor_common::{ runtime_blob::RuntimeBlob, diff --git a/substrate/client/executor/wasmtime/src/runtime.rs b/substrate/client/executor/wasmtime/src/runtime.rs index 286d134ecd171..fa6c9e4e879ce 100644 --- a/substrate/client/executor/wasmtime/src/runtime.rs +++ b/substrate/client/executor/wasmtime/src/runtime.rs @@ -18,6 +18,8 @@ //! Defines the compiled Wasm runtime that uses Wasmtime internally. +pub use wasmtime::ModuleVersionStrategy; + use crate::{ host::HostState, instance_wrapper::{EntryPoint, InstanceWrapper, MemoryWrapper}, @@ -247,7 +249,7 @@ fn common_config(semantics: &Semantics) -> std::result::Result std::result::Result { /// /// We use a `Path` here instead of simply passing a byte slice to allow `wasmtime` to /// map the runtime's linear memory on supported platforms in a copy-on-write fashion. - Precompiled(&'a Path), + Precompiled { + /// Path to the precompiled artifact + compiled_artifact_path: &'a Path, + /// Configure the strategy used for versioning check in deserializing precompiled artifact + module_version_strategy: ModuleVersionStrategy, + }, /// The runtime is instantiated using a precompiled module with the given bytes. /// @@ -527,12 +534,16 @@ where /// different configuration flags. In such case the caller will receive an `Err` deterministically. pub unsafe fn create_runtime_from_artifact( compiled_artifact_path: &Path, + module_version_strategy: ModuleVersionStrategy, config: Config, ) -> std::result::Result where H: HostFunctions, { - do_create_runtime::(CodeSupplyMode::Precompiled(compiled_artifact_path), config) + do_create_runtime::( + CodeSupplyMode::Precompiled { compiled_artifact_path, module_version_strategy }, + config, + ) } /// The same as [`create_runtime`] but takes the bytes of a precompiled artifact, @@ -574,9 +585,16 @@ where replace_strategy_if_broken(&mut config.semantics.instantiation_strategy); let mut wasmtime_config = common_config(&config.semantics)?; + + if let CodeSupplyMode::Precompiled { ref module_version_strategy, .. } = code_supply_mode { + wasmtime_config.module_version(module_version_strategy.clone()).map_err(|e| { + WasmError::Other(format!("fail to apply module_version_strategy: {:#}", e)) + })?; + } + if let Some(ref cache_path) = config.cache_path { if let Err(reason) = setup_wasmtime_caching(cache_path, &mut wasmtime_config) { - log::warn!( + tracing::warn!( "failed to setup wasmtime cache. Performance may degrade significantly: {}.", reason, ); @@ -602,7 +620,7 @@ where (module, InternalInstantiationStrategy::Builtin), } }, - CodeSupplyMode::Precompiled(compiled_artifact_path) => { + CodeSupplyMode::Precompiled { compiled_artifact_path, .. } => { // SAFETY: The unsafety of `deserialize_file` is covered by this function. The // responsibilities to maintain the invariants are passed to the caller. // @@ -661,6 +679,7 @@ fn prepare_blob_for_compilation( /// can then be used for calling [`create_runtime`] avoiding long compilation times. pub fn prepare_runtime_artifact( blob: RuntimeBlob, + module_version_strategy: ModuleVersionStrategy, semantics: &Semantics, ) -> std::result::Result, WasmError> { let mut semantics = semantics.clone(); @@ -668,7 +687,13 @@ pub fn prepare_runtime_artifact( let blob = prepare_blob_for_compilation(blob, &semantics)?; - let engine = Engine::new(&common_config(&semantics)?) + let mut wasmtime_config = common_config(&semantics)?; + + wasmtime_config + .module_version(module_version_strategy) + .map_err(|e| WasmError::Other(format!("fail to apply module_version_strategy: {:#}", e)))?; + + let engine = Engine::new(&wasmtime_config) .map_err(|e| WasmError::Other(format!("cannot create the engine: {:#}", e)))?; engine diff --git a/substrate/client/executor/wasmtime/src/tests.rs b/substrate/client/executor/wasmtime/src/tests.rs index abf2b9509c2b2..6a1afc9eb3322 100644 --- a/substrate/client/executor/wasmtime/src/tests.rs +++ b/substrate/client/executor/wasmtime/src/tests.rs @@ -156,9 +156,17 @@ impl RuntimeBuilder { // Delay the removal of the temporary directory until we're dropped. self.tmpdir = Some(dir); - let artifact = crate::prepare_runtime_artifact(blob, &config.semantics).unwrap(); + let artifact = + crate::prepare_runtime_artifact(blob, Default::default(), &config.semantics) + .unwrap(); std::fs::write(&path, artifact).unwrap(); - unsafe { crate::create_runtime_from_artifact::(&path, config) } + unsafe { + crate::create_runtime_from_artifact::( + &path, + Default::default(), + config, + ) + } } else { crate::create_runtime::(blob, config) } diff --git a/substrate/client/service/src/builder.rs b/substrate/client/service/src/builder.rs index a47a05c0a1901..0fea1e0773d0c 100644 --- a/substrate/client/service/src/builder.rs +++ b/substrate/client/service/src/builder.rs @@ -256,6 +256,7 @@ where ClientConfig { offchain_worker_enabled: config.offchain_worker.enabled, offchain_indexing_api: config.offchain_worker.indexing_enabled, + wasmtime_precompiled: config.executor.wasmtime_precompiled.clone(), wasm_runtime_overrides: config.wasm_runtime_overrides.clone(), no_genesis: config.no_genesis(), wasm_runtime_substitutes, @@ -291,6 +292,7 @@ pub fn new_wasm_executor(config: &ExecutorConfiguration) -> Wa .with_offchain_heap_alloc_strategy(strategy) .with_max_runtime_instances(config.max_runtime_instances) .with_runtime_cache_size(config.runtime_cache_size) + .with_optional_wasmtime_precompiled_path(config.wasmtime_precompiled.as_ref()) .build() } diff --git a/substrate/client/service/src/client/client.rs b/substrate/client/service/src/client/client.rs index eddbb9260c053..7e3ff3341bdd1 100644 --- a/substrate/client/service/src/client/client.rs +++ b/substrate/client/service/src/client/client.rs @@ -164,6 +164,8 @@ pub struct ClientConfig { /// Map of WASM runtime substitute starting at the child of the given block until the runtime /// version doesn't match anymore. pub wasm_runtime_substitutes: HashMap, Vec>, + /// Path where precompiled wasmtime modules exist. + pub wasmtime_precompiled: Option, /// Enable recording of storage proofs during block import pub enable_import_proof_recording: bool, } @@ -176,6 +178,7 @@ impl Default for ClientConfig { wasm_runtime_overrides: None, no_genesis: false, wasm_runtime_substitutes: HashMap::new(), + wasmtime_precompiled: None, enable_import_proof_recording: false, } } diff --git a/substrate/client/service/src/config.rs b/substrate/client/service/src/config.rs index fb9e9264dfe76..2864c58c04e2c 100644 --- a/substrate/client/service/src/config.rs +++ b/substrate/client/service/src/config.rs @@ -336,6 +336,10 @@ pub struct ExecutorConfiguration { pub default_heap_pages: Option, /// Maximum number of different runtime versions that can be cached. pub runtime_cache_size: u8, + /// Directory where local WASM precompiled artifacts live. These wasm modules + /// take precedence over runtimes when the spec and wasm config matches. Set to `None` to + /// disable (default). + pub wasmtime_precompiled: Option, } impl Default for ExecutorConfiguration { @@ -345,6 +349,7 @@ impl Default for ExecutorConfiguration { max_runtime_instances: 8, default_heap_pages: None, runtime_cache_size: 2, + wasmtime_precompiled: None, } } } diff --git a/substrate/primitives/wasm-interface/src/lib.rs b/substrate/primitives/wasm-interface/src/lib.rs index 4fc78ca15535b..751130cc3c46b 100644 --- a/substrate/primitives/wasm-interface/src/lib.rs +++ b/substrate/primitives/wasm-interface/src/lib.rs @@ -46,6 +46,8 @@ if_wasmtime_is_enabled! { pub use anyhow; } +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); + /// Result type used by traits in this crate. #[cfg(feature = "std")] pub type Result = result::Result; diff --git a/substrate/utils/frame/benchmarking-cli/src/overhead/command.rs b/substrate/utils/frame/benchmarking-cli/src/overhead/command.rs index 8df8ee5464f71..84c88e196c59f 100644 --- a/substrate/utils/frame/benchmarking-cli/src/overhead/command.rs +++ b/substrate/utils/frame/benchmarking-cli/src/overhead/command.rs @@ -520,6 +520,7 @@ impl OverheadCmd { no_genesis: false, wasm_runtime_substitutes: Default::default(), enable_import_proof_recording: chain_type.requires_proof_recording(), + wasmtime_precompiled: None, }, )?); diff --git a/templates/parachain/node/src/cli.rs b/templates/parachain/node/src/cli.rs index c8bdbc10d751f..ff79069c08268 100644 --- a/templates/parachain/node/src/cli.rs +++ b/templates/parachain/node/src/cli.rs @@ -39,6 +39,9 @@ pub enum Subcommand { /// The pallet benchmarking moved to the `pallet` sub-command. #[command(subcommand)] Benchmark(frame_benchmarking_cli::BenchmarkCmd), + + /// Precompile the WASM runtime into native code + PrecompileWasm(sc_cli::PrecompileWasmCmd), } const AFTER_HELP_EXAMPLE: &str = color_print::cstr!( diff --git a/templates/parachain/node/src/command.rs b/templates/parachain/node/src/command.rs index 5d9308aed154c..81b7dd4136c76 100644 --- a/templates/parachain/node/src/command.rs +++ b/templates/parachain/node/src/command.rs @@ -14,6 +14,7 @@ use sc_service::config::{BasePath, PrometheusConfig}; use crate::{ chain_spec, cli::{Cli, RelayChainCli, Subcommand}, + sc_service::PartialComponents, service::new_partial, }; @@ -214,6 +215,13 @@ pub fn run() -> Result<()> { _ => Err("Benchmarking sub-command unsupported".into()), } }, + Some(Subcommand::PrecompileWasm(cmd)) => { + let runner = cli.create_runner(cmd)?; + runner.async_run(|config| { + let PartialComponents { task_manager, backend, .. } = new_partial(&config)?; + Ok((cmd.run(backend, config.chain_spec), task_manager)) + }) + }, None => { let runner = cli.create_runner(&cli.run.normalize())?; let collator_options = cli.run.collator_options(); diff --git a/templates/parachain/node/src/service.rs b/templates/parachain/node/src/service.rs index 8c526317283ea..5451b014cb56a 100644 --- a/templates/parachain/node/src/service.rs +++ b/templates/parachain/node/src/service.rs @@ -89,6 +89,7 @@ pub fn new_partial(config: &Configuration) -> Result .with_offchain_heap_alloc_strategy(heap_pages) .with_max_runtime_instances(config.executor.max_runtime_instances) .with_runtime_cache_size(config.executor.runtime_cache_size) + .with_optional_wasmtime_precompiled_path(config.executor.wasmtime_precompiled.as_ref()) .build(); let (client, backend, keystore_container, task_manager) = diff --git a/templates/solochain/node/src/cli.rs b/templates/solochain/node/src/cli.rs index b2c53aa70949d..a20133c27f15d 100644 --- a/templates/solochain/node/src/cli.rs +++ b/templates/solochain/node/src/cli.rs @@ -43,4 +43,7 @@ pub enum Subcommand { /// Db meta columns information. ChainInfo(sc_cli::ChainInfoCmd), + + /// Precompile the WASM runtime into native code + PrecompileWasm(sc_cli::PrecompileWasmCmd), } diff --git a/templates/solochain/node/src/command.rs b/templates/solochain/node/src/command.rs index 1c23e395ede93..b325d7216e03c 100644 --- a/templates/solochain/node/src/command.rs +++ b/templates/solochain/node/src/command.rs @@ -175,6 +175,14 @@ pub fn run() -> sc_cli::Result<()> { let runner = cli.create_runner(cmd)?; runner.sync_run(|config| cmd.run::(&config)) }, + Some(Subcommand::PrecompileWasm(cmd)) => { + let runner = cli.create_runner(cmd)?; + runner.async_run(|config| { + let PartialComponents { task_manager, backend, .. } = + service::new_partial(&config)?; + Ok((cmd.run(backend, config.chain_spec), task_manager)) + }) + }, None => { let runner = cli.create_runner(&cli.run)?; runner.run_node_until_exit(|config| async move {