From cad645b697f7038041e5bc7cfa8b6abb3778a12e Mon Sep 17 00:00:00 2001 From: librelois Date: Tue, 19 Sep 2023 15:58:32 +0200 Subject: [PATCH] add command PrecompileWasmCmd This cherry-pick should match this pull request: https://github.com/paritytech/polkadot-sdk/pull/1641 --- Cargo.lock | 5 + .../core/pvf/common/src/executor_interface.rs | 2 +- .../bin/node/cli/benches/block_production.rs | 1 + .../bin/node/cli/benches/transaction_pool.rs | 1 + substrate/bin/node/cli/src/cli.rs | 3 + substrate/bin/node/cli/src/command.rs | 8 + .../client/chain-spec/src/genesis_block.rs | 5 +- substrate/client/cli/Cargo.toml | 6 + substrate/client/cli/src/commands/mod.rs | 5 +- .../cli/src/commands/precompile_wasm_cmd.rs | 143 +++++++++++++ substrate/client/cli/src/config.rs | 8 + .../client/cli/src/params/import_params.rs | 16 ++ substrate/client/cli/src/runner.rs | 1 + substrate/client/executor/Cargo.toml | 1 + substrate/client/executor/benches/bench.rs | 5 +- substrate/client/executor/common/Cargo.toml | 1 + .../executor/common/src/wasm_runtime.rs | 2 +- substrate/client/executor/src/executor.rs | 21 ++ substrate/client/executor/src/lib.rs | 11 +- substrate/client/executor/src/wasm_runtime.rs | 201 ++++++++++++++++-- substrate/client/executor/wasmtime/Cargo.toml | 2 +- substrate/client/executor/wasmtime/src/lib.rs | 4 +- .../client/executor/wasmtime/src/runtime.rs | 38 +++- .../client/executor/wasmtime/src/tests.rs | 6 +- substrate/client/service/src/builder.rs | 12 +- .../service/src/client/call_executor.rs | 5 +- substrate/client/service/src/client/client.rs | 3 + substrate/client/service/src/config.rs | 5 + .../primitives/wasm-interface/src/lib.rs | 2 + templates/parachain/node/src/service.rs | 11 +- templates/solochain/node/src/cli.rs | 3 + templates/solochain/node/src/command.rs | 8 + 32 files changed, 493 insertions(+), 52 deletions(-) create mode 100644 substrate/client/cli/src/commands/precompile_wasm_cmd.rs diff --git a/Cargo.lock b/Cargo.lock index 499fa65a1d1d..232ece995f9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17261,6 +17261,7 @@ dependencies = [ "rpassword", "sc-client-api", "sc-client-db", + "sc-executor", "sc-keystore", "sc-mixnet", "sc-network", @@ -17276,6 +17277,8 @@ dependencies = [ "sp-keystore", "sp-panic-handler", "sp-runtime", + "sp-state-machine", + "sp-storage", "sp-tracing", "sp-version", "tempfile", @@ -17721,6 +17724,7 @@ dependencies = [ "array-bytes", "assert_matches", "criterion", + "log", "num_cpus", "parity-scale-codec", "parking_lot 0.12.3", @@ -17757,6 +17761,7 @@ dependencies = [ name = "sc-executor-common" version = "0.35.0" dependencies = [ + "parity-scale-codec", "polkavm 0.9.3", "sc-allocator", "sp-maybe-compressed-blob", diff --git a/polkadot/node/core/pvf/common/src/executor_interface.rs b/polkadot/node/core/pvf/common/src/executor_interface.rs index 47f9ed1604e7..c02db118b7e4 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/benches/block_production.rs b/substrate/bin/node/cli/benches/block_production.rs index de883d1051f5..f7ff8fa8e33b 100644 --- a/substrate/bin/node/cli/benches/block_production.rs +++ b/substrate/bin/node/cli/benches/block_production.rs @@ -107,6 +107,7 @@ fn new_node(tokio_handle: Handle) -> node_cli::service::NewFullBase { announce_block: true, data_path: base_path.path().into(), base_path, + wasmtime_precompiled: None, wasm_runtime_overrides: None, }; diff --git a/substrate/bin/node/cli/benches/transaction_pool.rs b/substrate/bin/node/cli/benches/transaction_pool.rs index efec081427f4..aa4b0e388f2e 100644 --- a/substrate/bin/node/cli/benches/transaction_pool.rs +++ b/substrate/bin/node/cli/benches/transaction_pool.rs @@ -99,6 +99,7 @@ fn new_node(tokio_handle: Handle) -> node_cli::service::NewFullBase { announce_block: true, data_path: base_path.path().into(), base_path, + wasmtime_precompiled: None, wasm_runtime_overrides: None, }; diff --git a/substrate/bin/node/cli/src/cli.rs b/substrate/bin/node/cli/src/cli.rs index c0dcacb2e4b4..4c1348c39f1a 100644 --- a/substrate/bin/node/cli/src/cli.rs +++ b/substrate/bin/node/cli/src/cli.rs @@ -99,4 +99,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 51fbf0904cf8..76ae69232ab9 100644 --- a/substrate/bin/node/cli/src/command.rs +++ b/substrate/bin/node/cli/src/command.rs @@ -227,5 +227,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, .. } = + new_partial(&config, None)?; + Ok((cmd.run(backend, config.chain_spec), task_manager)) + }) + }, } } diff --git a/substrate/client/chain-spec/src/genesis_block.rs b/substrate/client/chain-spec/src/genesis_block.rs index 3c7b9f64dcd6..5aa0c5c562da 100644 --- a/substrate/client/chain-spec/src/genesis_block.rs +++ b/substrate/client/chain-spec/src/genesis_block.rs @@ -23,7 +23,10 @@ use std::{marker::PhantomData, sync::Arc}; use codec::Encode; use sc_client_api::{backend::Backend, BlockImportOperation}; use sc_executor::RuntimeVersionOf; -use sp_core::storage::{well_known_keys, StateVersion, Storage}; +use sp_core::{ + storage::{well_known_keys, StateVersion, Storage}, +}; + use sp_runtime::{ traits::{Block as BlockT, Hash as HashT, HashingFor, Header as HeaderT, Zero}, BuildStorage, diff --git a/substrate/client/cli/Cargo.toml b/substrate/client/cli/Cargo.toml index 3cc77479ed66..05dfb5060dfc 100644 --- a/substrate/client/cli/Cargo.toml +++ b/substrate/client/cli/Cargo.toml @@ -65,6 +65,12 @@ sp-runtime.workspace = true sp-runtime.default-features = true sp-version.workspace = true sp-version.default-features = true +sp-state-machine.workspace = true +sp-state-machine.default-features = true +sc-executor.workspace = true +sc-executor.default-features = true +sp-storage.workspace = true +sp-storage.default-features = true [dev-dependencies] tempfile = { workspace = true } diff --git a/substrate/client/cli/src/commands/mod.rs b/substrate/client/cli/src/commands/mod.rs index 2d7a0dc72ff5..abe495f73172 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 000000000000..98cf4255021e --- /dev/null +++ b/substrate/client/cli/src/commands/precompile_wasm_cmd.rs @@ -0,0 +1,143 @@ +// 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::{DEFAULT_WASMTIME_INSTANTIATION_STRATEGY, execution_method_from_cli, WasmExecutionMethod, WasmtimeInstantiationStrategy}, + error::{self, Error}, + params::{DatabaseParams, PruningParams, SharedParams}, + CliConfiguration +}; + +use clap::Parser; +use sc_client_api::{Backend, HeaderBackend}; +use sc_executor::{ + HeapAllocStrategy, DEFAULT_HEAP_ALLOC_PAGES, precompile_and_serialize_versioned_wasm_runtime, +}; +use sc_service::ChainSpec; +use sp_core::traits::RuntimeCode; +use sp_runtime::traits::Block as BlockT; +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. +#[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()] + 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: sp_core::blake2_256(&wasm_bytecode).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)))?; + } + } + + + Ok(()) + } +} + +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 59238b3307cf..29220cb008d1 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`. @@ -532,6 +539,7 @@ pub trait CliConfiguration: Sized { blocks_pruning: self.blocks_pruning()?, executor: ExecutorConfiguration { wasm_method: self.wasm_method()?, + wasmtime_precompiled: self.wasmtime_precompiled(), default_heap_pages: self.default_heap_pages()?, max_runtime_instances, runtime_cache_size, diff --git a/substrate/client/cli/src/params/import_params.rs b/substrate/client/cli/src/params/import_params.rs index add7cb4f8505..c2387d3ccba5 100644 --- a/substrate/client/cli/src/params/import_params.rs +++ b/substrate/client/cli/src/params/import_params.rs @@ -65,6 +65,16 @@ 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-runtimes` 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")] + pub wasmtime_precompiled: Option, + /// Specify the path where local WASM runtimes are stored. /// /// These runtimes will override on-chain runtimes when the version matches. @@ -107,6 +117,12 @@ 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 { diff --git a/substrate/client/cli/src/runner.rs b/substrate/client/cli/src/runner.rs index 9c5834d8d80a..1f63c3b2222d 100644 --- a/substrate/client/cli/src/runner.rs +++ b/substrate/client/cli/src/runner.rs @@ -266,6 +266,7 @@ mod tests { .build(), ), executor: ExecutorConfiguration::default(), + wasmtime_precompiled: None, wasm_runtime_overrides: None, rpc: RpcConfiguration { addr: None, diff --git a/substrate/client/executor/Cargo.toml b/substrate/client/executor/Cargo.toml index 89f5e4eb5732..161469428130 100644 --- a/substrate/client/executor/Cargo.toml +++ b/substrate/client/executor/Cargo.toml @@ -17,6 +17,7 @@ workspace = true targets = ["x86_64-unknown-linux-gnu"] [dependencies] +log = { workspace = true, default-features = true } parking_lot = { workspace = true, default-features = true } schnellru = { workspace = true } tracing = { workspace = true, default-features = true } diff --git a/substrate/client/executor/benches/bench.rs b/substrate/client/executor/benches/bench.rs index 4cde8c2a4a64..50be53b642d9 100644 --- a/substrate/client/executor/benches/bench.rs +++ b/substrate/client/executor/benches/bench.rs @@ -66,12 +66,13 @@ fn initialize( wasm_bulk_memory: false, wasm_reference_types: false, wasm_simd: false, + module_version_strategy: Default::default(), }, }; if precompile { let precompiled_blob = - sc_executor_wasmtime::prepare_runtime_artifact(blob, &config.semantics) + sc_executor_wasmtime::prepare_runtime_artifact(blob, Default::default(), &config.semantics) .unwrap(); // Create a fresh temporary directory to make absolutely sure @@ -84,7 +85,7 @@ fn initialize( unsafe { sc_executor_wasmtime::create_runtime_from_artifact::< sp_io::SubstrateHostFunctions, - >(&path, config) + >(&path, Default::defaut(), 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 c6998a173cf7..378c0cd80c5f 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 = { package = "parity-scale-codec", version = "3.6.1" } thiserror = { workspace = true } wasm-instrument = { workspace = true, default-features = true } sc-allocator.workspace = true diff --git a/substrate/client/executor/common/src/wasm_runtime.rs b/substrate/client/executor/common/src/wasm_runtime.rs index e8f429a3dbb2..778731c964c4 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 913bcdfcfe59..f322894a2564 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,17 @@ impl WasmExecutorBuilder { self } + /// Create the wasm executor with the given `wasmtime_precompiled_path`. + /// + /// The `wasmtime_precompiled_path` is a path to a directory where the executor load precompiled + /// wasmtime modules. + /// + /// By default there is no `wasmtime_precompiled_path` given. + pub fn with_wasmtime_precompiled_path(mut self, wasmtime_precompiled_path: impl Into) -> Self { + self.wasmtime_precompiled_path = Some(wasmtime_precompiled_path.into()); + self + } + /// Build the configured [`WasmExecutor`]. pub fn build(self) -> WasmExecutor { WasmExecutor { @@ -211,6 +224,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 +248,8 @@ pub struct WasmExecutor { cache_path: Option, /// Ignore missing function imports. allow_missing_host_functions: bool, + /// TODO + wasmtime_precompiled_path: Option, phantom: PhantomData, } @@ -247,6 +263,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 +318,7 @@ impl WasmExecutor { )), cache_path, allow_missing_host_functions: false, + wasmtime_precompiled_path: None, phantom: PhantomData, } } @@ -353,6 +371,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 +449,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/lib.rs b/substrate/client/executor/src/lib.rs index 204f1ff22d74..0ad3fe14de4a 100644 --- a/substrate/client/executor/src/lib.rs +++ b/substrate/client/executor/src/lib.rs @@ -36,17 +36,20 @@ mod executor; mod integration_tests; mod wasm_runtime; -pub use codec::Codec; #[allow(deprecated)] -pub use executor::NativeElseWasmExecutor; -pub use executor::{with_externalities_safe, NativeExecutionDispatch, WasmExecutor}; +pub use self::{ + executor::{ + with_externalities_safe, NativeElseWasmExecutor, NativeExecutionDispatch, WasmExecutor, + }, + wasm_runtime::{read_embedded_version, precompile_and_serialize_versioned_wasm_runtime, WasmExecutionMethod}, +}; +pub use codec::Codec; #[doc(hidden)] pub use sp_core::traits::Externalities; 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 sc_executor_common::{ error, diff --git a/substrate/client/executor/src/wasm_runtime.rs b/substrate/client/executor/src/wasm_runtime.rs index be8344ba79b7..cf98b3fbf088 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, @@ -35,6 +35,7 @@ use sp_version::RuntimeVersion; use sp_wasm_interface::HostFunctions; use std::{ + io::Write, panic::AssertUnwindSafe, path::{Path, PathBuf}, sync::Arc, @@ -220,6 +221,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 +257,8 @@ impl RuntimeCache { allow_missing_func_imports, self.max_runtime_instances, self.cache_path.as_deref(), + wasmtime_precompiled, + code_hash, ); match result { @@ -293,6 +297,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 +308,180 @@ where } match wasm_method { - WasmExecutionMethod::Compiled { instantiation_strategy } => - 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, + 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 handle_err = |e: std::io::Error| -> WasmError { + return WasmError::Instantiation(format!("Io error when loading wasmtime precompiled folder ({}): {}", wasmtime_precompiled_dir.display(), e)); + }; + let mut maybe_compiled_artifact = None; + + let artifact_version = + compute_artifact_version(allow_missing_func_imports, code_hash, &semantics); + log::debug!( + target: "wasmtime-runtime", + "Searching for wasm hash: {}", + artifact_version.clone() + ); + + for entry in std::fs::read_dir(wasmtime_precompiled_dir).map_err(handle_err)? { + let entry = entry.map_err(handle_err)?; + if let Some(file_name) = entry.file_name().to_str() { + // We check that the artifact was generated for this specific artifact + // version and with the same wasm interface version and configuration. + if file_name.contains(&artifact_version.clone()) { + log::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 + maybe_compiled_artifact = Some(( + entry.path(), + sc_executor_wasmtime::ModuleVersionStrategy::Custom( + artifact_version.clone(), + ), + )); + } + } else { + return Err(WasmError::Instantiation("wasmtime precompiled folder contain a file with invalid utf8 name".to_owned())); + } + } + + if let Some((compiled_artifact_path, module_version_strategy)) = maybe_compiled_artifact { + // # 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. + unsafe { + sc_executor_wasmtime::create_runtime_from_artifact::( + &compiled_artifact_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) }) + } else { + sc_executor_wasmtime::create_runtime::( + blob, + sc_executor_wasmtime::Config { + allow_missing_func_imports, + cache_path: cache_path.map(ToOwned::to_owned), + semantics, + }, + ) + .map(|runtime| -> Box { Box::new(runtime) }) + } + } else { + sc_executor_wasmtime::create_runtime::( + blob, + sc_executor_wasmtime::Config { + allow_missing_func_imports, + cache_path: cache_path.map(ToOwned::to_owned), + 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); + log::debug!( + target: "wasmtime-runtime", + "Generated precompiled wasm hash: {}", + artifact_version.clone() + ); + + 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 + let mut file = std::fs::File::create(wasmtime_precompiled_path.join(format!("precompiled_wasm_{}", &artifact_version))) + .map_err(|e| WasmError::Other(format!("Fail to create file 'precompiled_wasm_0x{}', I/O Error: {}", &artifact_version, e)))?; + file.write_all(&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 { + log::trace!( + target: "wasmtime-runtime", + "Computing wasm runtime hash [allow_missing_func_imports: {}, code_hash: {}, semantics: {:?}]", + allow_missing_func_imports, sp_core::bytes::to_hex(&code_hash, false), semantics + ); + 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 +546,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 +567,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 935d60d9d442..defeefaf874a 100644 --- a/substrate/client/executor/wasmtime/Cargo.toml +++ b/substrate/client/executor/wasmtime/Cargo.toml @@ -17,6 +17,7 @@ targets = ["x86_64-unknown-linux-gnu"] [dependencies] log = { workspace = true, default-features = true } +codec = { workspace = true, default-features = true } cfg-if = { workspace = true } libc = { workspace = true } parking_lot = { workspace = true, default-features = true } @@ -53,5 +54,4 @@ sc-runtime-test = { path = "../runtime-test" } sp-io = { default-features = true, path = "../../../primitives/io" } 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/lib.rs b/substrate/client/executor/wasmtime/src/lib.rs index 8e8e92017df9..0047c0ff8be4 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 286d134ecd17..16c701d5fe14 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}, @@ -354,7 +356,7 @@ fn common_config(semantics: &Semantics) -> 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,13 @@ 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,6 +582,13 @@ 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!( @@ -602,7 +617,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 +676,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,8 +684,14 @@ pub fn prepare_runtime_artifact( let blob = prepare_blob_for_compilation(blob, &semantics)?; - let engine = Engine::new(&common_config(&semantics)?) - .map_err(|e| WasmError::Other(format!("cannot create the engine: {:#}", e)))?; + 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 .precompile_module(&blob.serialize()) diff --git a/substrate/client/executor/wasmtime/src/tests.rs b/substrate/client/executor/wasmtime/src/tests.rs index f86a42757694..b29f6d13c2a9 100644 --- a/substrate/client/executor/wasmtime/src/tests.rs +++ b/substrate/client/executor/wasmtime/src/tests.rs @@ -146,6 +146,7 @@ impl RuntimeBuilder { wasm_bulk_memory: false, wasm_reference_types: false, wasm_simd: false, + module_version_strategy: Default::default(), }, }; @@ -156,9 +157,9 @@ 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) } @@ -473,6 +474,7 @@ fn test_instances_without_reuse_are_not_leaked() { wasm_bulk_memory: false, wasm_reference_types: false, wasm_simd: false, + module_version_strategy: Default::default(), }, }, ) diff --git a/substrate/client/service/src/builder.rs b/substrate/client/service/src/builder.rs index 0dc28d1361cb..2c47f3238c83 100644 --- a/substrate/client/service/src/builder.rs +++ b/substrate/client/service/src/builder.rs @@ -244,6 +244,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, @@ -273,13 +274,18 @@ pub fn new_wasm_executor(config: &ExecutorConfiguration) -> Wa let strategy = config .default_heap_pages .map_or(DEFAULT_HEAP_ALLOC_STRATEGY, |p| HeapAllocStrategy::Static { extra_pages: p as _ }); - WasmExecutor::::builder() + let mut wasm_builder = WasmExecutor::::builder() .with_execution_method(config.wasm_method) .with_onchain_heap_alloc_strategy(strategy) .with_offchain_heap_alloc_strategy(strategy) .with_max_runtime_instances(config.max_runtime_instances) - .with_runtime_cache_size(config.runtime_cache_size) - .build() + .with_runtime_cache_size(config.runtime_cache_size); + + if let Some(ref wasmtime_precompiled_path) = config.wasmtime_precompiled { + wasm_builder = wasm_builder.with_wasmtime_precompiled_path(wasmtime_precompiled_path); + } + + wasm_builder.build() } /// Create an instance of default DB-backend backend. diff --git a/substrate/client/service/src/client/call_executor.rs b/substrate/client/service/src/client/call_executor.rs index 1341aa0e7205..077a7e6b7944 100644 --- a/substrate/client/service/src/client/call_executor.rs +++ b/substrate/client/service/src/client/call_executor.rs @@ -29,7 +29,7 @@ use sp_runtime::{ traits::{Block as BlockT, HashingFor}, }; use sp_state_machine::{backend::AsTrieBackend, OverlayedChanges, StateMachine, StorageProof}; -use std::{cell::RefCell, sync::Arc}; +use std::{cell::RefCell, path::PathBuf, sync::Arc}; /// Call executor that executes methods locally, querying all required /// data from local backend. @@ -37,6 +37,7 @@ pub struct LocalCallExecutor { backend: Arc, executor: E, code_provider: CodeProvider, + wasmtime_precompiled_path: Option, execution_extensions: Arc>, } @@ -58,6 +59,7 @@ where backend, executor, code_provider, + wasmtime_precompiled_path: client_config.wasmtime_precompiled, execution_extensions: Arc::new(execution_extensions), }) } @@ -72,6 +74,7 @@ where backend: self.backend.clone(), executor: self.executor.clone(), code_provider: self.code_provider.clone(), + wasmtime_precompiled_path: self.wasmtime_precompiled_path.clone(), execution_extensions: self.execution_extensions.clone(), } } diff --git a/substrate/client/service/src/client/client.rs b/substrate/client/service/src/client/client.rs index 22defd7c5514..4b7fc0af4770 100644 --- a/substrate/client/service/src/client/client.rs +++ b/substrate/client/service/src/client/client.rs @@ -201,6 +201,8 @@ pub struct ClientConfig { pub wasm_runtime_substitutes: HashMap, Vec>, /// Enable recording of storage proofs during block import pub enable_import_proof_recording: bool, + /// Path where precompiled wasmtime modules exist. + pub wasmtime_precompiled: Option, } impl Default for ClientConfig { @@ -212,6 +214,7 @@ impl Default for ClientConfig { no_genesis: false, wasm_runtime_substitutes: HashMap::new(), enable_import_proof_recording: false, + wasmtime_precompiled: None, } } } diff --git a/substrate/client/service/src/config.rs b/substrate/client/service/src/config.rs index 6f65c2e2d81b..79e08df576f4 100644 --- a/substrate/client/service/src/config.rs +++ b/substrate/client/service/src/config.rs @@ -328,6 +328,10 @@ pub struct RpcConfiguration { pub struct ExecutorConfiguration { /// Wasm execution method. pub wasm_method: WasmExecutionMethod, + /// 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, /// The size of the instances cache. /// /// The default value is 8. @@ -342,6 +346,7 @@ impl Default for ExecutorConfiguration { fn default() -> Self { Self { wasm_method: WasmExecutionMethod::default(), + wasmtime_precompiled: Default::default(), max_runtime_instances: 8, default_heap_pages: None, runtime_cache_size: 2, diff --git a/substrate/primitives/wasm-interface/src/lib.rs b/substrate/primitives/wasm-interface/src/lib.rs index 4fc78ca15535..751130cc3c46 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/templates/parachain/node/src/service.rs b/templates/parachain/node/src/service.rs index 04e20be2bd40..6cedd0e0acb0 100644 --- a/templates/parachain/node/src/service.rs +++ b/templates/parachain/node/src/service.rs @@ -81,19 +81,22 @@ pub fn new_partial(config: &Configuration) -> Result .default_heap_pages .map_or(DEFAULT_HEAP_ALLOC_STRATEGY, |h| HeapAllocStrategy::Static { extra_pages: h as _ }); - let executor = ParachainExecutor::builder() + let mut wasm_builder = WasmExecutor::builder() .with_execution_method(config.executor.wasm_method) .with_onchain_heap_alloc_strategy(heap_pages) .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) - .build(); + .with_runtime_cache_size(config.executor.runtime_cache_size); + if let Some(ref wasmtime_precompiled_path) = config.executor.wasmtime_precompiled { + wasm_builder = wasm_builder.with_wasmtime_precompiled_path(wasmtime_precompiled_path); + } + let wasm = wasm_builder.build(); let (client, backend, keystore_container, task_manager) = sc_service::new_full_parts_record_import::( config, telemetry.as_ref().map(|(_, telemetry)| telemetry.handle()), - executor, + wasm, true, )?; let client = Arc::new(client); diff --git a/templates/solochain/node/src/cli.rs b/templates/solochain/node/src/cli.rs index b2c53aa70949..a20133c27f15 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 624ace1bf350..8680af45846e 100644 --- a/templates/solochain/node/src/command.rs +++ b/templates/solochain/node/src/command.rs @@ -174,6 +174,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 {