diff --git a/substrate/Cargo.lock b/substrate/Cargo.lock index c165c3ccb9..1560b7bddd 100644 --- a/substrate/Cargo.lock +++ b/substrate/Cargo.lock @@ -7321,6 +7321,7 @@ dependencies = [ "sp-utils", "sp-version", "substrate-prometheus-endpoint", + "substrate-test-runtime", "substrate-test-runtime-client", "tempfile", "tokio 0.2.22", diff --git a/substrate/client/cli/src/config.rs b/substrate/client/cli/src/config.rs index 43b7551002..ab7a335c1c 100644 --- a/substrate/client/cli/src/config.rs +++ b/substrate/client/cli/src/config.rs @@ -278,6 +278,15 @@ pub trait CliConfiguration: Sized { .unwrap_or_default()) } + /// Get the path where WASM overrides live. + /// + /// By default this is `None`. + fn wasm_runtime_overrides(&self) -> Option { + self.import_params() + .map(|x| x.wasm_runtime_overrides()) + .unwrap_or_default() + } + /// Get the execution strategies. /// /// By default this is retrieved from `ImportParams` if it is available. Otherwise its @@ -492,6 +501,7 @@ pub trait CliConfiguration: Sized { state_cache_child_ratio: self.state_cache_child_ratio()?, pruning: self.pruning(unsafe_pruning, &role)?, wasm_method: self.wasm_method()?, + wasm_runtime_overrides: self.wasm_runtime_overrides(), execution_strategies: self.execution_strategies(is_dev, is_validator)?, rpc_http: self.rpc_http(DCV::rpc_http_listen_port())?, rpc_ws: self.rpc_ws(DCV::rpc_ws_listen_port())?, diff --git a/substrate/client/cli/src/params/import_params.rs b/substrate/client/cli/src/params/import_params.rs index e60779429b..1efd438343 100644 --- a/substrate/client/cli/src/params/import_params.rs +++ b/substrate/client/cli/src/params/import_params.rs @@ -25,6 +25,7 @@ use crate::params::DatabaseParams; use crate::params::PruningParams; use sc_client_api::execution_extensions::ExecutionStrategies; use structopt::StructOpt; +use std::path::PathBuf; /// Parameters for block import. #[derive(Debug, StructOpt)] @@ -55,6 +56,12 @@ pub struct ImportParams { )] pub wasm_method: WasmExecutionMethod, + /// Specify the path where local WASM runtimes are stored. + /// + /// These runtimes will override on-chain runtimes when the version matches. + #[structopt(long, value_name = "PATH", parse(from_os_str))] + pub wasm_runtime_overrides: Option, + #[allow(missing_docs)] #[structopt(flatten)] pub execution_strategies: ExecutionStrategiesParams, @@ -103,6 +110,12 @@ impl ImportParams { self.wasm_method.into() } + /// 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() + } + /// Get execution strategies for the parameters pub fn execution_strategies(&self, is_dev: bool, is_validator: bool) -> ExecutionStrategies { let exec = &self.execution_strategies; diff --git a/substrate/client/service/Cargo.toml b/substrate/client/service/Cargo.toml index 14a1ce6aa0..5e2f8d051d 100644 --- a/substrate/client/service/Cargo.toml +++ b/substrate/client/service/Cargo.toml @@ -86,6 +86,7 @@ directories = "2.0.2" [dev-dependencies] substrate-test-runtime-client = { version = "2.0.0", path = "../../test-utils/runtime/client" } +substrate-test-runtime = { versino = "2.0.0", path = "../../test-utils/runtime/" } sp-consensus-babe = { version = "0.8.0", path = "../../primitives/consensus/babe" } grandpa = { version = "0.8.0", package = "sc-finality-grandpa", path = "../finality-grandpa" } grandpa-primitives = { version = "2.0.0", package = "sp-finality-grandpa", path = "../../primitives/finality-grandpa" } diff --git a/substrate/client/service/src/builder.rs b/substrate/client/service/src/builder.rs index 3b60db7ec5..2a4dda477a 100644 --- a/substrate/client/service/src/builder.rs +++ b/substrate/client/service/src/builder.rs @@ -303,8 +303,9 @@ pub fn new_full_parts( Box::new(task_manager.spawn_handle()), config.prometheus_config.as_ref().map(|config| config.registry.clone()), ClientConfig { - offchain_worker_enabled : config.offchain_worker.enabled , + offchain_worker_enabled : config.offchain_worker.enabled, offchain_indexing_api: config.offchain_worker.indexing_enabled, + wasm_runtime_overrides: config.wasm_runtime_overrides.clone(), }, )? }; @@ -396,7 +397,7 @@ pub fn new_client( const CANONICALIZATION_DELAY: u64 = 4096; let backend = Arc::new(Backend::new(settings, CANONICALIZATION_DELAY)?); - let executor = crate::client::LocalCallExecutor::new(backend.clone(), executor, spawn_handle, config.clone()); + let executor = crate::client::LocalCallExecutor::new(backend.clone(), executor, spawn_handle, config.clone())?; Ok(( crate::client::Client::new( backend.clone(), diff --git a/substrate/client/service/src/client/call_executor.rs b/substrate/client/service/src/client/call_executor.rs index 1919c76ff4..164976ecfe 100644 --- a/substrate/client/service/src/client/call_executor.rs +++ b/substrate/client/service/src/client/call_executor.rs @@ -28,36 +28,71 @@ use sp_state_machine::{ use sc_executor::{RuntimeVersion, RuntimeInfo, NativeVersion}; use sp_externalities::Extensions; use sp_core::{ - NativeOrEncoded, NeverNativeValue, traits::{CodeExecutor, SpawnNamed}, + NativeOrEncoded, NeverNativeValue, traits::{CodeExecutor, SpawnNamed, RuntimeCode}, offchain::storage::OffchainOverlayedChanges, }; use sp_api::{ProofRecorder, InitializeBlock, StorageTransactionCache}; use sc_client_api::{backend, call_executor::CallExecutor}; -use super::client::ClientConfig; +use super::{client::ClientConfig, wasm_override::WasmOverride}; /// Call executor that executes methods locally, querying all required /// data from local backend. pub struct LocalCallExecutor { backend: Arc, executor: E, + wasm_override: Option>, spawn_handle: Box, client_config: ClientConfig, } -impl LocalCallExecutor { +impl LocalCallExecutor +where + E: CodeExecutor + RuntimeInfo + Clone + 'static +{ /// Creates new instance of local call executor. pub fn new( backend: Arc, executor: E, spawn_handle: Box, client_config: ClientConfig, - ) -> Self { - LocalCallExecutor { + ) -> sp_blockchain::Result { + let wasm_override = client_config.wasm_runtime_overrides + .as_ref() + .map(|p| WasmOverride::new(p.clone(), executor.clone())) + .transpose()?; + + Ok(LocalCallExecutor { backend, executor, + wasm_override, spawn_handle, client_config, - } + }) + } + + /// Check if local runtime code overrides are enabled and one is available + /// for the given `BlockId`. If yes, return it; otherwise return the same + /// `RuntimeCode` instance that was passed. + fn check_override<'a, Block>( + &'a self, + onchain_code: RuntimeCode<'a>, + id: &BlockId, + ) -> sp_blockchain::Result> + where + Block: BlockT, + B: backend::Backend, + { + let code = self.wasm_override + .as_ref() + .map::>, _>(|o| { + let spec = self.runtime_version(id)?.spec_version; + Ok(o.get(&spec, onchain_code.heap_pages)) + }) + .transpose()? + .flatten() + .unwrap_or(onchain_code); + + Ok(code) } } @@ -66,6 +101,7 @@ impl Clone for LocalCallExecutor where E: Clone { LocalCallExecutor { backend: self.backend.clone(), executor: self.executor.clone(), + wasm_override: self.wasm_override.clone(), spawn_handle: self.spawn_handle.clone(), client_config: self.client_config.clone(), } @@ -101,6 +137,8 @@ where )?; let state = self.backend.state_at(*id)?; let state_runtime_code = sp_state_machine::backend::BackendRuntimeCode::new(&state); + let runtime_code = self.check_override(state_runtime_code.runtime_code()?, id)?; + let return_data = StateMachine::new( &state, changes_trie, @@ -110,7 +148,7 @@ where method, call_data, extensions.unwrap_or_default(), - &state_runtime_code.runtime_code()?, + &runtime_code, self.spawn_handle.clone(), ).execute_using_consensus_failure_handler::<_, NeverNativeValue, fn() -> _>( strategy.get_manager(), @@ -173,7 +211,7 @@ where let state_runtime_code = sp_state_machine::backend::BackendRuntimeCode::new(&trie_state); // It is important to extract the runtime code here before we create the proof // recorder. - let runtime_code = state_runtime_code.runtime_code()?; + let runtime_code = self.check_override(state_runtime_code.runtime_code()?, at)?; let backend = sp_state_machine::ProvingBackend::new_with_recorder( trie_state, @@ -198,7 +236,8 @@ where }, None => { let state_runtime_code = sp_state_machine::backend::BackendRuntimeCode::new(&state); - let runtime_code = state_runtime_code.runtime_code()?; + let runtime_code = self.check_override(state_runtime_code.runtime_code()?, at)?; + let mut state_machine = StateMachine::new( &state, changes_trie_state, @@ -279,3 +318,66 @@ impl sp_version::GetRuntimeVersion for LocalCallExecutor::new(WasmExecutionMethod::Interpreted, Some(128), 1); + + let overrides = crate::client::wasm_override::dummy_overrides(&executor); + let onchain_code = WrappedRuntimeCode(substrate_test_runtime::wasm_binary_unwrap().into()); + let onchain_code = RuntimeCode { + code_fetcher: &onchain_code, + heap_pages: Some(128), + hash: vec![0, 0, 0, 0], + }; + + let backend = Arc::new(in_mem::Backend::::new()); + + // wasm_runtime_overrides is `None` here because we construct the + // LocalCallExecutor directly later on + let client_config = ClientConfig { + offchain_worker_enabled: false, + offchain_indexing_api: false, + wasm_runtime_overrides: None, + }; + + // client is used for the convenience of creating and inserting the genesis block. + let _client = substrate_test_runtime_client::client::new_with_backend::< + _, + _, + runtime::Block, + _, + runtime::RuntimeApi, + >( + backend.clone(), + executor.clone(), + &substrate_test_runtime_client::GenesisParameters::default().genesis_storage(), + None, + Box::new(TaskExecutor::new()), + None, + Default::default(), + ).expect("Creates a client"); + + let call_executor = LocalCallExecutor { + backend: backend.clone(), + executor, + wasm_override: Some(overrides), + spawn_handle: Box::new(TaskExecutor::new()), + client_config, + }; + + let check = call_executor.check_override(onchain_code, &BlockId::Number(Default::default())) + .expect("RuntimeCode override"); + + assert_eq!(Some(vec![2, 2, 2, 2, 2, 2, 2, 2]), check.fetch_runtime_code().map(Into::into)); + } +} diff --git a/substrate/client/service/src/client/client.rs b/substrate/client/service/src/client/client.rs index 1b07e6b4f7..d423fdee39 100644 --- a/substrate/client/service/src/client/client.rs +++ b/substrate/client/service/src/client/client.rs @@ -22,6 +22,7 @@ use std::{ marker::PhantomData, collections::{HashSet, BTreeMap, HashMap}, sync::Arc, panic::UnwindSafe, result, + path::PathBuf }; use log::{info, trace, warn}; use parking_lot::{Mutex, RwLock}; @@ -181,6 +182,8 @@ pub struct ClientConfig { pub offchain_worker_enabled: bool, /// If true, allows access from the runtime to write into offchain worker db. pub offchain_indexing_api: bool, + /// Path where WASM files exist to override the on-chain WASM. + pub wasm_runtime_overrides: Option, } /// Create a client with the explicitly provided backend. @@ -201,7 +204,7 @@ pub fn new_with_backend( Block: BlockT, B: backend::LocalBackend + 'static, { - let call_executor = LocalCallExecutor::new(backend.clone(), executor, spawn_handle, config.clone()); + let call_executor = LocalCallExecutor::new(backend.clone(), executor, spawn_handle, config.clone())?; let extensions = ExecutionExtensions::new(Default::default(), keystore); Client::new( backend, diff --git a/substrate/client/service/src/client/light.rs b/substrate/client/service/src/client/light.rs index e8e1286ecc..6d4f9aa1c9 100644 --- a/substrate/client/service/src/client/light.rs +++ b/substrate/client/service/src/client/light.rs @@ -60,7 +60,7 @@ pub fn new_light( code_executor, spawn_handle.clone(), ClientConfig::default() - ); + )?; let executor = GenesisCallExecutor::new(backend.clone(), local_executor); Client::new( backend, diff --git a/substrate/client/service/src/client/mod.rs b/substrate/client/service/src/client/mod.rs index 7c96f61a78..b3aa2fa076 100644 --- a/substrate/client/service/src/client/mod.rs +++ b/substrate/client/service/src/client/mod.rs @@ -49,6 +49,7 @@ pub mod light; mod call_executor; mod client; mod block_rules; +mod wasm_override; pub use self::{ call_executor::LocalCallExecutor, diff --git a/substrate/client/service/src/client/wasm_override.rs b/substrate/client/service/src/client/wasm_override.rs new file mode 100644 index 0000000000..1025b96338 --- /dev/null +++ b/substrate/client/service/src/client/wasm_override.rs @@ -0,0 +1,267 @@ +// This file is part of Substrate. + +// Copyright (C) 2020 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 . + +//! # WASM Local Blob-Override +//! +//! WASM Local blob override provides tools to replace on-chain WASM with custom WASM. +//! These customized WASM blobs may include functionality that is not included in the +//! on-chain WASM, such as tracing or debugging information. This extra information is especially +//! useful in external scenarios, like exchanges or archive nodes. +//! +//! ## Usage +//! +//! WASM overrides may be enabled with the `--wasm-runtime-overrides` argument. The argument +//! expects a path to a directory that holds custom WASM. +//! +//! Any file ending in '.wasm' will be scraped and instantiated as a WASM blob. WASM can be built by +//! compiling the required runtime with the changes needed. For example, compiling a runtime with +//! tracing enabled would produce a WASM blob that can used. +//! +//! A custom WASM blob will override on-chain WASM if the spec version matches. If it is +//! required to overrides multiple runtimes, multiple WASM blobs matching each of the spec versions +//! needed must be provided in the given directory. +//! +use std::{ + fs, collections::{HashMap, hash_map::DefaultHasher}, path::Path, + hash::Hasher as _, +}; +use sp_core::traits::FetchRuntimeCode; +use sp_state_machine::BasicExternalities; +use sp_blockchain::Result; +use sc_executor::RuntimeInfo; +use sp_version::RuntimeVersion; +use sp_core::traits::RuntimeCode; + +#[derive(Clone, Debug, PartialEq)] +/// Auxiliary structure that holds a wasm blob and its hash. +struct WasmBlob { + code: Vec, + hash: Vec, +} + +impl WasmBlob { + fn new(code: Vec) -> Self { + let hash = make_hash(&code); + Self { code, hash } + } + + fn runtime_code(&self, heap_pages: Option) -> RuntimeCode { + RuntimeCode { + code_fetcher: self, + hash: self.hash.clone(), + heap_pages, + } + } +} + +/// Make a hash out of a byte string using the default rust hasher +fn make_hash(val: &K) -> Vec { + let mut state = DefaultHasher::new(); + val.hash(&mut state); + state.finish().to_le_bytes().to_vec() +} + +impl FetchRuntimeCode for WasmBlob { + fn fetch_runtime_code<'a>(&'a self) -> Option> { + Some(self.code.as_slice().into()) + } +} + +/// Scrapes WASM from a folder and returns WASM from that folder +/// if the runtime spec version matches. +#[derive(Clone, Debug)] +pub struct WasmOverride { + // Map of runtime spec version -> Wasm Blob + overrides: HashMap, + executor: E, +} + +impl WasmOverride +where + E: RuntimeInfo + Clone + 'static +{ + pub fn new

(path: P, executor: E) -> Result + where + P: AsRef, + { + let overrides = Self::scrape_overrides(path.as_ref(), &executor)?; + Ok(Self { overrides, executor }) + } + + /// Gets an override by it's runtime spec version. + /// + /// Returns `None` if an override for a spec version does not exist. + pub fn get<'a, 'b: 'a>( + &'b self, + spec: &u32, + pages: Option, + ) -> Option> { + self.overrides + .get(spec) + .map(|w| w.runtime_code(pages)) + } + + /// Scrapes a folder for WASM runtimes. + /// Returns a hashmap of the runtime version and wasm runtime code. + fn scrape_overrides(dir: &Path, executor: &E) -> Result> { + let handle_err = |e: std::io::Error | -> sp_blockchain::Error { + sp_blockchain::Error::Msg(format!("{}", e.to_string())) + }; + + if !dir.is_dir() { + return Err(sp_blockchain::Error::Msg(format!( + "Overwriting WASM requires a directory where \ + local WASM is stored. {:?} is not a directory", + dir, + ))); + } + + let mut overrides = HashMap::new(); + let mut duplicates = Vec::new(); + for entry in fs::read_dir(dir).map_err(handle_err)? { + let entry = entry.map_err(handle_err)?; + let path = entry.path(); + match path.extension().map(|e| e.to_str()).flatten() { + Some("wasm") => { + let wasm = WasmBlob::new(fs::read(&path).map_err(handle_err)?); + let version = Self::runtime_version(executor, &wasm, Some(128))?; + if let Some(_duplicate) = overrides.insert(version.spec_version, wasm) { + duplicates.push(format!("{}", path.display())); + } + } + _ => () + } + } + + if !duplicates.is_empty() { + let duplicate_file_list = duplicates.join("\n"); + let msg = format!("Duplicate WASM Runtimes found: \n{}\n", duplicate_file_list); + return Err(sp_blockchain::Error::Msg(msg)); + } + + Ok(overrides) + } + + fn runtime_version( + executor: &E, + code: &WasmBlob, + heap_pages: Option, + ) -> Result { + let mut ext = BasicExternalities::default(); + executor.runtime_version(&mut ext, &code.runtime_code(heap_pages)) + .map_err(|e| sp_blockchain::Error::VersionInvalid(format!("{:?}", e)).into()) + } +} + +/// Returns a WasmOverride struct filled with dummy data for testing. +#[cfg(test)] +pub fn dummy_overrides(executor: &E) -> WasmOverride +where + E: RuntimeInfo + Clone + 'static +{ + let mut overrides = HashMap::new(); + overrides.insert(0, WasmBlob::new(vec![0, 0, 0, 0, 0, 0, 0, 0])); + overrides.insert(1, WasmBlob::new(vec![1, 1, 1, 1, 1, 1, 1, 1])); + overrides.insert(2, WasmBlob::new(vec![2, 2, 2, 2, 2, 2, 2, 2])); + WasmOverride { + overrides, + executor: executor.clone() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use sc_executor::{NativeExecutor, WasmExecutionMethod}; + use substrate_test_runtime_client::LocalExecutor; + use std::fs::{self, File}; + + fn wasm_test(fun: F) + where + F: Fn(&Path, &[u8], &NativeExecutor::) + { + let exec = NativeExecutor::::new( + WasmExecutionMethod::Interpreted, + Some(128), + 1, + ); + let bytes = substrate_test_runtime::wasm_binary_unwrap(); + let dir = tempfile::tempdir().expect("Create a temporary directory"); + fun(dir.path(), bytes, &exec); + dir.close().expect("Temporary Directory should close"); + } + + #[test] + fn should_get_runtime_version() { + let wasm = WasmBlob::new(substrate_test_runtime::wasm_binary_unwrap().to_vec()); + let executor = + NativeExecutor::::new(WasmExecutionMethod::Interpreted, Some(128), 1); + + let version = WasmOverride::runtime_version(&executor, &wasm, Some(128)) + .expect("should get the `RuntimeVersion` of the test-runtime wasm blob"); + assert_eq!(version.spec_version, 2); + } + + #[test] + fn should_scrape_wasm() { + wasm_test(|dir, wasm_bytes, exec| { + fs::write(dir.join("test.wasm"), wasm_bytes).expect("Create test file"); + let overrides = WasmOverride::scrape_overrides(dir, exec) + .expect("HashMap of u32 and WasmBlob"); + let wasm = overrides.get(&2).expect("WASM binary"); + assert_eq!(wasm.code, substrate_test_runtime::wasm_binary_unwrap().to_vec()) + }); + } + + #[test] + fn should_check_for_duplicates() { + wasm_test(|dir, wasm_bytes, exec| { + fs::write(dir.join("test0.wasm"), wasm_bytes).expect("Create test file"); + fs::write(dir.join("test1.wasm"), wasm_bytes).expect("Create test file"); + let scraped = WasmOverride::scrape_overrides(dir, exec); + + match scraped { + Err(e) => { + match e { + sp_blockchain::Error::Msg(msg) => { + let is_match = msg + .matches("Duplicate WASM Runtimes found") + .map(ToString::to_string) + .collect::>(); + assert!(is_match.len() >= 1) + }, + _ => panic!("Test should end with Msg Error Variant") + } + }, + _ => panic!("Test should end in error") + } + }); + } + + #[test] + fn should_ignore_non_wasm() { + wasm_test(|dir, wasm_bytes, exec| { + File::create(dir.join("README.md")).expect("Create test file"); + File::create(dir.join("LICENSE")).expect("Create a test file"); + fs::write(dir.join("test0.wasm"), wasm_bytes).expect("Create test file"); + let scraped = WasmOverride::scrape_overrides(dir, exec) + .expect("HashMap of u32 and WasmBlob"); + assert_eq!(scraped.len(), 1); + }); + } +} diff --git a/substrate/client/service/src/config.rs b/substrate/client/service/src/config.rs index 15783a87f9..0caf05b248 100644 --- a/substrate/client/service/src/config.rs +++ b/substrate/client/service/src/config.rs @@ -62,6 +62,10 @@ pub struct Configuration { pub chain_spec: Box, /// Wasm execution method. pub wasm_method: WasmExecutionMethod, + /// Directory where local WASM runtimes live. These runtimes take precedence + /// over on-chain runtimes when the spec version matches. Set to `None` to + /// disable overrides (default). + pub wasm_runtime_overrides: Option, /// Execution strategies. pub execution_strategies: ExecutionStrategies, /// RPC over HTTP binding address. `None` if disabled. diff --git a/substrate/client/service/test/src/lib.rs b/substrate/client/service/test/src/lib.rs index 6d8b4decb1..8a9f0ace17 100644 --- a/substrate/client/service/test/src/lib.rs +++ b/substrate/client/service/test/src/lib.rs @@ -252,6 +252,7 @@ fn node_config TestClientBuilder< executor, Box::new(sp_core::testing::TaskExecutor::new()), Default::default(), - ); + ).expect("Creates LocalCallExecutor"); self.build_with_executor(executor) } diff --git a/substrate/test-utils/runtime/client/src/lib.rs b/substrate/test-utils/runtime/client/src/lib.rs index 5b343f7748..9089be3ad4 100644 --- a/substrate/test-utils/runtime/client/src/lib.rs +++ b/substrate/test-utils/runtime/client/src/lib.rs @@ -355,7 +355,7 @@ pub fn new_light() -> ( executor, Box::new(sp_core::testing::TaskExecutor::new()), Default::default(), - ); + ).expect("Creates LocalCallExecutor"); let call_executor = LightExecutor::new( backend.clone(), local_call_executor, diff --git a/substrate/utils/browser/src/lib.rs b/substrate/utils/browser/src/lib.rs index f5d3faeb86..95ec7ca19c 100644 --- a/substrate/utils/browser/src/lib.rs +++ b/substrate/utils/browser/src/lib.rs @@ -98,6 +98,7 @@ where tracing_targets: Default::default(), transaction_pool: Default::default(), wasm_method: Default::default(), + wasm_runtime_overrides: Default::default(), max_runtime_instances: 8, announce_block: true, base_path: None,