feat: initialize Kurdistan SDK - independent fork of Polkadot SDK
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
[package]
|
||||
name = "sc-executor"
|
||||
version = "0.32.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
description = "A crate that provides means of executing/dispatching calls into the runtime."
|
||||
documentation = "https://docs.rs/sc-executor"
|
||||
readme = "README.md"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[[bench]]
|
||||
name = "bench"
|
||||
harness = false
|
||||
|
||||
[dependencies]
|
||||
parking_lot = { workspace = true, default-features = true }
|
||||
schnellru = { workspace = true }
|
||||
tracing = { workspace = true, default-features = true }
|
||||
|
||||
codec = { workspace = true, default-features = true }
|
||||
sc-executor-common = { workspace = true, default-features = true }
|
||||
sc-executor-polkavm = { workspace = true, default-features = true }
|
||||
sc-executor-wasmtime = { workspace = true, default-features = true }
|
||||
sp-api = { workspace = true, default-features = true }
|
||||
sp-core = { workspace = true, default-features = true }
|
||||
sp-externalities = { workspace = true, default-features = true }
|
||||
sp-io = { workspace = true, default-features = true }
|
||||
sp-panic-handler = { workspace = true, default-features = true }
|
||||
sp-runtime-interface = { workspace = true, default-features = true }
|
||||
sp-trie = { workspace = true, default-features = true }
|
||||
sp-version = { workspace = true, default-features = true }
|
||||
sp-wasm-interface = { workspace = true, default-features = true }
|
||||
|
||||
[dev-dependencies]
|
||||
array-bytes = { workspace = true, default-features = true }
|
||||
assert_matches = { workspace = true }
|
||||
criterion = { workspace = true, default-features = true }
|
||||
num_cpus = { workspace = true }
|
||||
paste = { workspace = true, default-features = true }
|
||||
sc-runtime-test = { workspace = true }
|
||||
sc-tracing = { workspace = true, default-features = true }
|
||||
sp-crypto-hashing = { workspace = true, default-features = true }
|
||||
sp-maybe-compressed-blob = { workspace = true, default-features = true }
|
||||
sp-runtime = { workspace = true, default-features = true }
|
||||
sp-state-machine = { workspace = true, default-features = true }
|
||||
sp-tracing = { workspace = true, default-features = true }
|
||||
substrate-test-runtime = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
wat = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
# This crate does not have `no_std` support, we just require this for tests
|
||||
std = [
|
||||
"sc-runtime-test/std",
|
||||
"sp-api/std",
|
||||
"sp-core/std",
|
||||
"sp-externalities/std",
|
||||
"sp-io/std",
|
||||
"sp-runtime-interface/std",
|
||||
"sp-runtime/std",
|
||||
"sp-state-machine/std",
|
||||
"sp-tracing/std",
|
||||
"sp-trie/std",
|
||||
"sp-version/std",
|
||||
"sp-wasm-interface/std",
|
||||
"substrate-test-runtime/std",
|
||||
]
|
||||
wasm-extern-trace = []
|
||||
runtime-benchmarks = [
|
||||
"sc-executor-wasmtime/runtime-benchmarks",
|
||||
"sc-runtime-test/runtime-benchmarks",
|
||||
"sc-tracing/runtime-benchmarks",
|
||||
"sp-api/runtime-benchmarks",
|
||||
"sp-io/runtime-benchmarks",
|
||||
"sp-runtime-interface/runtime-benchmarks",
|
||||
"sp-runtime/runtime-benchmarks",
|
||||
"sp-state-machine/runtime-benchmarks",
|
||||
"sp-trie/runtime-benchmarks",
|
||||
"sp-version/runtime-benchmarks",
|
||||
"substrate-test-runtime/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
A crate that provides means of executing/dispatching calls into the runtime.
|
||||
|
||||
There are a few responsibilities of this crate at the moment:
|
||||
|
||||
- It provides an implementation of a common entrypoint for calling into the runtime, both
|
||||
wasm and compiled.
|
||||
- It defines the environment for the wasm execution, namely the host functions that are to be
|
||||
provided into the wasm runtime module.
|
||||
- It also provides the required infrastructure for executing the current wasm runtime (specified
|
||||
by the current value of `:code` in the provided externalities), i.e. interfacing with
|
||||
wasm engine used, instance cache.
|
||||
|
||||
License: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
@@ -0,0 +1,253 @@
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
|
||||
use codec::Encode;
|
||||
|
||||
use sc_executor_common::{
|
||||
runtime_blob::RuntimeBlob,
|
||||
wasm_runtime::{WasmInstance, WasmModule, DEFAULT_HEAP_ALLOC_STRATEGY},
|
||||
};
|
||||
use sc_executor_wasmtime::InstantiationStrategy;
|
||||
use sc_runtime_test::wasm_binary_unwrap as test_runtime;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Method {
|
||||
Compiled { instantiation_strategy: InstantiationStrategy, precompile: bool },
|
||||
}
|
||||
|
||||
// This is just a bog-standard Kusama runtime with an extra
|
||||
// `test_empty_return` and `test_dirty_plenty_memory` functions
|
||||
// copy-pasted from the test runtime.
|
||||
fn kusama_runtime() -> &'static [u8] {
|
||||
include_bytes!("kusama_runtime.wasm")
|
||||
}
|
||||
|
||||
fn initialize(
|
||||
_tmpdir: &mut Option<tempfile::TempDir>,
|
||||
runtime: &[u8],
|
||||
method: Method,
|
||||
) -> Box<dyn WasmModule> {
|
||||
let blob = RuntimeBlob::uncompress_if_needed(runtime).unwrap();
|
||||
|
||||
let allow_missing_func_imports = true;
|
||||
|
||||
match method {
|
||||
Method::Compiled { instantiation_strategy, precompile } => {
|
||||
let config = sc_executor_wasmtime::Config {
|
||||
allow_missing_func_imports,
|
||||
cache_path: None,
|
||||
semantics: sc_executor_wasmtime::Semantics {
|
||||
heap_alloc_strategy: DEFAULT_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 precompile {
|
||||
let precompiled_blob =
|
||||
sc_executor_wasmtime::prepare_runtime_artifact(blob, &config.semantics)
|
||||
.unwrap();
|
||||
|
||||
// Create a fresh temporary directory to make absolutely sure
|
||||
// we'll use the right module.
|
||||
*_tmpdir = Some(tempfile::tempdir().unwrap());
|
||||
let tmpdir = _tmpdir.as_ref().unwrap();
|
||||
|
||||
let path = tmpdir.path().join("module.bin");
|
||||
std::fs::write(&path, &precompiled_blob).unwrap();
|
||||
unsafe {
|
||||
sc_executor_wasmtime::create_runtime_from_artifact::<
|
||||
sp_io::SubstrateHostFunctions,
|
||||
>(&path, config)
|
||||
}
|
||||
} else {
|
||||
sc_executor_wasmtime::create_runtime::<sp_io::SubstrateHostFunctions>(blob, config)
|
||||
}
|
||||
.map(|runtime| -> Box<dyn WasmModule> { Box::new(runtime) })
|
||||
},
|
||||
}
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn run_benchmark(
|
||||
c: &mut Criterion,
|
||||
benchmark_name: &str,
|
||||
thread_count: usize,
|
||||
runtime: &dyn WasmModule,
|
||||
testcase: impl Fn(&mut Box<dyn WasmInstance>) + Copy + Send + 'static,
|
||||
) {
|
||||
c.bench_function(benchmark_name, |b| {
|
||||
// Here we deliberately start a bunch of extra threads which will just
|
||||
// keep on independently instantiating the runtime over and over again.
|
||||
//
|
||||
// We don't really have to measure how much time those take since the
|
||||
// work done is essentially the same on each thread, and what we're
|
||||
// interested in here is only how those extra threads affect the execution
|
||||
// on the current thread.
|
||||
//
|
||||
// In an ideal case assuming we have enough CPU cores those extra threads
|
||||
// shouldn't affect the main thread's runtime at all, however in practice
|
||||
// they're not completely independent. There might be per-process
|
||||
// locks in the kernel which are briefly held during instantiation, etc.,
|
||||
// and how much those affect the execution here is what we want to measure.
|
||||
let is_benchmark_running = Arc::new(AtomicBool::new(true));
|
||||
let threads_running = Arc::new(AtomicUsize::new(0));
|
||||
let aux_threads: Vec<_> = (0..thread_count - 1)
|
||||
.map(|_| {
|
||||
let mut instance = runtime.new_instance().unwrap();
|
||||
let is_benchmark_running = is_benchmark_running.clone();
|
||||
let threads_running = threads_running.clone();
|
||||
std::thread::spawn(move || {
|
||||
threads_running.fetch_add(1, Ordering::SeqCst);
|
||||
while is_benchmark_running.load(Ordering::Relaxed) {
|
||||
testcase(&mut instance);
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
while threads_running.load(Ordering::SeqCst) != (thread_count - 1) {
|
||||
std::thread::yield_now();
|
||||
}
|
||||
|
||||
let mut instance = runtime.new_instance().unwrap();
|
||||
b.iter(|| testcase(&mut instance));
|
||||
|
||||
is_benchmark_running.store(false, Ordering::SeqCst);
|
||||
for thread in aux_threads {
|
||||
thread.join().unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_call_instance(c: &mut Criterion) {
|
||||
sp_tracing::try_init_simple();
|
||||
|
||||
let strategies = [
|
||||
(
|
||||
"recreate_instance_vanilla",
|
||||
Method::Compiled {
|
||||
instantiation_strategy: InstantiationStrategy::RecreateInstance,
|
||||
precompile: false,
|
||||
},
|
||||
),
|
||||
(
|
||||
"recreate_instance_cow_fresh",
|
||||
Method::Compiled {
|
||||
instantiation_strategy: InstantiationStrategy::RecreateInstanceCopyOnWrite,
|
||||
precompile: false,
|
||||
},
|
||||
),
|
||||
(
|
||||
"recreate_instance_cow_precompiled",
|
||||
Method::Compiled {
|
||||
instantiation_strategy: InstantiationStrategy::RecreateInstanceCopyOnWrite,
|
||||
precompile: true,
|
||||
},
|
||||
),
|
||||
(
|
||||
"pooling_vanilla_fresh",
|
||||
Method::Compiled {
|
||||
instantiation_strategy: InstantiationStrategy::Pooling,
|
||||
precompile: false,
|
||||
},
|
||||
),
|
||||
(
|
||||
"pooling_vanilla_precompiled",
|
||||
Method::Compiled {
|
||||
instantiation_strategy: InstantiationStrategy::Pooling,
|
||||
precompile: true,
|
||||
},
|
||||
),
|
||||
(
|
||||
"pooling_cow_fresh",
|
||||
Method::Compiled {
|
||||
instantiation_strategy: InstantiationStrategy::PoolingCopyOnWrite,
|
||||
precompile: false,
|
||||
},
|
||||
),
|
||||
(
|
||||
"pooling_cow_precompiled",
|
||||
Method::Compiled {
|
||||
instantiation_strategy: InstantiationStrategy::PoolingCopyOnWrite,
|
||||
precompile: true,
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
let runtimes = [("kusama_runtime", kusama_runtime()), ("test_runtime", test_runtime())];
|
||||
|
||||
let thread_counts = [1, 2, 4, 8, 16];
|
||||
|
||||
fn test_call_empty_function(instance: &mut Box<dyn WasmInstance>) {
|
||||
instance.call_export("test_empty_return", &[0]).unwrap();
|
||||
}
|
||||
|
||||
fn test_dirty_1mb_of_memory(instance: &mut Box<dyn WasmInstance>) {
|
||||
instance.call_export("test_dirty_plenty_memory", &(0, 16).encode()).unwrap();
|
||||
}
|
||||
|
||||
let testcases = [
|
||||
("call_empty_function", test_call_empty_function as fn(&mut Box<dyn WasmInstance>)),
|
||||
("dirty_1mb_of_memory", test_dirty_1mb_of_memory),
|
||||
];
|
||||
|
||||
let num_cpus = num_cpus::get_physical();
|
||||
let mut tmpdir = None;
|
||||
|
||||
for (strategy_name, strategy) in strategies {
|
||||
for (runtime_name, runtime) in runtimes {
|
||||
let runtime = initialize(&mut tmpdir, runtime, strategy.clone());
|
||||
|
||||
for (testcase_name, testcase) in testcases {
|
||||
for thread_count in thread_counts {
|
||||
if thread_count > num_cpus {
|
||||
// If there are not enough cores available the benchmark is pointless.
|
||||
continue;
|
||||
}
|
||||
|
||||
let benchmark_name = format!(
|
||||
"{}_from_{}_with_{}_on_{}_threads",
|
||||
testcase_name, runtime_name, strategy_name, thread_count
|
||||
);
|
||||
|
||||
run_benchmark(c, &benchmark_name, thread_count, &*runtime, testcase);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = benches;
|
||||
config = Criterion::default();
|
||||
targets = bench_call_instance
|
||||
}
|
||||
criterion_main!(benches);
|
||||
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "sc-executor-common"
|
||||
version = "0.29.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
description = "A set of common definitions that are needed for defining execution engines."
|
||||
documentation = "https://docs.rs/sc-executor-common/"
|
||||
readme = "README.md"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
polkavm = { workspace = true }
|
||||
sc-allocator = { workspace = true, default-features = true }
|
||||
sp-maybe-compressed-blob = { workspace = true, default-features = true }
|
||||
sp-wasm-interface = { workspace = true, default-features = true }
|
||||
thiserror = { workspace = true }
|
||||
wasm-instrument = { workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
@@ -0,0 +1,3 @@
|
||||
A set of common definitions that are needed for defining execution engines.
|
||||
|
||||
License: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
@@ -0,0 +1,204 @@
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Rust executor possible errors.
|
||||
|
||||
/// Result type alias.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Error type.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum Error {
|
||||
#[error("Error calling api function: {0}")]
|
||||
ApiError(Box<dyn std::error::Error + Send + Sync>),
|
||||
|
||||
#[error("Method not found: '{0}'")]
|
||||
MethodNotFound(String),
|
||||
|
||||
#[error("On-chain runtime does not specify version")]
|
||||
VersionInvalid,
|
||||
|
||||
#[error("Externalities error")]
|
||||
Externalities,
|
||||
|
||||
#[error("Invalid index provided")]
|
||||
InvalidIndex,
|
||||
|
||||
#[error("Invalid type returned (should be u64)")]
|
||||
InvalidReturn,
|
||||
|
||||
#[error("Runtime panicked: {0}")]
|
||||
RuntimePanicked(String),
|
||||
|
||||
#[error("Invalid memory reference")]
|
||||
InvalidMemoryReference,
|
||||
|
||||
#[error("The runtime doesn't provide a global named `__heap_base` of type `i32`")]
|
||||
HeapBaseNotFoundOrInvalid,
|
||||
|
||||
#[error("The runtime must not have the `start` function defined")]
|
||||
RuntimeHasStartFn,
|
||||
|
||||
#[error("Other: {0}")]
|
||||
Other(String),
|
||||
|
||||
#[error(transparent)]
|
||||
Allocator(#[from] sc_allocator::Error),
|
||||
|
||||
#[error("Host function {0} execution failed with: {1}")]
|
||||
FunctionExecution(String, String),
|
||||
|
||||
#[error("No table exported by wasm blob")]
|
||||
NoTable,
|
||||
|
||||
#[error("No table entry with index {0} in wasm blob exported table")]
|
||||
NoTableEntryWithIndex(u32),
|
||||
|
||||
#[error("Table element with index {0} is not a function in wasm blob exported table")]
|
||||
TableElementIsNotAFunction(u32),
|
||||
|
||||
#[error("Table entry with index {0} in wasm blob is null")]
|
||||
FunctionRefIsNull(u32),
|
||||
|
||||
#[error(transparent)]
|
||||
RuntimeConstruction(#[from] WasmError),
|
||||
|
||||
#[error("Shared memory is not supported")]
|
||||
SharedMemUnsupported,
|
||||
|
||||
#[error("Imported globals are not supported yet")]
|
||||
ImportedGlobalsUnsupported,
|
||||
|
||||
#[error("initializer expression can have only up to 2 expressions in wasm 1.0")]
|
||||
InitializerHasTooManyExpressions,
|
||||
|
||||
#[error("Invalid initializer expression provided {0}")]
|
||||
InvalidInitializerExpression(String),
|
||||
|
||||
#[error("Execution aborted due to panic: {0}")]
|
||||
AbortedDueToPanic(MessageWithBacktrace),
|
||||
|
||||
#[error("Execution aborted due to trap: {0}")]
|
||||
AbortedDueToTrap(MessageWithBacktrace),
|
||||
|
||||
#[error("Output exceeds bounds of wasm memory")]
|
||||
OutputExceedsBounds,
|
||||
}
|
||||
|
||||
impl From<&'static str> for Error {
|
||||
fn from(err: &'static str) -> Error {
|
||||
Error::Other(err.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Error {
|
||||
fn from(err: String) -> Error {
|
||||
Error::Other(err)
|
||||
}
|
||||
}
|
||||
|
||||
/// Type for errors occurring during Wasm runtime construction.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum WasmError {
|
||||
#[error("Code could not be read from the state.")]
|
||||
CodeNotFound,
|
||||
|
||||
#[error("Failure to reinitialize runtime instance from snapshot.")]
|
||||
ApplySnapshotFailed,
|
||||
|
||||
/// Failure to erase the wasm memory.
|
||||
///
|
||||
/// Depending on the implementation might mean failure of allocating memory.
|
||||
#[error("Failure to erase the wasm memory: {0}")]
|
||||
ErasingFailed(String),
|
||||
|
||||
#[error("Wasm code failed validation.")]
|
||||
InvalidModule,
|
||||
|
||||
#[error("Wasm code could not be deserialized.")]
|
||||
CantDeserializeWasm,
|
||||
|
||||
#[error("The module does not export a linear memory named `memory`.")]
|
||||
InvalidMemory,
|
||||
|
||||
#[error("The number of heap pages requested is disallowed by the module.")]
|
||||
InvalidHeapPages,
|
||||
|
||||
/// Instantiation error.
|
||||
#[error("{0}")]
|
||||
Instantiation(String),
|
||||
|
||||
/// Other error happened.
|
||||
#[error("Other error happened while constructing the runtime: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl From<polkavm::program::ProgramParseError> for WasmError {
|
||||
fn from(error: polkavm::program::ProgramParseError) -> Self {
|
||||
WasmError::Other(error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<polkavm::Error> for WasmError {
|
||||
fn from(error: polkavm::Error) -> Self {
|
||||
WasmError::Other(error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<polkavm::Error> for Error {
|
||||
fn from(error: polkavm::Error) -> Self {
|
||||
Error::Other(error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// An error message with an attached backtrace.
|
||||
#[derive(Debug)]
|
||||
pub struct MessageWithBacktrace {
|
||||
/// The error message.
|
||||
pub message: String,
|
||||
|
||||
/// The backtrace associated with the error message.
|
||||
pub backtrace: Option<Backtrace>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for MessageWithBacktrace {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
fmt.write_str(&self.message)?;
|
||||
if let Some(ref backtrace) = self.backtrace {
|
||||
fmt.write_str("\nWASM backtrace:\n")?;
|
||||
backtrace.backtrace_string.fmt(fmt)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A WASM backtrace.
|
||||
#[derive(Debug)]
|
||||
pub struct Backtrace {
|
||||
/// The string containing the backtrace.
|
||||
pub backtrace_string: String,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Backtrace {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
fmt.write_str(&self.backtrace_string)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! A set of common definitions that are needed for defining execution engines.
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unused_crate_dependencies)]
|
||||
|
||||
pub mod error;
|
||||
pub mod runtime_blob;
|
||||
pub mod util;
|
||||
pub mod wasm_runtime;
|
||||
|
||||
pub(crate) fn is_polkavm_enabled() -> bool {
|
||||
std::env::var_os("SUBSTRATE_ENABLE_POLKAVM").map_or(false, |value| value == "1")
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! This module allows for inspection and instrumentation, i.e. modifying the module to alter it's
|
||||
//! structure or behavior, of a wasm module.
|
||||
//!
|
||||
//! ## Instrumentation
|
||||
//!
|
||||
//! In ideal world, there would be no instrumentation. However, in the real world the execution
|
||||
//! engines we use are somewhat limited in their APIs or abilities.
|
||||
//!
|
||||
//! To give you some examples:
|
||||
//!
|
||||
//! We need to reset the globals because when we
|
||||
//! execute the Substrate Runtime, we do not drop and create the instance anew, instead
|
||||
//! we restore some selected parts of the state.
|
||||
//!
|
||||
//! - stack depth metering can be performed via instrumentation or deferred to the engine and say be
|
||||
//! added directly in machine code. Implementing this in machine code is rather cumbersome so
|
||||
//! instrumentation looks like a good solution.
|
||||
//!
|
||||
//! Stack depth metering is needed to make a wasm blob
|
||||
//! execution deterministic, which in turn is needed by the Teyrchain Validation Function in
|
||||
//! Pezkuwi.
|
||||
//!
|
||||
//! ## Inspection
|
||||
//!
|
||||
//! Inspection of a wasm module may be needed to extract some useful information, such as to extract
|
||||
//! data segment snapshot, which is helpful for quickly restoring the initial state of instances.
|
||||
//! Inspection can be also useful to prove that a wasm module possesses some properties, such as,
|
||||
//! is free of any floating point operations, which is a useful step towards making instances
|
||||
//! produced from such a module deterministic.
|
||||
|
||||
mod runtime_blob;
|
||||
|
||||
pub use runtime_blob::RuntimeBlob;
|
||||
@@ -0,0 +1,234 @@
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::{error::WasmError, wasm_runtime::HeapAllocStrategy};
|
||||
use polkavm::ArcBytes;
|
||||
use wasm_instrument::parity_wasm::elements::{
|
||||
deserialize_buffer, serialize, ExportEntry, External, Internal, MemorySection, MemoryType,
|
||||
Module, Section,
|
||||
};
|
||||
|
||||
/// A program blob containing a Substrate runtime.
|
||||
#[derive(Clone)]
|
||||
pub struct RuntimeBlob(BlobKind);
|
||||
|
||||
#[derive(Clone)]
|
||||
enum BlobKind {
|
||||
WebAssembly(Module),
|
||||
PolkaVM((polkavm::ProgramBlob, ArcBytes)),
|
||||
}
|
||||
|
||||
impl RuntimeBlob {
|
||||
/// Create `RuntimeBlob` from the given WASM or PolkaVM compressed program blob.
|
||||
///
|
||||
/// See [`sp_maybe_compressed_blob`] for details about decompression.
|
||||
pub fn uncompress_if_needed(wasm_code: &[u8]) -> Result<Self, WasmError> {
|
||||
use sp_maybe_compressed_blob::CODE_BLOB_BOMB_LIMIT;
|
||||
let wasm_code = sp_maybe_compressed_blob::decompress(wasm_code, CODE_BLOB_BOMB_LIMIT)
|
||||
.map_err(|e| WasmError::Other(format!("Decompression error: {:?}", e)))?;
|
||||
Self::new(&wasm_code)
|
||||
}
|
||||
|
||||
/// Create `RuntimeBlob` from the given WASM or PolkaVM program blob.
|
||||
///
|
||||
/// Returns `Err` if the blob cannot be deserialized.
|
||||
///
|
||||
/// Will only accept a PolkaVM program if the `SUBSTRATE_ENABLE_POLKAVM` environment
|
||||
/// variable is set to `1`.
|
||||
pub fn new(raw_blob: &[u8]) -> Result<Self, WasmError> {
|
||||
if raw_blob.starts_with(b"PVM\0") {
|
||||
if crate::is_polkavm_enabled() {
|
||||
let raw = ArcBytes::from(raw_blob);
|
||||
let blob = polkavm::ProgramBlob::parse(raw.clone())?;
|
||||
return Ok(Self(BlobKind::PolkaVM((blob, raw))));
|
||||
} else {
|
||||
return Err(WasmError::Other("expected a WASM runtime blob, found a PolkaVM runtime blob; set the 'SUBSTRATE_ENABLE_POLKAVM' environment variable to enable the experimental PolkaVM-based executor".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
let raw_module: Module = deserialize_buffer(raw_blob)
|
||||
.map_err(|e| WasmError::Other(format!("cannot deserialize module: {:?}", e)))?;
|
||||
Ok(Self(BlobKind::WebAssembly(raw_module)))
|
||||
}
|
||||
|
||||
/// Run a pass that instrument this module so as to introduce a deterministic stack height
|
||||
/// limit.
|
||||
///
|
||||
/// It will introduce a global mutable counter. The instrumentation will increase the counter
|
||||
/// according to the "cost" of the callee. If the cost exceeds the `stack_depth_limit` constant,
|
||||
/// the instrumentation will trap. The counter will be decreased as soon as the the callee
|
||||
/// returns.
|
||||
///
|
||||
/// The stack cost of a function is computed based on how much locals there are and the maximum
|
||||
/// depth of the wasm operand stack.
|
||||
///
|
||||
/// Only valid for WASM programs; will return an error if the blob is a PolkaVM program.
|
||||
pub fn inject_stack_depth_metering(self, stack_depth_limit: u32) -> Result<Self, WasmError> {
|
||||
let injected_module =
|
||||
wasm_instrument::inject_stack_limiter(self.into_webassembly_blob()?, stack_depth_limit)
|
||||
.map_err(|e| {
|
||||
WasmError::Other(format!("cannot inject the stack limiter: {:?}", e))
|
||||
})?;
|
||||
|
||||
Ok(Self(BlobKind::WebAssembly(injected_module)))
|
||||
}
|
||||
|
||||
/// Converts a WASM memory import into a memory section and exports it.
|
||||
///
|
||||
/// Does nothing if there's no memory import.
|
||||
///
|
||||
/// May return an error in case the WASM module is invalid.
|
||||
///
|
||||
/// Only valid for WASM programs; will return an error if the blob is a PolkaVM program.
|
||||
pub fn convert_memory_import_into_export(&mut self) -> Result<(), WasmError> {
|
||||
let raw_module = self.as_webassembly_blob_mut()?;
|
||||
let import_section = match raw_module.import_section_mut() {
|
||||
Some(import_section) => import_section,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let import_entries = import_section.entries_mut();
|
||||
for index in 0..import_entries.len() {
|
||||
let entry = &import_entries[index];
|
||||
let memory_ty = match entry.external() {
|
||||
External::Memory(memory_ty) => *memory_ty,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
let memory_name = entry.field().to_owned();
|
||||
import_entries.remove(index);
|
||||
|
||||
raw_module
|
||||
.insert_section(Section::Memory(MemorySection::with_entries(vec![memory_ty])))
|
||||
.map_err(|error| {
|
||||
WasmError::Other(format!(
|
||||
"can't convert a memory import into an export: failed to insert a new memory section: {}",
|
||||
error
|
||||
))
|
||||
})?;
|
||||
|
||||
if raw_module.export_section_mut().is_none() {
|
||||
// A module without an export section is somewhat unrealistic, but let's do this
|
||||
// just in case to cover all of our bases.
|
||||
raw_module
|
||||
.insert_section(Section::Export(Default::default()))
|
||||
.expect("an export section can be always inserted if it doesn't exist; qed");
|
||||
}
|
||||
raw_module
|
||||
.export_section_mut()
|
||||
.expect("export section already existed or we just added it above, so it always exists; qed")
|
||||
.entries_mut()
|
||||
.push(ExportEntry::new(memory_name, Internal::Memory(0)));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Modifies the blob's memory section according to the given `heap_alloc_strategy`.
|
||||
///
|
||||
/// Will return an error in case there is no memory section present,
|
||||
/// or if the memory section is empty.
|
||||
///
|
||||
/// Only valid for WASM programs; will return an error if the blob is a PolkaVM program.
|
||||
pub fn setup_memory_according_to_heap_alloc_strategy(
|
||||
&mut self,
|
||||
heap_alloc_strategy: HeapAllocStrategy,
|
||||
) -> Result<(), WasmError> {
|
||||
let raw_module = self.as_webassembly_blob_mut()?;
|
||||
let memory_section = raw_module
|
||||
.memory_section_mut()
|
||||
.ok_or_else(|| WasmError::Other("no memory section found".into()))?;
|
||||
|
||||
if memory_section.entries().is_empty() {
|
||||
return Err(WasmError::Other("memory section is empty".into()));
|
||||
}
|
||||
for memory_ty in memory_section.entries_mut() {
|
||||
let initial = memory_ty.limits().initial();
|
||||
let (min, max) = match heap_alloc_strategy {
|
||||
HeapAllocStrategy::Dynamic { maximum_pages } => {
|
||||
// Ensure `initial <= maximum_pages`
|
||||
(maximum_pages.map(|m| m.min(initial)).unwrap_or(initial), maximum_pages)
|
||||
},
|
||||
HeapAllocStrategy::Static { extra_pages } => {
|
||||
let pages = initial.saturating_add(extra_pages);
|
||||
(pages, Some(pages))
|
||||
},
|
||||
};
|
||||
*memory_ty = MemoryType::new(min, max);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Scans the wasm blob for the first section with the name that matches the given. Returns the
|
||||
/// contents of the custom section if found or `None` otherwise.
|
||||
///
|
||||
/// Only valid for WASM programs; will return an error if the blob is a PolkaVM program.
|
||||
pub fn custom_section_contents(&self, section_name: &str) -> Option<&[u8]> {
|
||||
self.as_webassembly_blob()
|
||||
.ok()?
|
||||
.custom_sections()
|
||||
.find(|cs| cs.name() == section_name)
|
||||
.map(|cs| cs.payload())
|
||||
}
|
||||
|
||||
/// Consumes this runtime blob and serializes it.
|
||||
pub fn serialize(self) -> Vec<u8> {
|
||||
match self.0 {
|
||||
BlobKind::WebAssembly(raw_module) =>
|
||||
serialize(raw_module).expect("serializing into a vec should succeed; qed"),
|
||||
BlobKind::PolkaVM(ref blob) => blob.1.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
fn as_webassembly_blob(&self) -> Result<&Module, WasmError> {
|
||||
match self.0 {
|
||||
BlobKind::WebAssembly(ref raw_module) => Ok(raw_module),
|
||||
BlobKind::PolkaVM(..) => Err(WasmError::Other(
|
||||
"expected a WebAssembly program; found a PolkaVM program blob".into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn as_webassembly_blob_mut(&mut self) -> Result<&mut Module, WasmError> {
|
||||
match self.0 {
|
||||
BlobKind::WebAssembly(ref mut raw_module) => Ok(raw_module),
|
||||
BlobKind::PolkaVM(..) => Err(WasmError::Other(
|
||||
"expected a WebAssembly program; found a PolkaVM program blob".into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_webassembly_blob(self) -> Result<Module, WasmError> {
|
||||
match self.0 {
|
||||
BlobKind::WebAssembly(raw_module) => Ok(raw_module),
|
||||
BlobKind::PolkaVM(..) => Err(WasmError::Other(
|
||||
"expected a WebAssembly program; found a PolkaVM program blob".into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a reference to the inner PolkaVM program blob, if this is a PolkaVM program.
|
||||
pub fn as_polkavm_blob(&self) -> Option<&polkavm::ProgramBlob> {
|
||||
match self.0 {
|
||||
BlobKind::WebAssembly(..) => None,
|
||||
BlobKind::PolkaVM((ref blob, _)) => Some(blob),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Utilities used by all backends
|
||||
|
||||
use crate::error::Result;
|
||||
use sp_wasm_interface::Pointer;
|
||||
use std::ops::Range;
|
||||
|
||||
/// Construct a range from an offset to a data length after the offset.
|
||||
/// Returns None if the end of the range would exceed some maximum offset.
|
||||
pub fn checked_range(offset: usize, len: usize, max: usize) -> Option<Range<usize>> {
|
||||
let end = offset.checked_add(len)?;
|
||||
(end <= max).then(|| offset..end)
|
||||
}
|
||||
|
||||
/// Provides safe memory access interface using an external buffer
|
||||
pub trait MemoryTransfer {
|
||||
/// Read data from a slice of memory into a newly allocated buffer.
|
||||
///
|
||||
/// Returns an error if the read would go out of the memory bounds.
|
||||
fn read(&self, source_addr: Pointer<u8>, size: usize) -> Result<Vec<u8>>;
|
||||
|
||||
/// Read data from a slice of memory into a destination buffer.
|
||||
///
|
||||
/// Returns an error if the read would go out of the memory bounds.
|
||||
fn read_into(&self, source_addr: Pointer<u8>, destination: &mut [u8]) -> Result<()>;
|
||||
|
||||
/// Write data to a slice of memory.
|
||||
///
|
||||
/// Returns an error if the write would go out of the memory bounds.
|
||||
fn write_from(&self, dest_addr: Pointer<u8>, source: &[u8]) -> Result<()>;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Definitions for a wasm runtime.
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
pub use sc_allocator::AllocationStats;
|
||||
|
||||
/// Default heap allocation strategy.
|
||||
pub const DEFAULT_HEAP_ALLOC_STRATEGY: HeapAllocStrategy =
|
||||
HeapAllocStrategy::Static { extra_pages: DEFAULT_HEAP_ALLOC_PAGES };
|
||||
|
||||
/// Default heap allocation pages.
|
||||
pub const DEFAULT_HEAP_ALLOC_PAGES: u32 = 2048;
|
||||
|
||||
/// A trait that defines an abstract WASM runtime module.
|
||||
///
|
||||
/// This can be implemented by an execution engine.
|
||||
pub trait WasmModule: Sync + Send {
|
||||
/// Create a new instance.
|
||||
fn new_instance(&self) -> Result<Box<dyn WasmInstance>, Error>;
|
||||
}
|
||||
|
||||
/// A trait that defines an abstract wasm module instance.
|
||||
///
|
||||
/// This can be implemented by an execution engine.
|
||||
pub trait WasmInstance: Send {
|
||||
/// Call a method on this WASM instance.
|
||||
///
|
||||
/// Before execution, instance is reset.
|
||||
///
|
||||
/// Returns the encoded result on success.
|
||||
fn call(&mut self, method: &str, data: &[u8]) -> Result<Vec<u8>, Error> {
|
||||
self.call_with_allocation_stats(method, data).0
|
||||
}
|
||||
|
||||
/// Call a method on this WASM instance.
|
||||
///
|
||||
/// Before execution, instance is reset.
|
||||
///
|
||||
/// Returns the encoded result on success.
|
||||
fn call_with_allocation_stats(
|
||||
&mut self,
|
||||
method: &str,
|
||||
data: &[u8],
|
||||
) -> (Result<Vec<u8>, Error>, Option<AllocationStats>);
|
||||
|
||||
/// Call an exported method on this WASM instance.
|
||||
///
|
||||
/// Before execution, instance is reset.
|
||||
///
|
||||
/// Returns the encoded result on success.
|
||||
fn call_export(&mut self, method: &str, data: &[u8]) -> Result<Vec<u8>, Error> {
|
||||
self.call(method.into(), data)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
pub enum HeapAllocStrategy {
|
||||
/// Allocate a static number of heap pages.
|
||||
///
|
||||
/// The total number of allocated heap pages is the initial number of heap pages requested by
|
||||
/// the wasm file plus the `extra_pages`.
|
||||
Static {
|
||||
/// The number of pages that will be added on top of the initial heap pages requested by
|
||||
/// the wasm file.
|
||||
extra_pages: u32,
|
||||
},
|
||||
/// Allocate the initial heap pages as requested by the wasm file and then allow it to grow
|
||||
/// dynamically.
|
||||
Dynamic {
|
||||
/// The absolute maximum size of the linear memory (in pages).
|
||||
///
|
||||
/// When `Some(_)` the linear memory will be allowed to grow up to this limit.
|
||||
/// When `None` the linear memory will be allowed to grow up to the maximum limit supported
|
||||
/// by WASM (4GB).
|
||||
maximum_pages: Option<u32>,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "sc-executor-polkavm"
|
||||
version = "0.29.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
description = "PolkaVM executor for Substrate"
|
||||
readme = "README.md"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
log = { workspace = true }
|
||||
polkavm = { workspace = true }
|
||||
|
||||
sc-executor-common = { workspace = true, default-features = true }
|
||||
sp-wasm-interface = { workspace = true, default-features = true }
|
||||
@@ -0,0 +1 @@
|
||||
License: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
@@ -0,0 +1,294 @@
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
use polkavm::{CallError, Caller, Reg};
|
||||
use sc_executor_common::{
|
||||
error::{Error, WasmError},
|
||||
wasm_runtime::{AllocationStats, WasmInstance, WasmModule},
|
||||
};
|
||||
use sp_wasm_interface::{
|
||||
Function, FunctionContext, HostFunctions, Pointer, Value, ValueType, WordSize,
|
||||
};
|
||||
|
||||
#[repr(transparent)]
|
||||
pub struct InstancePre(polkavm::InstancePre<(), String>);
|
||||
|
||||
#[repr(transparent)]
|
||||
pub struct Instance(polkavm::Instance<(), String>);
|
||||
|
||||
impl WasmModule for InstancePre {
|
||||
fn new_instance(&self) -> Result<Box<dyn WasmInstance>, Error> {
|
||||
Ok(Box::new(Instance(self.0.instantiate()?)))
|
||||
}
|
||||
}
|
||||
|
||||
impl WasmInstance for Instance {
|
||||
fn call_with_allocation_stats(
|
||||
&mut self,
|
||||
name: &str,
|
||||
raw_data: &[u8],
|
||||
) -> (Result<Vec<u8>, Error>, Option<AllocationStats>) {
|
||||
let pc = match self.0.module().exports().find(|e| e.symbol() == name) {
|
||||
Some(export) => export.program_counter(),
|
||||
None =>
|
||||
return (
|
||||
Err(format!("cannot call into the runtime: export not found: '{name}'").into()),
|
||||
None,
|
||||
),
|
||||
};
|
||||
|
||||
let Ok(raw_data_length) = u32::try_from(raw_data.len()) else {
|
||||
return (
|
||||
Err(format!("cannot call runtime method '{name}': input payload is too big").into()),
|
||||
None,
|
||||
);
|
||||
};
|
||||
|
||||
// TODO: This will leak guest memory; find a better solution.
|
||||
|
||||
// Make sure that the memory is cleared...
|
||||
if let Err(err) = self.0.reset_memory() {
|
||||
return (
|
||||
Err(format!(
|
||||
"call into the runtime method '{name}' failed: reset memory failed: {err}"
|
||||
)
|
||||
.into()),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
// ... and allocate space for the input payload.
|
||||
if let Err(err) = self.0.sbrk(raw_data_length) {
|
||||
return (
|
||||
Err(format!(
|
||||
"call into the runtime method '{name}' failed: reset memory failed: {err}"
|
||||
)
|
||||
.into()),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
// Grab the address of where the guest's heap starts; that's where we've just allocated
|
||||
// the memory for the input payload.
|
||||
let data_pointer = self.0.module().memory_map().heap_base();
|
||||
|
||||
if let Err(err) = self.0.write_memory(data_pointer, raw_data) {
|
||||
return (Err(format!("call into the runtime method '{name}': failed to write the input payload into guest memory: {err}").into()), None);
|
||||
}
|
||||
|
||||
match self.0.call_typed(&mut (), pc, (data_pointer, raw_data_length)) {
|
||||
Ok(()) => {},
|
||||
Err(CallError::Trap) =>
|
||||
return (
|
||||
Err(format!("call into the runtime method '{name}' failed: trap").into()),
|
||||
None,
|
||||
),
|
||||
Err(CallError::Error(err)) =>
|
||||
return (
|
||||
Err(format!("call into the runtime method '{name}' failed: {err}").into()),
|
||||
None,
|
||||
),
|
||||
Err(CallError::User(err)) =>
|
||||
return (
|
||||
Err(format!("call into the runtime method '{name}' failed: {err}").into()),
|
||||
None,
|
||||
),
|
||||
Err(CallError::NotEnoughGas) => unreachable!("gas metering is never enabled"),
|
||||
Err(CallError::Step) => unreachable!("stepping is never enabled"),
|
||||
};
|
||||
|
||||
let result_pointer = self.0.reg(Reg::A0);
|
||||
let result_length = self.0.reg(Reg::A1);
|
||||
let output = match self.0.read_memory(result_pointer as u32, result_length as u32) {
|
||||
Ok(output) => output,
|
||||
Err(error) => {
|
||||
return (Err(format!("call into the runtime method '{name}' failed: failed to read the return payload: {error}").into()), None)
|
||||
},
|
||||
};
|
||||
|
||||
(Ok(output), None)
|
||||
}
|
||||
}
|
||||
|
||||
struct Context<'r, 'a>(&'r mut polkavm::Caller<'a, ()>);
|
||||
|
||||
impl<'r, 'a> FunctionContext for Context<'r, 'a> {
|
||||
fn read_memory_into(
|
||||
&self,
|
||||
address: Pointer<u8>,
|
||||
dest: &mut [u8],
|
||||
) -> sp_wasm_interface::Result<()> {
|
||||
self.0
|
||||
.instance
|
||||
.read_memory_into(u32::from(address), dest)
|
||||
.map_err(|error| error.to_string())
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
fn write_memory(&mut self, address: Pointer<u8>, data: &[u8]) -> sp_wasm_interface::Result<()> {
|
||||
self.0
|
||||
.instance
|
||||
.write_memory(u32::from(address), data)
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
fn allocate_memory(&mut self, size: WordSize) -> sp_wasm_interface::Result<Pointer<u8>> {
|
||||
let pointer = match self.0.instance.sbrk(0) {
|
||||
Ok(pointer) => pointer.expect("fetching the current heap pointer never fails"),
|
||||
Err(err) => return Err(format!("sbrk failed: {err}")),
|
||||
};
|
||||
|
||||
// TODO: This will leak guest memory; find a better solution.
|
||||
match self.0.instance.sbrk(size) {
|
||||
Ok(Some(_)) => (),
|
||||
Ok(None) => return Err(String::from("allocation error")),
|
||||
Err(err) => return Err(format!("sbrk failed: {err}")),
|
||||
}
|
||||
|
||||
Ok(Pointer::new(pointer))
|
||||
}
|
||||
|
||||
fn deallocate_memory(&mut self, _ptr: Pointer<u8>) -> sp_wasm_interface::Result<()> {
|
||||
// This is only used by the allocator host function, which is unused under PolkaVM.
|
||||
unimplemented!("'deallocate_memory' is never used when running under PolkaVM");
|
||||
}
|
||||
|
||||
fn register_panic_error_message(&mut self, _message: &str) {
|
||||
unimplemented!("'register_panic_error_message' is never used when running under PolkaVM");
|
||||
}
|
||||
}
|
||||
|
||||
fn call_host_function(caller: &mut Caller<()>, function: &dyn Function) -> Result<(), String> {
|
||||
let mut args = [Value::I64(0); Reg::ARG_REGS.len()];
|
||||
let mut nth_reg = 0;
|
||||
for (nth_arg, kind) in function.signature().args.iter().enumerate() {
|
||||
match kind {
|
||||
ValueType::I32 => {
|
||||
args[nth_arg] = Value::I32(caller.instance.reg(Reg::ARG_REGS[nth_reg]) as i32);
|
||||
nth_reg += 1;
|
||||
},
|
||||
ValueType::F32 => {
|
||||
args[nth_arg] = Value::F32(caller.instance.reg(Reg::ARG_REGS[nth_reg]) as u32);
|
||||
nth_reg += 1;
|
||||
},
|
||||
ValueType::I64 =>
|
||||
if caller.instance.is_64_bit() {
|
||||
args[nth_arg] = Value::I64(caller.instance.reg(Reg::ARG_REGS[nth_reg]) as i64);
|
||||
nth_reg += 1;
|
||||
} else {
|
||||
let value_lo = caller.instance.reg(Reg::ARG_REGS[nth_reg]);
|
||||
nth_reg += 1;
|
||||
|
||||
let value_hi = caller.instance.reg(Reg::ARG_REGS[nth_reg]);
|
||||
nth_reg += 1;
|
||||
|
||||
args[nth_arg] =
|
||||
Value::I64((u64::from(value_lo) | (u64::from(value_hi) << 32)) as i64);
|
||||
},
|
||||
ValueType::F64 =>
|
||||
if caller.instance.is_64_bit() {
|
||||
args[nth_arg] = Value::F64(caller.instance.reg(Reg::ARG_REGS[nth_reg]));
|
||||
nth_reg += 1;
|
||||
} else {
|
||||
let value_lo = caller.instance.reg(Reg::ARG_REGS[nth_reg]);
|
||||
nth_reg += 1;
|
||||
|
||||
let value_hi = caller.instance.reg(Reg::ARG_REGS[nth_reg]);
|
||||
nth_reg += 1;
|
||||
|
||||
args[nth_arg] = Value::F64(u64::from(value_lo) | (u64::from(value_hi) << 32));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
log::trace!(
|
||||
"Calling host function: '{}', args = {:?}",
|
||||
function.name(),
|
||||
&args[..function.signature().args.len()]
|
||||
);
|
||||
|
||||
let value = match function
|
||||
.execute(&mut Context(caller), &mut args.into_iter().take(function.signature().args.len()))
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
let name = function.name();
|
||||
return Err(format!("call into the host function '{name}' failed: {error}"));
|
||||
},
|
||||
};
|
||||
|
||||
if let Some(value) = value {
|
||||
match value {
|
||||
Value::I32(value) => {
|
||||
caller.instance.set_reg(Reg::A0, value as u64);
|
||||
},
|
||||
Value::F32(value) => {
|
||||
caller.instance.set_reg(Reg::A0, value as u64);
|
||||
},
|
||||
Value::I64(value) =>
|
||||
if caller.instance.is_64_bit() {
|
||||
caller.instance.set_reg(Reg::A0, value as u64);
|
||||
} else {
|
||||
caller.instance.set_reg(Reg::A0, value as u64);
|
||||
caller.instance.set_reg(Reg::A1, (value >> 32) as u64);
|
||||
},
|
||||
Value::F64(value) =>
|
||||
if caller.instance.is_64_bit() {
|
||||
caller.instance.set_reg(Reg::A0, value as u64);
|
||||
} else {
|
||||
caller.instance.set_reg(Reg::A0, value as u64);
|
||||
caller.instance.set_reg(Reg::A1, (value >> 32) as u64);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_runtime<H>(blob: &polkavm::ProgramBlob) -> Result<Box<dyn WasmModule>, WasmError>
|
||||
where
|
||||
H: HostFunctions,
|
||||
{
|
||||
static ENGINE: std::sync::OnceLock<Result<polkavm::Engine, polkavm::Error>> =
|
||||
std::sync::OnceLock::new();
|
||||
|
||||
let engine = ENGINE.get_or_init(|| {
|
||||
let config = polkavm::Config::from_env()?;
|
||||
polkavm::Engine::new(&config)
|
||||
});
|
||||
|
||||
let engine = match engine {
|
||||
Ok(ref engine) => engine,
|
||||
Err(ref error) => {
|
||||
return Err(WasmError::Other(error.to_string()));
|
||||
},
|
||||
};
|
||||
|
||||
let module =
|
||||
polkavm::Module::from_blob(&engine, &polkavm::ModuleConfig::default(), blob.clone())?;
|
||||
|
||||
let mut linker = polkavm::Linker::new();
|
||||
|
||||
for function in H::host_functions() {
|
||||
linker.define_untyped(function.name(), |mut caller: Caller<()>| {
|
||||
call_host_function(&mut caller, function)
|
||||
})?;
|
||||
}
|
||||
let instance_pre = linker.instantiate_pre(&module)?;
|
||||
Ok(Box::new(InstancePre(instance_pre)))
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
[package]
|
||||
name = "sc-runtime-test"
|
||||
version = "2.0.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
build = "build.rs"
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
publish = false
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
sp-core = { workspace = true }
|
||||
sp-io = { features = ["improved_panic_error_reporting"], workspace = true }
|
||||
sp-runtime = { workspace = true }
|
||||
sp-runtime-interface = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
substrate-wasm-builder = { optional = true, workspace = true, default-features = true }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"sp-core/std",
|
||||
"sp-io/std",
|
||||
"sp-runtime-interface/std",
|
||||
"sp-runtime/std",
|
||||
"substrate-wasm-builder",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"sp-io/runtime-benchmarks",
|
||||
"sp-runtime-interface/runtime-benchmarks",
|
||||
"sp-runtime/runtime-benchmarks",
|
||||
"substrate-wasm-builder?/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1,43 @@
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
fn main() {
|
||||
// regular build
|
||||
#[cfg(feature = "std")]
|
||||
{
|
||||
substrate_wasm_builder::WasmBuilder::new()
|
||||
.with_current_project()
|
||||
.export_heap_base()
|
||||
.import_memory()
|
||||
.disable_runtime_version_section_check()
|
||||
.build();
|
||||
}
|
||||
|
||||
// and building with tracing activated
|
||||
#[cfg(feature = "std")]
|
||||
{
|
||||
substrate_wasm_builder::WasmBuilder::new()
|
||||
.with_current_project()
|
||||
.export_heap_base()
|
||||
.import_memory()
|
||||
.set_file_name("wasm_binary_with_tracing.rs")
|
||||
.append_to_rust_flags(r#"--cfg feature="with-tracing""#)
|
||||
.disable_runtime_version_section_check()
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
// Make the WASM binary available.
|
||||
#[cfg(feature = "std")]
|
||||
include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs"));
|
||||
|
||||
/// Wasm binary unwrapped. If built with `SKIP_WASM_BUILD`, the function panics.
|
||||
#[cfg(feature = "std")]
|
||||
pub fn wasm_binary_unwrap() -> &'static [u8] {
|
||||
WASM_BINARY.expect(
|
||||
"Development wasm binary is not available. Testing is only supported with the flag \
|
||||
disabled.",
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
extern crate alloc;
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
use alloc::{vec, vec::Vec};
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
use sp_core::{ed25519, sr25519};
|
||||
#[cfg(not(feature = "std"))]
|
||||
use sp_io::{
|
||||
crypto::{ed25519_verify, sr25519_verify},
|
||||
hashing::{blake2_128, blake2_256, sha2_256, twox_128, twox_256},
|
||||
storage, wasm_tracing,
|
||||
};
|
||||
#[cfg(not(feature = "std"))]
|
||||
use sp_runtime::{
|
||||
print,
|
||||
traits::{BlakeTwo256, Hash},
|
||||
};
|
||||
|
||||
extern "C" {
|
||||
#[allow(dead_code)]
|
||||
fn missing_external();
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn yet_another_missing_external();
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
/// The size of a WASM page in bytes.
|
||||
const WASM_PAGE_SIZE: usize = 65536;
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
/// Mutable static variables should be always observed to have
|
||||
/// the initialized value at the start of a runtime call.
|
||||
static mut MUTABLE_STATIC: u64 = 32;
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
/// This is similar to `MUTABLE_STATIC`. The tests need `MUTABLE_STATIC` for testing that
|
||||
/// non-null initialization data is properly restored during instance reusing.
|
||||
///
|
||||
/// `MUTABLE_STATIC_BSS` on the other hand focuses on the zeroed data. This is important since there
|
||||
/// may be differences in handling zeroed and non-zeroed data.
|
||||
static mut MUTABLE_STATIC_BSS: u64 = 0;
|
||||
|
||||
sp_core::wasm_export_functions! {
|
||||
fn test_calling_missing_external() {
|
||||
unsafe { missing_external() }
|
||||
}
|
||||
|
||||
fn test_calling_yet_another_missing_external() {
|
||||
unsafe { yet_another_missing_external() }
|
||||
}
|
||||
|
||||
fn test_data_in(input: Vec<u8>) -> Vec<u8> {
|
||||
print("set_storage");
|
||||
storage::set(b"input", &input);
|
||||
|
||||
print("storage");
|
||||
let foo = storage::get(b"foo").unwrap();
|
||||
|
||||
print("set_storage");
|
||||
storage::set(b"baz", &foo);
|
||||
|
||||
print("finished!");
|
||||
b"all ok!".to_vec()
|
||||
}
|
||||
|
||||
fn test_clear_prefix(input: Vec<u8>) -> Vec<u8> {
|
||||
storage::clear_prefix(&input, None);
|
||||
b"all ok!".to_vec()
|
||||
}
|
||||
|
||||
fn test_empty_return() {}
|
||||
|
||||
fn test_dirty_plenty_memory(heap_base: u32, heap_pages: u32) {
|
||||
// This piece of code will dirty multiple pages of memory. The number of pages is given by
|
||||
// the `heap_pages`. It's unit is a wasm page (64KiB). The first page to be cleared
|
||||
// is a wasm page that that follows the one that holds the `heap_base` address.
|
||||
//
|
||||
// This function dirties the **host** pages. I.e. we dirty 4KiB at a time and it will take
|
||||
// 16 writes to process a single wasm page.
|
||||
|
||||
let heap_ptr = heap_base as usize;
|
||||
|
||||
// Find the next wasm page boundary.
|
||||
let heap_ptr = round_up_to(heap_ptr, WASM_PAGE_SIZE);
|
||||
|
||||
// Make it an actual pointer
|
||||
let heap_ptr = heap_ptr as *mut u8;
|
||||
|
||||
// Traverse the host pages and make each one dirty
|
||||
let host_pages = heap_pages as usize * 16;
|
||||
for i in 0..host_pages {
|
||||
unsafe {
|
||||
// technically this is an UB, but there is no way Rust can find this out.
|
||||
heap_ptr.add(i * 4096).write(0);
|
||||
}
|
||||
}
|
||||
|
||||
fn round_up_to(n: usize, divisor: usize) -> usize {
|
||||
(n + divisor - 1) / divisor
|
||||
}
|
||||
}
|
||||
|
||||
fn test_allocate_vec(size: u32) -> Vec<u8> {
|
||||
Vec::with_capacity(size as usize)
|
||||
}
|
||||
|
||||
fn test_fp_f32add(a: [u8; 4], b: [u8; 4]) -> [u8; 4] {
|
||||
let a = f32::from_le_bytes(a);
|
||||
let b = f32::from_le_bytes(b);
|
||||
f32::to_le_bytes(a + b)
|
||||
}
|
||||
|
||||
fn test_panic() { panic!("test panic") }
|
||||
|
||||
fn test_conditional_panic(input: Vec<u8>) -> Vec<u8> {
|
||||
if input.len() > 0 {
|
||||
panic!("test panic")
|
||||
}
|
||||
|
||||
input
|
||||
}
|
||||
|
||||
fn test_blake2_256(input: Vec<u8>) -> Vec<u8> {
|
||||
blake2_256(&input).to_vec()
|
||||
}
|
||||
|
||||
fn test_blake2_128(input: Vec<u8>) -> Vec<u8> {
|
||||
blake2_128(&input).to_vec()
|
||||
}
|
||||
|
||||
fn test_sha2_256(input: Vec<u8>) -> Vec<u8> {
|
||||
sha2_256(&input).to_vec()
|
||||
}
|
||||
|
||||
fn test_twox_256(input: Vec<u8>) -> Vec<u8> {
|
||||
twox_256(&input).to_vec()
|
||||
}
|
||||
|
||||
fn test_twox_128(input: Vec<u8>) -> Vec<u8> {
|
||||
twox_128(&input).to_vec()
|
||||
}
|
||||
|
||||
fn test_ed25519_verify(input: Vec<u8>) -> bool {
|
||||
let mut pubkey = [0; 32];
|
||||
let mut sig = [0; 64];
|
||||
|
||||
pubkey.copy_from_slice(&input[0..32]);
|
||||
sig.copy_from_slice(&input[32..96]);
|
||||
|
||||
let msg = b"all ok!";
|
||||
ed25519_verify(&ed25519::Signature::from(sig), &msg[..], &ed25519::Public::from(pubkey))
|
||||
}
|
||||
|
||||
fn test_sr25519_verify(input: Vec<u8>) -> bool {
|
||||
let mut pubkey = [0; 32];
|
||||
let mut sig = [0; 64];
|
||||
|
||||
pubkey.copy_from_slice(&input[0..32]);
|
||||
sig.copy_from_slice(&input[32..96]);
|
||||
|
||||
let msg = b"all ok!";
|
||||
sr25519_verify(&sr25519::Signature::from(sig), &msg[..], &sr25519::Public::from(pubkey))
|
||||
}
|
||||
|
||||
fn test_ordered_trie_root() -> Vec<u8> {
|
||||
BlakeTwo256::ordered_trie_root(
|
||||
vec![
|
||||
b"zero"[..].into(),
|
||||
b"one"[..].into(),
|
||||
b"two"[..].into(),
|
||||
],
|
||||
sp_core::storage::StateVersion::V1,
|
||||
).as_ref().to_vec()
|
||||
}
|
||||
|
||||
fn test_offchain_index_set() {
|
||||
sp_io::offchain_index::set(b"k", b"v");
|
||||
}
|
||||
|
||||
fn test_offchain_local_storage() -> bool {
|
||||
let kind = sp_core::offchain::StorageKind::PERSISTENT;
|
||||
assert_eq!(sp_io::offchain::local_storage_get(kind, b"test"), None);
|
||||
sp_io::offchain::local_storage_set(kind, b"test", b"asd");
|
||||
assert_eq!(sp_io::offchain::local_storage_get(kind, b"test"), Some(b"asd".to_vec()));
|
||||
|
||||
let res = sp_io::offchain::local_storage_compare_and_set(
|
||||
kind,
|
||||
b"test",
|
||||
Some(b"asd".to_vec()),
|
||||
b"",
|
||||
);
|
||||
assert_eq!(sp_io::offchain::local_storage_get(kind, b"test"), Some(b"".to_vec()));
|
||||
res
|
||||
}
|
||||
|
||||
fn test_offchain_local_storage_with_none() {
|
||||
let kind = sp_core::offchain::StorageKind::PERSISTENT;
|
||||
assert_eq!(sp_io::offchain::local_storage_get(kind, b"test"), None);
|
||||
|
||||
let res = sp_io::offchain::local_storage_compare_and_set(kind, b"test", None, b"value");
|
||||
assert_eq!(res, true);
|
||||
assert_eq!(sp_io::offchain::local_storage_get(kind, b"test"), Some(b"value".to_vec()));
|
||||
}
|
||||
|
||||
fn test_offchain_http() -> bool {
|
||||
use sp_core::offchain::HttpRequestStatus;
|
||||
let run = || -> Option<()> {
|
||||
let id = sp_io::offchain::http_request_start(
|
||||
"POST",
|
||||
"http://localhost:12345",
|
||||
&[],
|
||||
).ok()?;
|
||||
sp_io::offchain::http_request_add_header(id, "X-Auth", "test").ok()?;
|
||||
sp_io::offchain::http_request_write_body(id, &[1, 2, 3, 4], None).ok()?;
|
||||
sp_io::offchain::http_request_write_body(id, &[], None).ok()?;
|
||||
let status = sp_io::offchain::http_response_wait(&[id], None);
|
||||
assert!(status == vec![HttpRequestStatus::Finished(200)], "Expected Finished(200) status.");
|
||||
let headers = sp_io::offchain::http_response_headers(id);
|
||||
assert_eq!(headers, vec![(b"X-Auth".to_vec(), b"hello".to_vec())]);
|
||||
let mut buffer = vec![0; 64];
|
||||
let read = sp_io::offchain::http_response_read_body(id, &mut buffer, None).ok()?;
|
||||
assert_eq!(read, 3);
|
||||
assert_eq!(&buffer[0..read as usize], &[1, 2, 3]);
|
||||
let read = sp_io::offchain::http_response_read_body(id, &mut buffer, None).ok()?;
|
||||
assert_eq!(read, 0);
|
||||
|
||||
Some(())
|
||||
};
|
||||
|
||||
run().is_some()
|
||||
}
|
||||
|
||||
fn test_enter_span() -> u64 {
|
||||
wasm_tracing::enter_span(Default::default())
|
||||
}
|
||||
|
||||
fn test_exit_span(span_id: u64) {
|
||||
wasm_tracing::exit(span_id)
|
||||
}
|
||||
|
||||
fn test_nested_spans() {
|
||||
sp_io::init_tracing();
|
||||
let span_id = wasm_tracing::enter_span(Default::default());
|
||||
{
|
||||
sp_io::init_tracing();
|
||||
let span_id = wasm_tracing::enter_span(Default::default());
|
||||
wasm_tracing::exit(span_id);
|
||||
}
|
||||
wasm_tracing::exit(span_id);
|
||||
}
|
||||
|
||||
fn returns_mutable_static() -> u64 {
|
||||
unsafe {
|
||||
MUTABLE_STATIC += 1;
|
||||
MUTABLE_STATIC
|
||||
}
|
||||
}
|
||||
|
||||
fn returns_mutable_static_bss() -> u64 {
|
||||
unsafe {
|
||||
MUTABLE_STATIC_BSS += 1;
|
||||
MUTABLE_STATIC_BSS
|
||||
}
|
||||
}
|
||||
|
||||
fn allocates_huge_stack_array(trap: bool) -> Vec<u8> {
|
||||
// Allocate a stack frame that is approx. 75% of the stack (assuming it is 1MB).
|
||||
// This will just decrease (stacks in wasm32-u-u grow downwards) the stack
|
||||
// pointer. This won't trap on the current compilers.
|
||||
let mut data = [0u8; 1024 * 768];
|
||||
|
||||
// Then make sure we actually write something to it.
|
||||
//
|
||||
// If:
|
||||
// 1. the stack area is placed at the beginning of the linear memory space, and
|
||||
// 2. the stack pointer points to out-of-bounds area, and
|
||||
// 3. a write is performed around the current stack pointer.
|
||||
//
|
||||
// then a trap should happen.
|
||||
//
|
||||
for (i, v) in data.iter_mut().enumerate() {
|
||||
*v = i as u8; // deliberate truncation
|
||||
}
|
||||
|
||||
if trap {
|
||||
// There is a small chance of this to be pulled up in theory. In practice
|
||||
// the probability of that is rather low.
|
||||
panic!()
|
||||
}
|
||||
|
||||
data.to_vec()
|
||||
}
|
||||
|
||||
// Check that the heap at `heap_base + offset` don't contains the test message.
|
||||
// After the check succeeds the test message is written into the heap.
|
||||
//
|
||||
// It is expected that the given pointer is not allocated.
|
||||
fn check_and_set_in_heap(heap_base: u32, offset: u32) {
|
||||
let test_message = b"Hello invalid heap memory";
|
||||
let ptr = (heap_base + offset) as *mut u8;
|
||||
|
||||
let message_slice = unsafe { alloc::slice::from_raw_parts_mut(ptr, test_message.len()) };
|
||||
|
||||
assert_ne!(test_message, message_slice);
|
||||
message_slice.copy_from_slice(test_message);
|
||||
}
|
||||
|
||||
fn test_return_i8() -> i8 {
|
||||
-66
|
||||
}
|
||||
|
||||
fn test_take_i8(value: i8) {
|
||||
assert_eq!(value, -66);
|
||||
}
|
||||
|
||||
fn allocate_two_gigabyte() -> u32 {
|
||||
let mut data = Vec::new();
|
||||
for _ in 0..205 {
|
||||
data.push(Vec::<u8>::with_capacity(10 * 1024 * 1024));
|
||||
}
|
||||
|
||||
data.iter().map(|d| d.capacity() as u32).sum()
|
||||
}
|
||||
|
||||
fn test_abort_on_panic() {
|
||||
sp_io::panic_handler::abort_on_panic("test_abort_on_panic called");
|
||||
}
|
||||
|
||||
fn test_unreachable_intrinsic() {
|
||||
core::arch::wasm32::unreachable()
|
||||
}
|
||||
|
||||
fn test_return_value() -> u64 {
|
||||
// Mainly a test that the macro is working when we have a return statement here.
|
||||
return 1234;
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that check output validity. We explicitly return the ptr and len, so we avoid using the
|
||||
// `wasm_export_functions` macro.
|
||||
mod output_validity {
|
||||
#[cfg(not(feature = "std"))]
|
||||
use super::WASM_PAGE_SIZE;
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
use sp_runtime_interface::pack_ptr_and_len;
|
||||
|
||||
// Returns a huge len. It should result in an error, and not an allocation.
|
||||
#[no_mangle]
|
||||
#[cfg(not(feature = "std"))]
|
||||
pub extern "C" fn test_return_huge_len(_params: *const u8, _len: usize) -> u64 {
|
||||
pack_ptr_and_len(0, u32::MAX)
|
||||
}
|
||||
|
||||
// Returns an offset right before the edge of the wasm memory boundary. It should succeed.
|
||||
#[no_mangle]
|
||||
#[cfg(not(feature = "std"))]
|
||||
pub extern "C" fn test_return_max_memory_offset(_params: *const u8, _len: usize) -> u64 {
|
||||
let output_ptr = (core::arch::wasm32::memory_size(0) * WASM_PAGE_SIZE) as u32 - 1;
|
||||
let ptr = output_ptr as *mut u8;
|
||||
unsafe {
|
||||
ptr.write(u8::MAX);
|
||||
}
|
||||
pack_ptr_and_len(output_ptr, 1)
|
||||
}
|
||||
|
||||
// Returns an offset right after the edge of the wasm memory boundary. It should fail.
|
||||
#[no_mangle]
|
||||
#[cfg(not(feature = "std"))]
|
||||
pub extern "C" fn test_return_max_memory_offset_plus_one(
|
||||
_params: *const u8,
|
||||
_len: usize,
|
||||
) -> u64 {
|
||||
pack_ptr_and_len((core::arch::wasm32::memory_size(0) * WASM_PAGE_SIZE) as u32, 1)
|
||||
}
|
||||
|
||||
// Returns an output that overflows the u32 range. It should result in an error.
|
||||
#[no_mangle]
|
||||
#[cfg(not(feature = "std"))]
|
||||
pub extern "C" fn test_return_overflow(_params: *const u8, _len: usize) -> u64 {
|
||||
pack_ptr_and_len(u32::MAX, 1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,809 @@
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::{
|
||||
error::{Error, Result},
|
||||
wasm_runtime::{RuntimeCache, WasmExecutionMethod},
|
||||
RuntimeVersionOf,
|
||||
};
|
||||
|
||||
use std::{
|
||||
marker::PhantomData,
|
||||
panic::{AssertUnwindSafe, UnwindSafe},
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use codec::Encode;
|
||||
use sc_executor_common::{
|
||||
runtime_blob::RuntimeBlob,
|
||||
wasm_runtime::{
|
||||
AllocationStats, HeapAllocStrategy, WasmInstance, WasmModule, DEFAULT_HEAP_ALLOC_STRATEGY,
|
||||
},
|
||||
};
|
||||
use sp_core::traits::{CallContext, CodeExecutor, Externalities, RuntimeCode};
|
||||
use sp_version::{GetNativeVersion, NativeVersion, RuntimeVersion};
|
||||
use sp_wasm_interface::{ExtendedHostFunctions, HostFunctions};
|
||||
|
||||
/// Set up the externalities and safe calling environment to execute runtime calls.
|
||||
///
|
||||
/// If the inner closure panics, it will be caught and return an error.
|
||||
pub fn with_externalities_safe<F, U>(ext: &mut dyn Externalities, f: F) -> Result<U>
|
||||
where
|
||||
F: UnwindSafe + FnOnce() -> U,
|
||||
{
|
||||
sp_externalities::set_and_run_with_externalities(ext, move || {
|
||||
// Substrate uses custom panic hook that terminates process on panic. Disable
|
||||
// termination for the native call.
|
||||
let _guard = sp_panic_handler::AbortGuard::force_unwind();
|
||||
std::panic::catch_unwind(f).map_err(|e| {
|
||||
if let Some(err) = e.downcast_ref::<String>() {
|
||||
Error::RuntimePanicked(err.clone())
|
||||
} else if let Some(err) = e.downcast_ref::<&'static str>() {
|
||||
Error::RuntimePanicked(err.to_string())
|
||||
} else {
|
||||
Error::RuntimePanicked("Unknown panic".into())
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Delegate for dispatching a CodeExecutor call.
|
||||
///
|
||||
/// By dispatching we mean that we execute a runtime function specified by it's name.
|
||||
pub trait NativeExecutionDispatch: Send + Sync {
|
||||
/// Host functions for custom runtime interfaces that should be callable from within the runtime
|
||||
/// besides the default Substrate runtime interfaces.
|
||||
type ExtendHostFunctions: HostFunctions;
|
||||
|
||||
/// Dispatch a method in the runtime.
|
||||
fn dispatch(method: &str, data: &[u8]) -> Option<Vec<u8>>;
|
||||
|
||||
/// Provide native runtime version.
|
||||
fn native_version() -> NativeVersion;
|
||||
}
|
||||
|
||||
fn unwrap_heap_pages(pages: Option<HeapAllocStrategy>) -> HeapAllocStrategy {
|
||||
pages.unwrap_or_else(|| DEFAULT_HEAP_ALLOC_STRATEGY)
|
||||
}
|
||||
|
||||
/// Builder for creating a [`WasmExecutor`] instance.
|
||||
pub struct WasmExecutorBuilder<H = sp_io::SubstrateHostFunctions> {
|
||||
_phantom: PhantomData<H>,
|
||||
method: WasmExecutionMethod,
|
||||
onchain_heap_alloc_strategy: Option<HeapAllocStrategy>,
|
||||
offchain_heap_alloc_strategy: Option<HeapAllocStrategy>,
|
||||
ignore_onchain_heap_pages: bool,
|
||||
max_runtime_instances: usize,
|
||||
cache_path: Option<PathBuf>,
|
||||
allow_missing_host_functions: bool,
|
||||
runtime_cache_size: u8,
|
||||
}
|
||||
|
||||
impl<H> WasmExecutorBuilder<H> {
|
||||
/// Create a new instance of `Self`
|
||||
///
|
||||
/// - `method`: The wasm execution method that should be used by the executor.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
_phantom: PhantomData,
|
||||
method: WasmExecutionMethod::default(),
|
||||
onchain_heap_alloc_strategy: None,
|
||||
offchain_heap_alloc_strategy: None,
|
||||
ignore_onchain_heap_pages: false,
|
||||
max_runtime_instances: 2,
|
||||
runtime_cache_size: 4,
|
||||
allow_missing_host_functions: false,
|
||||
cache_path: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the wasm executor with execution method that should be used by the executor.
|
||||
pub fn with_execution_method(mut self, method: WasmExecutionMethod) -> Self {
|
||||
self.method = method;
|
||||
self
|
||||
}
|
||||
|
||||
/// Create the wasm executor with the given number of `heap_alloc_strategy` for onchain runtime
|
||||
/// calls.
|
||||
pub fn with_onchain_heap_alloc_strategy(
|
||||
mut self,
|
||||
heap_alloc_strategy: HeapAllocStrategy,
|
||||
) -> Self {
|
||||
self.onchain_heap_alloc_strategy = Some(heap_alloc_strategy);
|
||||
self
|
||||
}
|
||||
|
||||
/// Create the wasm executor with the given number of `heap_alloc_strategy` for offchain runtime
|
||||
/// calls.
|
||||
pub fn with_offchain_heap_alloc_strategy(
|
||||
mut self,
|
||||
heap_alloc_strategy: HeapAllocStrategy,
|
||||
) -> Self {
|
||||
self.offchain_heap_alloc_strategy = Some(heap_alloc_strategy);
|
||||
self
|
||||
}
|
||||
|
||||
/// Create the wasm executor and follow/ignore onchain heap pages value.
|
||||
///
|
||||
/// By default this the onchain heap pages value is followed.
|
||||
pub fn with_ignore_onchain_heap_pages(mut self, ignore_onchain_heap_pages: bool) -> Self {
|
||||
self.ignore_onchain_heap_pages = ignore_onchain_heap_pages;
|
||||
self
|
||||
}
|
||||
|
||||
/// Create the wasm executor with the given maximum number of `instances`.
|
||||
///
|
||||
/// The number of `instances` defines how many different instances of a runtime the cache is
|
||||
/// storing.
|
||||
///
|
||||
/// By default the maximum number of `instances` is `2`.
|
||||
pub fn with_max_runtime_instances(mut self, instances: usize) -> Self {
|
||||
self.max_runtime_instances = instances;
|
||||
self
|
||||
}
|
||||
|
||||
/// Create the wasm executor with the given `cache_path`.
|
||||
///
|
||||
/// The `cache_path` is A path to a directory where the executor can place its files for
|
||||
/// purposes of caching. This may be important in cases when there are many different modules
|
||||
/// with the compiled execution method is used.
|
||||
///
|
||||
/// By default there is no `cache_path` given.
|
||||
pub fn with_cache_path(mut self, cache_path: impl Into<PathBuf>) -> Self {
|
||||
self.cache_path = Some(cache_path.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Create the wasm executor and allow/forbid missing host functions.
|
||||
///
|
||||
/// If missing host functions are forbidden, the instantiation of a wasm blob will fail
|
||||
/// for imported host functions that the executor is not aware of. If they are allowed,
|
||||
/// a stub is generated that will return an error when being called while executing the wasm.
|
||||
///
|
||||
/// By default missing host functions are forbidden.
|
||||
pub fn with_allow_missing_host_functions(mut self, allow: bool) -> Self {
|
||||
self.allow_missing_host_functions = allow;
|
||||
self
|
||||
}
|
||||
|
||||
/// Create the wasm executor with the given `runtime_cache_size`.
|
||||
///
|
||||
/// Defines the number of different runtimes/instantiated wasm blobs the cache stores.
|
||||
/// Runtimes/wasm blobs are differentiated based on the hash and the number of heap pages.
|
||||
///
|
||||
/// By default this value is set to `4`.
|
||||
pub fn with_runtime_cache_size(mut self, runtime_cache_size: u8) -> Self {
|
||||
self.runtime_cache_size = runtime_cache_size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the configured [`WasmExecutor`].
|
||||
pub fn build(self) -> WasmExecutor<H> {
|
||||
WasmExecutor {
|
||||
method: self.method,
|
||||
default_offchain_heap_alloc_strategy: unwrap_heap_pages(
|
||||
self.offchain_heap_alloc_strategy,
|
||||
),
|
||||
default_onchain_heap_alloc_strategy: unwrap_heap_pages(
|
||||
self.onchain_heap_alloc_strategy,
|
||||
),
|
||||
ignore_onchain_heap_pages: self.ignore_onchain_heap_pages,
|
||||
cache: Arc::new(RuntimeCache::new(
|
||||
self.max_runtime_instances,
|
||||
self.cache_path.clone(),
|
||||
self.runtime_cache_size,
|
||||
)),
|
||||
cache_path: self.cache_path,
|
||||
allow_missing_host_functions: self.allow_missing_host_functions,
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An abstraction over Wasm code executor. Supports selecting execution backend and
|
||||
/// manages runtime cache.
|
||||
pub struct WasmExecutor<H = sp_io::SubstrateHostFunctions> {
|
||||
/// Method used to execute fallback Wasm code.
|
||||
method: WasmExecutionMethod,
|
||||
/// The heap allocation strategy for onchain Wasm calls.
|
||||
default_onchain_heap_alloc_strategy: HeapAllocStrategy,
|
||||
/// The heap allocation strategy for offchain Wasm calls.
|
||||
default_offchain_heap_alloc_strategy: HeapAllocStrategy,
|
||||
/// Ignore onchain heap pages value.
|
||||
ignore_onchain_heap_pages: bool,
|
||||
/// WASM runtime cache.
|
||||
cache: Arc<RuntimeCache>,
|
||||
/// The path to a directory which the executor can leverage for a file cache, e.g. put there
|
||||
/// compiled artifacts.
|
||||
cache_path: Option<PathBuf>,
|
||||
/// Ignore missing function imports.
|
||||
allow_missing_host_functions: bool,
|
||||
phantom: PhantomData<H>,
|
||||
}
|
||||
|
||||
impl<H> Clone for WasmExecutor<H> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
method: self.method,
|
||||
default_onchain_heap_alloc_strategy: self.default_onchain_heap_alloc_strategy,
|
||||
default_offchain_heap_alloc_strategy: self.default_offchain_heap_alloc_strategy,
|
||||
ignore_onchain_heap_pages: self.ignore_onchain_heap_pages,
|
||||
cache: self.cache.clone(),
|
||||
cache_path: self.cache_path.clone(),
|
||||
allow_missing_host_functions: self.allow_missing_host_functions,
|
||||
phantom: self.phantom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WasmExecutor<sp_io::SubstrateHostFunctions> {
|
||||
fn default() -> Self {
|
||||
WasmExecutorBuilder::new().build()
|
||||
}
|
||||
}
|
||||
|
||||
impl<H> WasmExecutor<H> {
|
||||
/// Create new instance.
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// `method` - Method used to execute Wasm code.
|
||||
///
|
||||
/// `default_heap_pages` - Number of 64KB pages to allocate for Wasm execution. Internally this
|
||||
/// will be mapped as [`HeapAllocStrategy::Static`] where `default_heap_pages` represent the
|
||||
/// static number of heap pages to allocate. Defaults to `DEFAULT_HEAP_ALLOC_STRATEGY` if `None`
|
||||
/// is provided.
|
||||
///
|
||||
/// `max_runtime_instances` - The number of runtime instances to keep in memory ready for reuse.
|
||||
///
|
||||
/// `cache_path` - A path to a directory where the executor can place its files for purposes of
|
||||
/// caching. This may be important in cases when there are many different modules with the
|
||||
/// compiled execution method is used.
|
||||
///
|
||||
/// `runtime_cache_size` - The capacity of runtime cache.
|
||||
#[deprecated(note = "use `Self::builder` method instead of it")]
|
||||
pub fn new(
|
||||
method: WasmExecutionMethod,
|
||||
default_heap_pages: Option<u64>,
|
||||
max_runtime_instances: usize,
|
||||
cache_path: Option<PathBuf>,
|
||||
runtime_cache_size: u8,
|
||||
) -> Self {
|
||||
WasmExecutor {
|
||||
method,
|
||||
default_onchain_heap_alloc_strategy: unwrap_heap_pages(
|
||||
default_heap_pages.map(|h| HeapAllocStrategy::Static { extra_pages: h as _ }),
|
||||
),
|
||||
default_offchain_heap_alloc_strategy: unwrap_heap_pages(
|
||||
default_heap_pages.map(|h| HeapAllocStrategy::Static { extra_pages: h as _ }),
|
||||
),
|
||||
ignore_onchain_heap_pages: false,
|
||||
cache: Arc::new(RuntimeCache::new(
|
||||
max_runtime_instances,
|
||||
cache_path.clone(),
|
||||
runtime_cache_size,
|
||||
)),
|
||||
cache_path,
|
||||
allow_missing_host_functions: false,
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Instantiate a builder for creating an instance of `Self`.
|
||||
pub fn builder() -> WasmExecutorBuilder<H> {
|
||||
WasmExecutorBuilder::new()
|
||||
}
|
||||
|
||||
/// Ignore missing function imports if set true.
|
||||
#[deprecated(note = "use `Self::builder` method instead of it")]
|
||||
pub fn allow_missing_host_functions(&mut self, allow_missing_host_functions: bool) {
|
||||
self.allow_missing_host_functions = allow_missing_host_functions
|
||||
}
|
||||
}
|
||||
|
||||
impl<H> WasmExecutor<H>
|
||||
where
|
||||
H: HostFunctions,
|
||||
{
|
||||
/// Execute the given closure `f` with the latest runtime (based on `runtime_code`).
|
||||
///
|
||||
/// The closure `f` is expected to return `Err(_)` when there happened a `panic!` in native code
|
||||
/// while executing the runtime in Wasm. If a `panic!` occurred, the runtime is invalidated to
|
||||
/// prevent any poisoned state. Native runtime execution does not need to report back
|
||||
/// any `panic!`.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// `runtime` and `ext` are given as `AssertUnwindSafe` to the closure. As described above, the
|
||||
/// runtime is invalidated on any `panic!` to prevent a poisoned state. `ext` is already
|
||||
/// implicitly handled as unwind safe, as we store it in a global variable while executing the
|
||||
/// native runtime.
|
||||
pub fn with_instance<R, F>(
|
||||
&self,
|
||||
runtime_code: &RuntimeCode,
|
||||
ext: &mut dyn Externalities,
|
||||
heap_alloc_strategy: HeapAllocStrategy,
|
||||
f: F,
|
||||
) -> Result<R>
|
||||
where
|
||||
F: FnOnce(
|
||||
AssertUnwindSafe<&dyn WasmModule>,
|
||||
AssertUnwindSafe<&mut dyn WasmInstance>,
|
||||
Option<&RuntimeVersion>,
|
||||
AssertUnwindSafe<&mut dyn Externalities>,
|
||||
) -> Result<Result<R>>,
|
||||
{
|
||||
match self.cache.with_instance::<H, _, _>(
|
||||
runtime_code,
|
||||
ext,
|
||||
self.method,
|
||||
heap_alloc_strategy,
|
||||
self.allow_missing_host_functions,
|
||||
|module, instance, version, ext| {
|
||||
let module = AssertUnwindSafe(module);
|
||||
let instance = AssertUnwindSafe(instance);
|
||||
let ext = AssertUnwindSafe(ext);
|
||||
f(module, instance, version, ext)
|
||||
},
|
||||
)? {
|
||||
Ok(r) => r,
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform a call into the given runtime.
|
||||
///
|
||||
/// The runtime is passed as a [`RuntimeBlob`]. The runtime will be instantiated with the
|
||||
/// parameters this `WasmExecutor` was initialized with.
|
||||
///
|
||||
/// In case of problems with during creation of the runtime or instantiation, a `Err` is
|
||||
/// returned. that describes the message.
|
||||
#[doc(hidden)] // We use this function for tests across multiple crates.
|
||||
pub fn uncached_call(
|
||||
&self,
|
||||
runtime_blob: RuntimeBlob,
|
||||
ext: &mut dyn Externalities,
|
||||
allow_missing_host_functions: bool,
|
||||
export_name: &str,
|
||||
call_data: &[u8],
|
||||
) -> std::result::Result<Vec<u8>, Error> {
|
||||
self.uncached_call_impl(
|
||||
runtime_blob,
|
||||
ext,
|
||||
allow_missing_host_functions,
|
||||
export_name,
|
||||
call_data,
|
||||
&mut None,
|
||||
)
|
||||
}
|
||||
|
||||
/// Same as `uncached_call`, except it also returns allocation statistics.
|
||||
#[doc(hidden)] // We use this function in tests.
|
||||
pub fn uncached_call_with_allocation_stats(
|
||||
&self,
|
||||
runtime_blob: RuntimeBlob,
|
||||
ext: &mut dyn Externalities,
|
||||
allow_missing_host_functions: bool,
|
||||
export_name: &str,
|
||||
call_data: &[u8],
|
||||
) -> (std::result::Result<Vec<u8>, Error>, Option<AllocationStats>) {
|
||||
let mut allocation_stats = None;
|
||||
let result = self.uncached_call_impl(
|
||||
runtime_blob,
|
||||
ext,
|
||||
allow_missing_host_functions,
|
||||
export_name,
|
||||
call_data,
|
||||
&mut allocation_stats,
|
||||
);
|
||||
(result, allocation_stats)
|
||||
}
|
||||
|
||||
fn uncached_call_impl(
|
||||
&self,
|
||||
runtime_blob: RuntimeBlob,
|
||||
ext: &mut dyn Externalities,
|
||||
allow_missing_host_functions: bool,
|
||||
export_name: &str,
|
||||
call_data: &[u8],
|
||||
allocation_stats_out: &mut Option<AllocationStats>,
|
||||
) -> std::result::Result<Vec<u8>, Error> {
|
||||
let module = crate::wasm_runtime::create_wasm_runtime_with_code::<H>(
|
||||
self.method,
|
||||
self.default_onchain_heap_alloc_strategy,
|
||||
runtime_blob,
|
||||
allow_missing_host_functions,
|
||||
self.cache_path.as_deref(),
|
||||
)
|
||||
.map_err(|e| format!("Failed to create module: {}", e))?;
|
||||
|
||||
let instance =
|
||||
module.new_instance().map_err(|e| format!("Failed to create instance: {}", e))?;
|
||||
|
||||
let mut instance = AssertUnwindSafe(instance);
|
||||
let mut ext = AssertUnwindSafe(ext);
|
||||
let mut allocation_stats_out = AssertUnwindSafe(allocation_stats_out);
|
||||
|
||||
with_externalities_safe(&mut **ext, move || {
|
||||
let (result, allocation_stats) =
|
||||
instance.call_with_allocation_stats(export_name.into(), call_data);
|
||||
**allocation_stats_out = allocation_stats;
|
||||
result
|
||||
})
|
||||
.and_then(|r| r)
|
||||
}
|
||||
}
|
||||
|
||||
impl<H> sp_core::traits::ReadRuntimeVersion for WasmExecutor<H>
|
||||
where
|
||||
H: HostFunctions,
|
||||
{
|
||||
fn read_runtime_version(
|
||||
&self,
|
||||
wasm_code: &[u8],
|
||||
ext: &mut dyn Externalities,
|
||||
) -> std::result::Result<Vec<u8>, String> {
|
||||
let runtime_blob = RuntimeBlob::uncompress_if_needed(wasm_code)
|
||||
.map_err(|e| format!("Failed to create runtime blob: {:?}", e))?;
|
||||
|
||||
if let Some(version) = crate::wasm_runtime::read_embedded_version(&runtime_blob)
|
||||
.map_err(|e| format!("Failed to read the static section: {:?}", e))
|
||||
.map(|v| v.map(|v| v.encode()))?
|
||||
{
|
||||
return Ok(version);
|
||||
}
|
||||
|
||||
// If the blob didn't have embedded runtime version section, we fallback to the legacy
|
||||
// way of fetching the version: i.e. instantiating the given instance and calling
|
||||
// `Core_version` on it.
|
||||
|
||||
self.uncached_call(
|
||||
runtime_blob,
|
||||
ext,
|
||||
// If a runtime upgrade introduces new host functions that are not provided by
|
||||
// the node, we should not fail at instantiation. Otherwise nodes that are
|
||||
// updated could run this successfully and it could lead to a storage root
|
||||
// mismatch when importing this block.
|
||||
true,
|
||||
"Core_version",
|
||||
&[],
|
||||
)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<H> CodeExecutor for WasmExecutor<H>
|
||||
where
|
||||
H: HostFunctions,
|
||||
{
|
||||
type Error = Error;
|
||||
|
||||
fn call(
|
||||
&self,
|
||||
ext: &mut dyn Externalities,
|
||||
runtime_code: &RuntimeCode,
|
||||
method: &str,
|
||||
data: &[u8],
|
||||
context: CallContext,
|
||||
) -> (Result<Vec<u8>>, bool) {
|
||||
tracing::trace!(
|
||||
target: "executor",
|
||||
%method,
|
||||
"Executing function",
|
||||
);
|
||||
|
||||
let on_chain_heap_alloc_strategy = if self.ignore_onchain_heap_pages {
|
||||
self.default_onchain_heap_alloc_strategy
|
||||
} else {
|
||||
runtime_code
|
||||
.heap_pages
|
||||
.map(|h| HeapAllocStrategy::Static { extra_pages: h as _ })
|
||||
.unwrap_or_else(|| self.default_onchain_heap_alloc_strategy)
|
||||
};
|
||||
|
||||
let heap_alloc_strategy = match context {
|
||||
CallContext::Offchain => self.default_offchain_heap_alloc_strategy,
|
||||
CallContext::Onchain => on_chain_heap_alloc_strategy,
|
||||
};
|
||||
|
||||
let result = self.with_instance(
|
||||
runtime_code,
|
||||
ext,
|
||||
heap_alloc_strategy,
|
||||
|_, mut instance, _on_chain_version, mut ext| {
|
||||
with_externalities_safe(&mut **ext, move || instance.call_export(method, data))
|
||||
},
|
||||
);
|
||||
|
||||
(result, false)
|
||||
}
|
||||
}
|
||||
|
||||
impl<H> RuntimeVersionOf for WasmExecutor<H>
|
||||
where
|
||||
H: HostFunctions,
|
||||
{
|
||||
fn runtime_version(
|
||||
&self,
|
||||
ext: &mut dyn Externalities,
|
||||
runtime_code: &RuntimeCode,
|
||||
) -> Result<RuntimeVersion> {
|
||||
let on_chain_heap_pages = if self.ignore_onchain_heap_pages {
|
||||
self.default_onchain_heap_alloc_strategy
|
||||
} else {
|
||||
runtime_code
|
||||
.heap_pages
|
||||
.map(|h| HeapAllocStrategy::Static { extra_pages: h as _ })
|
||||
.unwrap_or_else(|| self.default_onchain_heap_alloc_strategy)
|
||||
};
|
||||
|
||||
self.with_instance(
|
||||
runtime_code,
|
||||
ext,
|
||||
on_chain_heap_pages,
|
||||
|_module, _instance, version, _ext| {
|
||||
Ok(version.cloned().ok_or_else(|| Error::ApiError("Unknown version".into())))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A generic `CodeExecutor` implementation that uses a delegate to determine wasm code equivalence
|
||||
/// and dispatch to native code when possible, falling back on `WasmExecutor` when not.
|
||||
#[deprecated(
|
||||
note = "Native execution will be deprecated, please replace with `WasmExecutor`. Will be removed at end of 2024."
|
||||
)]
|
||||
pub struct NativeElseWasmExecutor<D: NativeExecutionDispatch> {
|
||||
/// Native runtime version info.
|
||||
native_version: NativeVersion,
|
||||
/// Fallback wasm executor.
|
||||
wasm:
|
||||
WasmExecutor<ExtendedHostFunctions<sp_io::SubstrateHostFunctions, D::ExtendHostFunctions>>,
|
||||
|
||||
use_native: bool,
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<D: NativeExecutionDispatch> NativeElseWasmExecutor<D> {
|
||||
///
|
||||
/// Create new instance.
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// `fallback_method` - Method used to execute fallback Wasm code.
|
||||
///
|
||||
/// `default_heap_pages` - Number of 64KB pages to allocate for Wasm execution. Internally this
|
||||
/// will be mapped as [`HeapAllocStrategy::Static`] where `default_heap_pages` represent the
|
||||
/// static number of heap pages to allocate. Defaults to `DEFAULT_HEAP_ALLOC_STRATEGY` if `None`
|
||||
/// is provided.
|
||||
///
|
||||
/// `max_runtime_instances` - The number of runtime instances to keep in memory ready for reuse.
|
||||
///
|
||||
/// `runtime_cache_size` - The capacity of runtime cache.
|
||||
#[deprecated(note = "use `Self::new_with_wasm_executor` method instead of it")]
|
||||
pub fn new(
|
||||
fallback_method: WasmExecutionMethod,
|
||||
default_heap_pages: Option<u64>,
|
||||
max_runtime_instances: usize,
|
||||
runtime_cache_size: u8,
|
||||
) -> Self {
|
||||
let heap_pages = default_heap_pages.map_or(DEFAULT_HEAP_ALLOC_STRATEGY, |h| {
|
||||
HeapAllocStrategy::Static { extra_pages: h as _ }
|
||||
});
|
||||
let wasm = WasmExecutor::builder()
|
||||
.with_execution_method(fallback_method)
|
||||
.with_onchain_heap_alloc_strategy(heap_pages)
|
||||
.with_offchain_heap_alloc_strategy(heap_pages)
|
||||
.with_max_runtime_instances(max_runtime_instances)
|
||||
.with_runtime_cache_size(runtime_cache_size)
|
||||
.build();
|
||||
|
||||
NativeElseWasmExecutor { native_version: D::native_version(), wasm, use_native: true }
|
||||
}
|
||||
|
||||
/// Create a new instance using the given [`WasmExecutor`].
|
||||
pub fn new_with_wasm_executor(
|
||||
executor: WasmExecutor<
|
||||
ExtendedHostFunctions<sp_io::SubstrateHostFunctions, D::ExtendHostFunctions>,
|
||||
>,
|
||||
) -> Self {
|
||||
Self { native_version: D::native_version(), wasm: executor, use_native: true }
|
||||
}
|
||||
|
||||
/// Disable to use native runtime when possible just behave like `WasmExecutor`.
|
||||
///
|
||||
/// Default to enabled.
|
||||
pub fn disable_use_native(&mut self) {
|
||||
self.use_native = false;
|
||||
}
|
||||
|
||||
/// Ignore missing function imports if set true.
|
||||
#[deprecated(note = "use `Self::new_with_wasm_executor` method instead of it")]
|
||||
pub fn allow_missing_host_functions(&mut self, allow_missing_host_functions: bool) {
|
||||
self.wasm.allow_missing_host_functions = allow_missing_host_functions
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<D: NativeExecutionDispatch> RuntimeVersionOf for NativeElseWasmExecutor<D> {
|
||||
fn runtime_version(
|
||||
&self,
|
||||
ext: &mut dyn Externalities,
|
||||
runtime_code: &RuntimeCode,
|
||||
) -> Result<RuntimeVersion> {
|
||||
self.wasm.runtime_version(ext, runtime_code)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<D: NativeExecutionDispatch> GetNativeVersion for NativeElseWasmExecutor<D> {
|
||||
fn native_version(&self) -> &NativeVersion {
|
||||
&self.native_version
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<D: NativeExecutionDispatch + 'static> CodeExecutor for NativeElseWasmExecutor<D> {
|
||||
type Error = Error;
|
||||
|
||||
fn call(
|
||||
&self,
|
||||
ext: &mut dyn Externalities,
|
||||
runtime_code: &RuntimeCode,
|
||||
method: &str,
|
||||
data: &[u8],
|
||||
context: CallContext,
|
||||
) -> (Result<Vec<u8>>, bool) {
|
||||
let use_native = self.use_native;
|
||||
|
||||
tracing::trace!(
|
||||
target: "executor",
|
||||
function = %method,
|
||||
"Executing function",
|
||||
);
|
||||
|
||||
let on_chain_heap_alloc_strategy = if self.wasm.ignore_onchain_heap_pages {
|
||||
self.wasm.default_onchain_heap_alloc_strategy
|
||||
} else {
|
||||
runtime_code
|
||||
.heap_pages
|
||||
.map(|h| HeapAllocStrategy::Static { extra_pages: h as _ })
|
||||
.unwrap_or_else(|| self.wasm.default_onchain_heap_alloc_strategy)
|
||||
};
|
||||
|
||||
let heap_alloc_strategy = match context {
|
||||
CallContext::Offchain => self.wasm.default_offchain_heap_alloc_strategy,
|
||||
CallContext::Onchain => on_chain_heap_alloc_strategy,
|
||||
};
|
||||
|
||||
let mut used_native = false;
|
||||
let result = self.wasm.with_instance(
|
||||
runtime_code,
|
||||
ext,
|
||||
heap_alloc_strategy,
|
||||
|_, mut instance, on_chain_version, mut ext| {
|
||||
let on_chain_version =
|
||||
on_chain_version.ok_or_else(|| Error::ApiError("Unknown version".into()))?;
|
||||
|
||||
let can_call_with =
|
||||
on_chain_version.can_call_with(&self.native_version.runtime_version);
|
||||
|
||||
if use_native && can_call_with {
|
||||
tracing::trace!(
|
||||
target: "executor",
|
||||
native = %self.native_version.runtime_version,
|
||||
chain = %on_chain_version,
|
||||
"Request for native execution succeeded",
|
||||
);
|
||||
|
||||
used_native = true;
|
||||
Ok(with_externalities_safe(&mut **ext, move || D::dispatch(method, data))?
|
||||
.ok_or_else(|| Error::MethodNotFound(method.to_owned())))
|
||||
} else {
|
||||
if !can_call_with {
|
||||
tracing::trace!(
|
||||
target: "executor",
|
||||
native = %self.native_version.runtime_version,
|
||||
chain = %on_chain_version,
|
||||
"Request for native execution failed",
|
||||
);
|
||||
}
|
||||
|
||||
with_externalities_safe(&mut **ext, move || instance.call_export(method, data))
|
||||
}
|
||||
},
|
||||
);
|
||||
(result, used_native)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<D: NativeExecutionDispatch> Clone for NativeElseWasmExecutor<D> {
|
||||
fn clone(&self) -> Self {
|
||||
NativeElseWasmExecutor {
|
||||
native_version: D::native_version(),
|
||||
wasm: self.wasm.clone(),
|
||||
use_native: self.use_native,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<D: NativeExecutionDispatch> sp_core::traits::ReadRuntimeVersion for NativeElseWasmExecutor<D> {
|
||||
fn read_runtime_version(
|
||||
&self,
|
||||
wasm_code: &[u8],
|
||||
ext: &mut dyn Externalities,
|
||||
) -> std::result::Result<Vec<u8>, String> {
|
||||
self.wasm.read_runtime_version(wasm_code, ext)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use sp_runtime_interface::{pass_by::PassFatPointerAndRead, runtime_interface};
|
||||
|
||||
#[runtime_interface]
|
||||
trait MyInterface {
|
||||
fn say_hello_world(data: PassFatPointerAndRead<&str>) {
|
||||
println!("Hello world from: {}", data);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MyExecutorDispatch;
|
||||
|
||||
impl NativeExecutionDispatch for MyExecutorDispatch {
|
||||
type ExtendHostFunctions = (my_interface::HostFunctions, my_interface::HostFunctions);
|
||||
|
||||
fn dispatch(method: &str, data: &[u8]) -> Option<Vec<u8>> {
|
||||
substrate_test_runtime::api::dispatch(method, data)
|
||||
}
|
||||
|
||||
fn native_version() -> NativeVersion {
|
||||
substrate_test_runtime::native_version()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(deprecated)]
|
||||
fn native_executor_registers_custom_interface() {
|
||||
let executor = NativeElseWasmExecutor::<MyExecutorDispatch>::new_with_wasm_executor(
|
||||
WasmExecutor::builder().build(),
|
||||
);
|
||||
|
||||
fn extract_host_functions<H>(
|
||||
_: &WasmExecutor<H>,
|
||||
) -> Vec<&'static dyn sp_wasm_interface::Function>
|
||||
where
|
||||
H: HostFunctions,
|
||||
{
|
||||
H::host_functions()
|
||||
}
|
||||
|
||||
my_interface::HostFunctions::host_functions().iter().for_each(|function| {
|
||||
assert_eq!(
|
||||
extract_host_functions(&executor.wasm).iter().filter(|f| f == &function).count(),
|
||||
2
|
||||
);
|
||||
});
|
||||
|
||||
my_interface::say_hello_world("hey");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,801 @@
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use codec::{Decode, Encode};
|
||||
use sc_executor_common::{
|
||||
error::Error,
|
||||
runtime_blob::RuntimeBlob,
|
||||
wasm_runtime::{HeapAllocStrategy, WasmModule},
|
||||
};
|
||||
use sc_runtime_test::wasm_binary_unwrap;
|
||||
use sp_core::{
|
||||
ed25519, map,
|
||||
offchain::{testing, OffchainDbExt, OffchainWorkerExt},
|
||||
sr25519,
|
||||
traits::Externalities,
|
||||
Pair,
|
||||
};
|
||||
use sp_crypto_hashing::{blake2_128, blake2_256, sha2_256, twox_128, twox_256};
|
||||
use sp_runtime::traits::BlakeTwo256;
|
||||
use sp_state_machine::TestExternalities as CoreTestExternalities;
|
||||
use sp_trie::{LayoutV1 as Layout, TrieConfiguration};
|
||||
use std::sync::Arc;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
|
||||
use crate::WasmExecutionMethod;
|
||||
|
||||
pub type TestExternalities = CoreTestExternalities<BlakeTwo256>;
|
||||
type HostFunctions = sp_io::SubstrateHostFunctions;
|
||||
|
||||
/// Simple macro that runs a given method as test with the available wasm execution methods.
|
||||
#[macro_export]
|
||||
macro_rules! test_wasm_execution {
|
||||
($method_name:ident) => {
|
||||
paste::item! {
|
||||
#[test]
|
||||
fn [<$method_name _compiled_recreate_instance_cow>]() {
|
||||
let _ = sp_tracing::try_init_simple();
|
||||
$method_name(WasmExecutionMethod::Compiled {
|
||||
instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::RecreateInstanceCopyOnWrite
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn [<$method_name _compiled_recreate_instance_vanilla>]() {
|
||||
let _ = sp_tracing::try_init_simple();
|
||||
$method_name(WasmExecutionMethod::Compiled {
|
||||
instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::RecreateInstance
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn [<$method_name _compiled_pooling_cow>]() {
|
||||
let _ = sp_tracing::try_init_simple();
|
||||
$method_name(WasmExecutionMethod::Compiled {
|
||||
instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::PoolingCopyOnWrite
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn [<$method_name _compiled_pooling_vanilla>]() {
|
||||
let _ = sp_tracing::try_init_simple();
|
||||
$method_name(WasmExecutionMethod::Compiled {
|
||||
instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::Pooling
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn call_in_wasm<E: Externalities>(
|
||||
function: &str,
|
||||
call_data: &[u8],
|
||||
execution_method: WasmExecutionMethod,
|
||||
ext: &mut E,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
let executor = crate::WasmExecutor::<HostFunctions>::builder()
|
||||
.with_execution_method(execution_method)
|
||||
.build();
|
||||
|
||||
executor.uncached_call(
|
||||
RuntimeBlob::uncompress_if_needed(wasm_binary_unwrap()).unwrap(),
|
||||
ext,
|
||||
true,
|
||||
function,
|
||||
call_data,
|
||||
)
|
||||
}
|
||||
|
||||
test_wasm_execution!(returning_should_work);
|
||||
fn returning_should_work(wasm_method: WasmExecutionMethod) {
|
||||
let mut ext = TestExternalities::default();
|
||||
let mut ext = ext.ext();
|
||||
|
||||
let output = call_in_wasm("test_empty_return", &[], wasm_method, &mut ext).unwrap();
|
||||
assert_eq!(output, vec![0u8; 0]);
|
||||
}
|
||||
|
||||
test_wasm_execution!(call_not_existing_function);
|
||||
fn call_not_existing_function(wasm_method: WasmExecutionMethod) {
|
||||
let mut ext = TestExternalities::default();
|
||||
let mut ext = ext.ext();
|
||||
|
||||
match call_in_wasm("test_calling_missing_external", &[], wasm_method, &mut ext).unwrap_err() {
|
||||
Error::AbortedDueToTrap(error) => {
|
||||
let expected = match wasm_method {
|
||||
WasmExecutionMethod::Compiled { .. } =>
|
||||
"call to a missing function env:missing_external",
|
||||
};
|
||||
assert_eq!(error.message, expected);
|
||||
},
|
||||
error => panic!("unexpected error: {:?}", error),
|
||||
}
|
||||
}
|
||||
|
||||
test_wasm_execution!(call_yet_another_not_existing_function);
|
||||
fn call_yet_another_not_existing_function(wasm_method: WasmExecutionMethod) {
|
||||
let mut ext = TestExternalities::default();
|
||||
let mut ext = ext.ext();
|
||||
|
||||
match call_in_wasm("test_calling_yet_another_missing_external", &[], wasm_method, &mut ext)
|
||||
.unwrap_err()
|
||||
{
|
||||
Error::AbortedDueToTrap(error) => {
|
||||
let expected = match wasm_method {
|
||||
WasmExecutionMethod::Compiled { .. } =>
|
||||
"call to a missing function env:yet_another_missing_external",
|
||||
};
|
||||
assert_eq!(error.message, expected);
|
||||
},
|
||||
error => panic!("unexpected error: {:?}", error),
|
||||
}
|
||||
}
|
||||
|
||||
test_wasm_execution!(panicking_should_work);
|
||||
fn panicking_should_work(wasm_method: WasmExecutionMethod) {
|
||||
let mut ext = TestExternalities::default();
|
||||
let mut ext = ext.ext();
|
||||
|
||||
let output = call_in_wasm("test_panic", &[], wasm_method, &mut ext);
|
||||
assert!(output.is_err());
|
||||
|
||||
let output = call_in_wasm("test_conditional_panic", &[0], wasm_method, &mut ext);
|
||||
assert_eq!(Decode::decode(&mut &output.unwrap()[..]), Ok(Vec::<u8>::new()));
|
||||
|
||||
let output = call_in_wasm("test_conditional_panic", &vec![2].encode(), wasm_method, &mut ext);
|
||||
assert!(output.is_err());
|
||||
}
|
||||
|
||||
test_wasm_execution!(storage_should_work);
|
||||
fn storage_should_work(wasm_method: WasmExecutionMethod) {
|
||||
let mut ext = TestExternalities::default();
|
||||
// Test value must be bigger than 32 bytes
|
||||
// to test the trie versioning.
|
||||
let value = vec![7u8; 60];
|
||||
|
||||
{
|
||||
let mut ext = ext.ext();
|
||||
ext.set_storage(b"foo".to_vec(), b"bar".to_vec());
|
||||
|
||||
let output = call_in_wasm("test_data_in", &value.encode(), wasm_method, &mut ext).unwrap();
|
||||
|
||||
assert_eq!(output, b"all ok!".to_vec().encode());
|
||||
}
|
||||
|
||||
let mut expected = TestExternalities::new(sp_core::storage::Storage {
|
||||
top: map![
|
||||
b"input".to_vec() => value,
|
||||
b"foo".to_vec() => b"bar".to_vec(),
|
||||
b"baz".to_vec() => b"bar".to_vec()
|
||||
],
|
||||
children_default: map![],
|
||||
});
|
||||
assert!(ext.eq(&mut expected));
|
||||
}
|
||||
|
||||
test_wasm_execution!(clear_prefix_should_work);
|
||||
fn clear_prefix_should_work(wasm_method: WasmExecutionMethod) {
|
||||
let mut ext = TestExternalities::default();
|
||||
{
|
||||
let mut ext = ext.ext();
|
||||
ext.set_storage(b"aaa".to_vec(), b"1".to_vec());
|
||||
ext.set_storage(b"aab".to_vec(), b"2".to_vec());
|
||||
ext.set_storage(b"aba".to_vec(), b"3".to_vec());
|
||||
ext.set_storage(b"abb".to_vec(), b"4".to_vec());
|
||||
ext.set_storage(b"bbb".to_vec(), b"5".to_vec());
|
||||
|
||||
// This will clear all entries which prefix is "ab".
|
||||
let output =
|
||||
call_in_wasm("test_clear_prefix", &b"ab".to_vec().encode(), wasm_method, &mut ext)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(output, b"all ok!".to_vec().encode());
|
||||
}
|
||||
|
||||
let mut expected = TestExternalities::new(sp_core::storage::Storage {
|
||||
top: map![
|
||||
b"aaa".to_vec() => b"1".to_vec(),
|
||||
b"aab".to_vec() => b"2".to_vec(),
|
||||
b"bbb".to_vec() => b"5".to_vec()
|
||||
],
|
||||
children_default: map![],
|
||||
});
|
||||
assert!(expected.eq(&mut ext));
|
||||
}
|
||||
|
||||
test_wasm_execution!(blake2_256_should_work);
|
||||
fn blake2_256_should_work(wasm_method: WasmExecutionMethod) {
|
||||
let mut ext = TestExternalities::default();
|
||||
let mut ext = ext.ext();
|
||||
assert_eq!(
|
||||
call_in_wasm("test_blake2_256", &[0], wasm_method, &mut ext,).unwrap(),
|
||||
blake2_256(b"").to_vec().encode(),
|
||||
);
|
||||
assert_eq!(
|
||||
call_in_wasm("test_blake2_256", &b"Hello world!".to_vec().encode(), wasm_method, &mut ext,)
|
||||
.unwrap(),
|
||||
blake2_256(b"Hello world!").to_vec().encode(),
|
||||
);
|
||||
}
|
||||
|
||||
test_wasm_execution!(blake2_128_should_work);
|
||||
fn blake2_128_should_work(wasm_method: WasmExecutionMethod) {
|
||||
let mut ext = TestExternalities::default();
|
||||
let mut ext = ext.ext();
|
||||
assert_eq!(
|
||||
call_in_wasm("test_blake2_128", &[0], wasm_method, &mut ext,).unwrap(),
|
||||
blake2_128(b"").to_vec().encode(),
|
||||
);
|
||||
assert_eq!(
|
||||
call_in_wasm("test_blake2_128", &b"Hello world!".to_vec().encode(), wasm_method, &mut ext,)
|
||||
.unwrap(),
|
||||
blake2_128(b"Hello world!").to_vec().encode(),
|
||||
);
|
||||
}
|
||||
|
||||
test_wasm_execution!(sha2_256_should_work);
|
||||
fn sha2_256_should_work(wasm_method: WasmExecutionMethod) {
|
||||
let mut ext = TestExternalities::default();
|
||||
let mut ext = ext.ext();
|
||||
assert_eq!(
|
||||
call_in_wasm("test_sha2_256", &[0], wasm_method, &mut ext,).unwrap(),
|
||||
sha2_256(b"").to_vec().encode(),
|
||||
);
|
||||
assert_eq!(
|
||||
call_in_wasm("test_sha2_256", &b"Hello world!".to_vec().encode(), wasm_method, &mut ext,)
|
||||
.unwrap(),
|
||||
sha2_256(b"Hello world!").to_vec().encode(),
|
||||
);
|
||||
}
|
||||
|
||||
test_wasm_execution!(twox_256_should_work);
|
||||
fn twox_256_should_work(wasm_method: WasmExecutionMethod) {
|
||||
let mut ext = TestExternalities::default();
|
||||
let mut ext = ext.ext();
|
||||
assert_eq!(
|
||||
call_in_wasm("test_twox_256", &[0], wasm_method, &mut ext,).unwrap(),
|
||||
twox_256(b"").to_vec().encode()
|
||||
);
|
||||
assert_eq!(
|
||||
call_in_wasm("test_twox_256", &b"Hello world!".to_vec().encode(), wasm_method, &mut ext,)
|
||||
.unwrap(),
|
||||
twox_256(b"Hello world!").to_vec().encode()
|
||||
);
|
||||
}
|
||||
|
||||
test_wasm_execution!(twox_128_should_work);
|
||||
fn twox_128_should_work(wasm_method: WasmExecutionMethod) {
|
||||
let mut ext = TestExternalities::default();
|
||||
let mut ext = ext.ext();
|
||||
assert_eq!(
|
||||
call_in_wasm("test_twox_128", &[0], wasm_method, &mut ext,).unwrap(),
|
||||
twox_128(b"").to_vec().encode()
|
||||
);
|
||||
assert_eq!(
|
||||
call_in_wasm("test_twox_128", &b"Hello world!".to_vec().encode(), wasm_method, &mut ext,)
|
||||
.unwrap(),
|
||||
twox_128(b"Hello world!").to_vec().encode()
|
||||
);
|
||||
}
|
||||
|
||||
test_wasm_execution!(ed25519_verify_should_work);
|
||||
fn ed25519_verify_should_work(wasm_method: WasmExecutionMethod) {
|
||||
let mut ext = TestExternalities::default();
|
||||
let mut ext = ext.ext();
|
||||
let key = ed25519::Pair::from_seed(&blake2_256(b"test"));
|
||||
let sig = key.sign(b"all ok!");
|
||||
let mut calldata = vec![];
|
||||
calldata.extend_from_slice(key.public().as_ref());
|
||||
calldata.extend_from_slice(sig.as_ref());
|
||||
|
||||
assert_eq!(
|
||||
call_in_wasm("test_ed25519_verify", &calldata.encode(), wasm_method, &mut ext,).unwrap(),
|
||||
true.encode(),
|
||||
);
|
||||
|
||||
let other_sig = key.sign(b"all is not ok!");
|
||||
let mut calldata = vec![];
|
||||
calldata.extend_from_slice(key.public().as_ref());
|
||||
calldata.extend_from_slice(other_sig.as_ref());
|
||||
|
||||
assert_eq!(
|
||||
call_in_wasm("test_ed25519_verify", &calldata.encode(), wasm_method, &mut ext,).unwrap(),
|
||||
false.encode(),
|
||||
);
|
||||
}
|
||||
|
||||
test_wasm_execution!(sr25519_verify_should_work);
|
||||
fn sr25519_verify_should_work(wasm_method: WasmExecutionMethod) {
|
||||
let mut ext = TestExternalities::default();
|
||||
let mut ext = ext.ext();
|
||||
let key = sr25519::Pair::from_seed(&blake2_256(b"test"));
|
||||
let sig = key.sign(b"all ok!");
|
||||
let mut calldata = vec![];
|
||||
calldata.extend_from_slice(key.public().as_ref());
|
||||
calldata.extend_from_slice(sig.as_ref());
|
||||
|
||||
assert_eq!(
|
||||
call_in_wasm("test_sr25519_verify", &calldata.encode(), wasm_method, &mut ext,).unwrap(),
|
||||
true.encode(),
|
||||
);
|
||||
|
||||
let other_sig = key.sign(b"all is not ok!");
|
||||
let mut calldata = vec![];
|
||||
calldata.extend_from_slice(key.public().as_ref());
|
||||
calldata.extend_from_slice(other_sig.as_ref());
|
||||
|
||||
assert_eq!(
|
||||
call_in_wasm("test_sr25519_verify", &calldata.encode(), wasm_method, &mut ext,).unwrap(),
|
||||
false.encode(),
|
||||
);
|
||||
}
|
||||
|
||||
test_wasm_execution!(ordered_trie_root_should_work);
|
||||
fn ordered_trie_root_should_work(wasm_method: WasmExecutionMethod) {
|
||||
let mut ext = TestExternalities::default();
|
||||
let trie_input = vec![b"zero".to_vec(), b"one".to_vec(), b"two".to_vec()];
|
||||
assert_eq!(
|
||||
call_in_wasm("test_ordered_trie_root", &[0], wasm_method, &mut ext.ext(),).unwrap(),
|
||||
Layout::<BlakeTwo256>::ordered_trie_root(trie_input.iter()).as_bytes().encode(),
|
||||
);
|
||||
}
|
||||
|
||||
test_wasm_execution!(offchain_index);
|
||||
fn offchain_index(wasm_method: WasmExecutionMethod) {
|
||||
let mut ext = TestExternalities::default();
|
||||
let (offchain, _state) = testing::TestOffchainExt::new();
|
||||
ext.register_extension(OffchainWorkerExt::new(offchain));
|
||||
call_in_wasm("test_offchain_index_set", &[0], wasm_method, &mut ext.ext()).unwrap();
|
||||
|
||||
use sp_core::offchain::OffchainOverlayedChange;
|
||||
let data = ext
|
||||
.overlayed_changes()
|
||||
.clone()
|
||||
.offchain_drain_committed()
|
||||
.find(|(k, _v)| k == &(sp_core::offchain::STORAGE_PREFIX.to_vec(), b"k".to_vec()));
|
||||
assert_eq!(data.map(|data| data.1), Some(OffchainOverlayedChange::SetValue(b"v".to_vec())));
|
||||
}
|
||||
|
||||
test_wasm_execution!(offchain_local_storage_should_work);
|
||||
fn offchain_local_storage_should_work(wasm_method: WasmExecutionMethod) {
|
||||
let mut ext = TestExternalities::default();
|
||||
let (offchain, state) = testing::TestOffchainExt::new();
|
||||
ext.register_extension(OffchainDbExt::new(offchain.clone()));
|
||||
ext.register_extension(OffchainWorkerExt::new(offchain));
|
||||
assert_eq!(
|
||||
call_in_wasm("test_offchain_local_storage", &[0], wasm_method, &mut ext.ext(),).unwrap(),
|
||||
true.encode(),
|
||||
);
|
||||
assert_eq!(state.read().persistent_storage.get(b"test"), Some(vec![]));
|
||||
}
|
||||
|
||||
test_wasm_execution!(offchain_http_should_work);
|
||||
fn offchain_http_should_work(wasm_method: WasmExecutionMethod) {
|
||||
let mut ext = TestExternalities::default();
|
||||
let (offchain, state) = testing::TestOffchainExt::new();
|
||||
ext.register_extension(OffchainWorkerExt::new(offchain));
|
||||
state.write().expect_request(testing::PendingRequest {
|
||||
method: "POST".into(),
|
||||
uri: "http://localhost:12345".into(),
|
||||
body: vec![1, 2, 3, 4],
|
||||
headers: vec![("X-Auth".to_owned(), "test".to_owned())],
|
||||
sent: true,
|
||||
response: Some(vec![1, 2, 3]),
|
||||
response_headers: vec![("X-Auth".to_owned(), "hello".to_owned())],
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
call_in_wasm("test_offchain_http", &[0], wasm_method, &mut ext.ext(),).unwrap(),
|
||||
true.encode(),
|
||||
);
|
||||
}
|
||||
|
||||
test_wasm_execution!(should_trap_when_heap_exhausted);
|
||||
fn should_trap_when_heap_exhausted(wasm_method: WasmExecutionMethod) {
|
||||
let mut ext = TestExternalities::default();
|
||||
|
||||
let executor = crate::WasmExecutor::<HostFunctions>::builder()
|
||||
.with_execution_method(wasm_method)
|
||||
// `17` is the initial number of pages compiled into the binary.
|
||||
.with_onchain_heap_alloc_strategy(HeapAllocStrategy::Static { extra_pages: 17 })
|
||||
.build();
|
||||
|
||||
let err = executor
|
||||
.uncached_call(
|
||||
RuntimeBlob::uncompress_if_needed(wasm_binary_unwrap()).unwrap(),
|
||||
&mut ext.ext(),
|
||||
true,
|
||||
"test_allocate_vec",
|
||||
&16777216_u32.encode(),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
match err {
|
||||
Error::AbortedDueToTrap(error)
|
||||
if matches!(wasm_method, WasmExecutionMethod::Compiled { .. }) =>
|
||||
{
|
||||
assert_eq!(
|
||||
error.message,
|
||||
r#"host code panicked while being called by the runtime: Failed to allocate memory: "Allocator ran out of space""#
|
||||
);
|
||||
},
|
||||
error => panic!("unexpected error: {:?}", error),
|
||||
}
|
||||
}
|
||||
|
||||
fn mk_test_runtime(
|
||||
wasm_method: WasmExecutionMethod,
|
||||
pages: HeapAllocStrategy,
|
||||
) -> Box<dyn WasmModule> {
|
||||
let blob = RuntimeBlob::uncompress_if_needed(wasm_binary_unwrap())
|
||||
.expect("failed to create a runtime blob out of test runtime");
|
||||
|
||||
crate::wasm_runtime::create_wasm_runtime_with_code::<HostFunctions>(
|
||||
wasm_method,
|
||||
pages,
|
||||
blob,
|
||||
true,
|
||||
None,
|
||||
)
|
||||
.expect("failed to instantiate wasm runtime")
|
||||
}
|
||||
|
||||
test_wasm_execution!(returns_mutable_static);
|
||||
fn returns_mutable_static(wasm_method: WasmExecutionMethod) {
|
||||
let runtime =
|
||||
mk_test_runtime(wasm_method, HeapAllocStrategy::Dynamic { maximum_pages: Some(1024) });
|
||||
|
||||
let mut instance = runtime.new_instance().unwrap();
|
||||
let res = instance.call_export("returns_mutable_static", &[0]).unwrap();
|
||||
assert_eq!(33, u64::decode(&mut &res[..]).unwrap());
|
||||
|
||||
// We expect that every invocation will need to return the initial
|
||||
// value plus one. If the value increases more than that then it is
|
||||
// a sign that the wasm runtime preserves the memory content.
|
||||
let res = instance.call_export("returns_mutable_static", &[0]).unwrap();
|
||||
assert_eq!(33, u64::decode(&mut &res[..]).unwrap());
|
||||
}
|
||||
|
||||
test_wasm_execution!(returns_mutable_static_bss);
|
||||
fn returns_mutable_static_bss(wasm_method: WasmExecutionMethod) {
|
||||
let runtime =
|
||||
mk_test_runtime(wasm_method, HeapAllocStrategy::Dynamic { maximum_pages: Some(1024) });
|
||||
|
||||
let mut instance = runtime.new_instance().unwrap();
|
||||
let res = instance.call_export("returns_mutable_static_bss", &[0]).unwrap();
|
||||
assert_eq!(1, u64::decode(&mut &res[..]).unwrap());
|
||||
|
||||
// We expect that every invocation will need to return the initial
|
||||
// value plus one. If the value increases more than that then it is
|
||||
// a sign that the wasm runtime preserves the memory content.
|
||||
let res = instance.call_export("returns_mutable_static_bss", &[0]).unwrap();
|
||||
assert_eq!(1, u64::decode(&mut &res[..]).unwrap());
|
||||
}
|
||||
|
||||
// If we didn't restore the wasm instance properly, on a trap the stack pointer would not be
|
||||
// returned to its initial value and thus the stack space is going to be leaked.
|
||||
//
|
||||
// See https://github.com/paritytech/substrate/issues/2967 for details
|
||||
test_wasm_execution!(restoration_of_globals);
|
||||
fn restoration_of_globals(wasm_method: WasmExecutionMethod) {
|
||||
// Allocate 32 pages (of 65536 bytes) which gives the runtime 2048KB of heap to operate on
|
||||
// (plus some additional space unused from the initial pages requested by the wasm runtime
|
||||
// module).
|
||||
//
|
||||
// The fixture performs 2 allocations of 768KB and this theoretically gives 1536KB, however, due
|
||||
// to our allocator algorithm there are inefficiencies.
|
||||
const REQUIRED_MEMORY_PAGES: u32 = 32;
|
||||
|
||||
let runtime = mk_test_runtime(
|
||||
wasm_method,
|
||||
HeapAllocStrategy::Static { extra_pages: REQUIRED_MEMORY_PAGES },
|
||||
);
|
||||
let mut instance = runtime.new_instance().unwrap();
|
||||
|
||||
// On the first invocation we allocate approx. 768KB (75%) of stack and then trap.
|
||||
let res = instance.call_export("allocates_huge_stack_array", &true.encode());
|
||||
assert!(res.is_err());
|
||||
|
||||
// On the second invocation we allocate yet another 768KB (75%) of stack
|
||||
let res = instance.call_export("allocates_huge_stack_array", &false.encode());
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
test_wasm_execution!(parallel_execution);
|
||||
fn parallel_execution(wasm_method: WasmExecutionMethod) {
|
||||
let executor = Arc::new(
|
||||
crate::WasmExecutor::<HostFunctions>::builder()
|
||||
.with_execution_method(wasm_method)
|
||||
.build(),
|
||||
);
|
||||
let threads: Vec<_> = (0..8)
|
||||
.map(|_| {
|
||||
let executor = executor.clone();
|
||||
std::thread::spawn(move || {
|
||||
let mut ext = TestExternalities::default();
|
||||
let mut ext = ext.ext();
|
||||
assert_eq!(
|
||||
executor
|
||||
.uncached_call(
|
||||
RuntimeBlob::uncompress_if_needed(wasm_binary_unwrap()).unwrap(),
|
||||
&mut ext,
|
||||
true,
|
||||
"test_twox_128",
|
||||
&[0],
|
||||
)
|
||||
.unwrap(),
|
||||
array_bytes::hex2bytes_unchecked("99e9d85137db46ef4bbea33613baafd5").encode()
|
||||
);
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
for t in threads.into_iter() {
|
||||
t.join().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
test_wasm_execution!(wasm_tracing_should_work);
|
||||
fn wasm_tracing_should_work(wasm_method: WasmExecutionMethod) {
|
||||
use sc_tracing::{SpanDatum, TraceEvent};
|
||||
use std::sync::Mutex;
|
||||
|
||||
struct TestTraceHandler(Arc<Mutex<Vec<SpanDatum>>>);
|
||||
|
||||
impl sc_tracing::TraceHandler for TestTraceHandler {
|
||||
fn handle_span(&self, sd: &SpanDatum) {
|
||||
self.0.lock().unwrap().push(sd.clone());
|
||||
}
|
||||
|
||||
fn handle_event(&self, _event: &TraceEvent) {}
|
||||
}
|
||||
|
||||
let traces = Arc::new(Mutex::new(Vec::new()));
|
||||
let handler = TestTraceHandler(traces.clone());
|
||||
|
||||
// Create subscriber with wasm_tracing disabled
|
||||
let test_subscriber = tracing_subscriber::fmt()
|
||||
.finish()
|
||||
.with(sc_tracing::ProfilingLayer::new_with_handler(Box::new(handler), "default"));
|
||||
|
||||
let _guard = tracing::subscriber::set_default(test_subscriber);
|
||||
|
||||
let mut ext = TestExternalities::default();
|
||||
let mut ext = ext.ext();
|
||||
|
||||
let span_id =
|
||||
call_in_wasm("test_enter_span", Default::default(), wasm_method, &mut ext).unwrap();
|
||||
|
||||
let span_id = u64::decode(&mut &span_id[..]).unwrap();
|
||||
|
||||
assert!(span_id > 0);
|
||||
|
||||
call_in_wasm("test_exit_span", &span_id.encode(), wasm_method, &mut ext).unwrap();
|
||||
|
||||
// Check there is only the single trace
|
||||
let len = traces.lock().unwrap().len();
|
||||
assert_eq!(len, 1);
|
||||
|
||||
let span_datum = traces.lock().unwrap().pop().unwrap();
|
||||
let values = span_datum.values;
|
||||
assert_eq!(span_datum.target, "default");
|
||||
assert_eq!(span_datum.name, "");
|
||||
assert_eq!(values.bool_values.get("wasm").unwrap(), &true);
|
||||
|
||||
call_in_wasm("test_nested_spans", Default::default(), wasm_method, &mut ext).unwrap();
|
||||
let len = traces.lock().unwrap().len();
|
||||
assert_eq!(len, 2);
|
||||
}
|
||||
|
||||
test_wasm_execution!(allocate_two_gigabyte);
|
||||
fn allocate_two_gigabyte(wasm_method: WasmExecutionMethod) {
|
||||
let runtime = mk_test_runtime(wasm_method, HeapAllocStrategy::Dynamic { maximum_pages: None });
|
||||
|
||||
let mut instance = runtime.new_instance().unwrap();
|
||||
let res = instance.call_export("allocate_two_gigabyte", &[0]).unwrap();
|
||||
assert_eq!(10 * 1024 * 1024 * 205, u32::decode(&mut &res[..]).unwrap());
|
||||
}
|
||||
|
||||
test_wasm_execution!(memory_is_cleared_between_invocations);
|
||||
fn memory_is_cleared_between_invocations(wasm_method: WasmExecutionMethod) {
|
||||
// This is based on the code generated by compiling a runtime *without*
|
||||
// the `-C link-arg=--import-memory` using the following code and then
|
||||
// disassembling the resulting blob with `wasm-dis`:
|
||||
//
|
||||
// ```
|
||||
// #[no_mangle]
|
||||
// #[cfg(not(feature = "std"))]
|
||||
// pub fn returns_no_bss_mutable_static(_: *mut u8, _: usize) -> u64 {
|
||||
// static mut COUNTER: usize = 0;
|
||||
// let output = unsafe {
|
||||
// COUNTER += 1;
|
||||
// COUNTER as u64
|
||||
// };
|
||||
// sp_core::to_substrate_wasm_fn_return_value(&output)
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// This results in the BSS section to *not* be emitted, hence the executor has no way
|
||||
// of knowing about the `static` variable's existence, so this test will fail if the linear
|
||||
// memory is not properly cleared between invocations.
|
||||
let binary = wat::parse_str(r#"
|
||||
(module
|
||||
(type $i32_=>_i32 (func (param i32) (result i32)))
|
||||
(type $i32_i32_=>_i64 (func (param i32 i32) (result i64)))
|
||||
(import "env" "ext_allocator_malloc_version_1" (func $ext_allocator_malloc_version_1 (param i32) (result i32)))
|
||||
(global $__stack_pointer (mut i32) (i32.const 1048576))
|
||||
(global $global$1 i32 (i32.const 1048580))
|
||||
(global $global$2 i32 (i32.const 1048592))
|
||||
(memory $0 17)
|
||||
(export "memory" (memory $0))
|
||||
(export "returns_no_bss_mutable_static" (func $returns_no_bss_mutable_static))
|
||||
(export "__data_end" (global $global$1))
|
||||
(export "__heap_base" (global $global$2))
|
||||
(func $returns_no_bss_mutable_static (param $0 i32) (param $1 i32) (result i64)
|
||||
(local $2 i32)
|
||||
(local $3 i32)
|
||||
(i32.store offset=1048576
|
||||
(i32.const 0)
|
||||
(local.tee $2
|
||||
(i32.add
|
||||
(i32.load offset=1048576 (i32.const 0))
|
||||
(i32.const 1)
|
||||
)
|
||||
)
|
||||
)
|
||||
(i64.store
|
||||
(local.tee $3
|
||||
(call $ext_allocator_malloc_version_1 (i32.const 8))
|
||||
)
|
||||
(i64.extend_i32_u (local.get $2))
|
||||
)
|
||||
(i64.or
|
||||
(i64.extend_i32_u (local.get $3))
|
||||
(i64.const 34359738368)
|
||||
)
|
||||
)
|
||||
)"#).unwrap();
|
||||
|
||||
let runtime = crate::wasm_runtime::create_wasm_runtime_with_code::<HostFunctions>(
|
||||
wasm_method,
|
||||
HeapAllocStrategy::Dynamic { maximum_pages: Some(1024) },
|
||||
RuntimeBlob::uncompress_if_needed(&binary[..]).unwrap(),
|
||||
true,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut instance = runtime.new_instance().unwrap();
|
||||
let res = instance.call_export("returns_no_bss_mutable_static", &[0]).unwrap();
|
||||
assert_eq!(1, u64::decode(&mut &res[..]).unwrap());
|
||||
|
||||
let res = instance.call_export("returns_no_bss_mutable_static", &[0]).unwrap();
|
||||
assert_eq!(1, u64::decode(&mut &res[..]).unwrap());
|
||||
}
|
||||
|
||||
test_wasm_execution!(return_i8);
|
||||
fn return_i8(wasm_method: WasmExecutionMethod) {
|
||||
let mut ext = TestExternalities::default();
|
||||
let mut ext = ext.ext();
|
||||
|
||||
assert_eq!(
|
||||
call_in_wasm("test_return_i8", &[], wasm_method, &mut ext).unwrap(),
|
||||
(-66_i8).encode()
|
||||
);
|
||||
}
|
||||
|
||||
test_wasm_execution!(take_i8);
|
||||
fn take_i8(wasm_method: WasmExecutionMethod) {
|
||||
let mut ext = TestExternalities::default();
|
||||
let mut ext = ext.ext();
|
||||
|
||||
call_in_wasm("test_take_i8", &(-66_i8).encode(), wasm_method, &mut ext).unwrap();
|
||||
}
|
||||
|
||||
test_wasm_execution!(abort_on_panic);
|
||||
fn abort_on_panic(wasm_method: WasmExecutionMethod) {
|
||||
let mut ext = TestExternalities::default();
|
||||
let mut ext = ext.ext();
|
||||
|
||||
match call_in_wasm("test_abort_on_panic", &[], wasm_method, &mut ext).unwrap_err() {
|
||||
Error::AbortedDueToPanic(error) => assert_eq!(error.message, "test_abort_on_panic called"),
|
||||
error => panic!("unexpected error: {:?}", error),
|
||||
}
|
||||
}
|
||||
|
||||
test_wasm_execution!(unreachable_intrinsic);
|
||||
fn unreachable_intrinsic(wasm_method: WasmExecutionMethod) {
|
||||
let mut ext = TestExternalities::default();
|
||||
let mut ext = ext.ext();
|
||||
|
||||
match call_in_wasm("test_unreachable_intrinsic", &[], wasm_method, &mut ext).unwrap_err() {
|
||||
Error::AbortedDueToTrap(error) => {
|
||||
let expected = match wasm_method {
|
||||
WasmExecutionMethod::Compiled { .. } =>
|
||||
"wasm trap: wasm `unreachable` instruction executed",
|
||||
};
|
||||
assert_eq!(error.message, expected);
|
||||
},
|
||||
error => panic!("unexpected error: {:?}", error),
|
||||
}
|
||||
}
|
||||
|
||||
test_wasm_execution!(return_value);
|
||||
fn return_value(wasm_method: WasmExecutionMethod) {
|
||||
let mut ext = TestExternalities::default();
|
||||
let mut ext = ext.ext();
|
||||
|
||||
assert_eq!(
|
||||
call_in_wasm("test_return_value", &[], wasm_method, &mut ext).unwrap(),
|
||||
(1234u64).encode()
|
||||
);
|
||||
}
|
||||
|
||||
test_wasm_execution!(return_huge_len);
|
||||
fn return_huge_len(wasm_method: WasmExecutionMethod) {
|
||||
let mut ext = TestExternalities::default();
|
||||
let mut ext = ext.ext();
|
||||
|
||||
match call_in_wasm("test_return_huge_len", &[], wasm_method, &mut ext).unwrap_err() {
|
||||
Error::OutputExceedsBounds => {
|
||||
assert_matches!(wasm_method, WasmExecutionMethod::Compiled { .. });
|
||||
},
|
||||
error => panic!("unexpected error: {:?}", error),
|
||||
}
|
||||
}
|
||||
|
||||
test_wasm_execution!(return_max_memory_offset);
|
||||
fn return_max_memory_offset(wasm_method: WasmExecutionMethod) {
|
||||
let mut ext = TestExternalities::default();
|
||||
let mut ext = ext.ext();
|
||||
|
||||
assert_eq!(
|
||||
call_in_wasm("test_return_max_memory_offset", &[], wasm_method, &mut ext).unwrap(),
|
||||
(u8::MAX).encode()
|
||||
);
|
||||
}
|
||||
|
||||
test_wasm_execution!(return_max_memory_offset_plus_one);
|
||||
fn return_max_memory_offset_plus_one(wasm_method: WasmExecutionMethod) {
|
||||
let mut ext = TestExternalities::default();
|
||||
let mut ext = ext.ext();
|
||||
|
||||
match call_in_wasm("test_return_max_memory_offset_plus_one", &[], wasm_method, &mut ext)
|
||||
.unwrap_err()
|
||||
{
|
||||
Error::OutputExceedsBounds => {
|
||||
assert_matches!(wasm_method, WasmExecutionMethod::Compiled { .. });
|
||||
},
|
||||
error => panic!("unexpected error: {:?}", error),
|
||||
}
|
||||
}
|
||||
|
||||
test_wasm_execution!(return_overflow);
|
||||
fn return_overflow(wasm_method: WasmExecutionMethod) {
|
||||
let mut ext = TestExternalities::default();
|
||||
let mut ext = ext.ext();
|
||||
|
||||
match call_in_wasm("test_return_overflow", &[], wasm_method, &mut ext).unwrap_err() {
|
||||
Error::OutputExceedsBounds => {
|
||||
assert_matches!(wasm_method, WasmExecutionMethod::Compiled { .. });
|
||||
},
|
||||
error => panic!("unexpected error: {:?}", error),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! A crate that provides means of executing/dispatching calls into the runtime.
|
||||
//!
|
||||
//! There are a few responsibilities of this crate at the moment:
|
||||
//!
|
||||
//! - It provides an implementation of a common entrypoint for calling into the runtime, both
|
||||
//! wasm and compiled.
|
||||
//! - It defines the environment for the wasm execution, namely the host functions that are to be
|
||||
//! provided into the wasm runtime module.
|
||||
//! - It also provides the required infrastructure for executing the current wasm runtime (specified
|
||||
//! by the current value of `:code` in the provided externalities), i.e. interfacing with
|
||||
//! wasm engine used, instance cache.
|
||||
|
||||
#![warn(missing_docs)]
|
||||
|
||||
#[macro_use]
|
||||
mod executor;
|
||||
#[cfg(test)]
|
||||
mod integration_tests;
|
||||
mod wasm_runtime;
|
||||
|
||||
pub use codec::Codec;
|
||||
#[allow(deprecated)]
|
||||
pub use executor::NativeElseWasmExecutor;
|
||||
pub use executor::{with_externalities_safe, NativeExecutionDispatch, WasmExecutor};
|
||||
#[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,
|
||||
wasm_runtime::{HeapAllocStrategy, DEFAULT_HEAP_ALLOC_PAGES, DEFAULT_HEAP_ALLOC_STRATEGY},
|
||||
};
|
||||
pub use sc_executor_wasmtime::InstantiationStrategy as WasmtimeInstantiationStrategy;
|
||||
|
||||
/// Extracts the runtime version of a given runtime code.
|
||||
pub trait RuntimeVersionOf {
|
||||
/// Extract [`RuntimeVersion`] of the given `runtime_code`.
|
||||
fn runtime_version(
|
||||
&self,
|
||||
ext: &mut dyn Externalities,
|
||||
runtime_code: &sp_core::traits::RuntimeCode,
|
||||
) -> error::Result<RuntimeVersion>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use sc_executor_common::runtime_blob::RuntimeBlob;
|
||||
use sc_runtime_test::wasm_binary_unwrap;
|
||||
use sp_io::TestExternalities;
|
||||
|
||||
#[test]
|
||||
fn call_in_interpreted_wasm_works() {
|
||||
let mut ext = TestExternalities::default();
|
||||
let mut ext = ext.ext();
|
||||
|
||||
let executor = WasmExecutor::<sp_io::SubstrateHostFunctions>::builder().build();
|
||||
let res = executor
|
||||
.uncached_call(
|
||||
RuntimeBlob::uncompress_if_needed(wasm_binary_unwrap()).unwrap(),
|
||||
&mut ext,
|
||||
true,
|
||||
"test_empty_return",
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(res, vec![0u8; 0]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,564 @@
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Traits and accessor functions for calling into the Substrate Wasm runtime.
|
||||
//!
|
||||
//! The primary means of accessing the runtimes is through a cache which saves the reusable
|
||||
//! components of the runtime that are expensive to initialize.
|
||||
|
||||
use crate::error::{Error, WasmError};
|
||||
|
||||
use codec::Decode;
|
||||
use parking_lot::Mutex;
|
||||
use sc_executor_common::{
|
||||
runtime_blob::RuntimeBlob,
|
||||
wasm_runtime::{HeapAllocStrategy, WasmInstance, WasmModule},
|
||||
};
|
||||
use schnellru::{ByLength, LruMap};
|
||||
use sp_core::traits::{Externalities, FetchRuntimeCode, RuntimeCode};
|
||||
use sp_version::RuntimeVersion;
|
||||
use sp_wasm_interface::HostFunctions;
|
||||
|
||||
use std::{
|
||||
panic::AssertUnwindSafe,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
/// Specification of different methods of executing the runtime Wasm code.
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
|
||||
pub enum WasmExecutionMethod {
|
||||
/// Uses the Wasmtime compiled runtime.
|
||||
Compiled {
|
||||
/// The instantiation strategy to use.
|
||||
instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for WasmExecutionMethod {
|
||||
fn default() -> Self {
|
||||
Self::Compiled {
|
||||
instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::PoolingCopyOnWrite,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
|
||||
struct VersionedRuntimeId {
|
||||
/// Runtime code hash.
|
||||
code_hash: Vec<u8>,
|
||||
/// Wasm runtime type.
|
||||
wasm_method: WasmExecutionMethod,
|
||||
/// The heap allocation strategy this runtime was created with.
|
||||
heap_alloc_strategy: HeapAllocStrategy,
|
||||
}
|
||||
|
||||
/// A Wasm runtime object along with its cached runtime version.
|
||||
struct VersionedRuntime {
|
||||
/// Shared runtime that can spawn instances.
|
||||
module: Box<dyn WasmModule>,
|
||||
/// Runtime version according to `Core_version` if any.
|
||||
version: Option<RuntimeVersion>,
|
||||
|
||||
// TODO: Remove this once the legacy instance reuse instantiation strategy
|
||||
// for `wasmtime` is gone, as this only makes sense with that particular strategy.
|
||||
/// Cached instance pool.
|
||||
instances: Vec<Mutex<Option<Box<dyn WasmInstance>>>>,
|
||||
}
|
||||
|
||||
impl VersionedRuntime {
|
||||
/// Run the given closure `f` with an instance of this runtime.
|
||||
fn with_instance<R, F>(&self, ext: &mut dyn Externalities, f: F) -> Result<R, Error>
|
||||
where
|
||||
F: FnOnce(
|
||||
&dyn WasmModule,
|
||||
&mut dyn WasmInstance,
|
||||
Option<&RuntimeVersion>,
|
||||
&mut dyn Externalities,
|
||||
) -> Result<R, Error>,
|
||||
{
|
||||
// Find a free instance
|
||||
let instance = self
|
||||
.instances
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(index, i)| i.try_lock().map(|i| (index, i)));
|
||||
|
||||
match instance {
|
||||
Some((index, mut locked)) => {
|
||||
let (mut instance, new_inst) = locked
|
||||
.take()
|
||||
.map(|r| Ok((r, false)))
|
||||
.unwrap_or_else(|| self.module.new_instance().map(|i| (i, true)))?;
|
||||
|
||||
let result = f(&*self.module, &mut *instance, self.version.as_ref(), ext);
|
||||
if let Err(e) = &result {
|
||||
if new_inst {
|
||||
tracing::warn!(
|
||||
target: "wasm-runtime",
|
||||
error = %e,
|
||||
"Fresh runtime instance failed",
|
||||
)
|
||||
} else {
|
||||
tracing::warn!(
|
||||
target: "wasm-runtime",
|
||||
error = %e,
|
||||
"Evicting failed runtime instance",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
*locked = Some(instance);
|
||||
|
||||
if new_inst {
|
||||
tracing::debug!(
|
||||
target: "wasm-runtime",
|
||||
"Allocated WASM instance {}/{}",
|
||||
index + 1,
|
||||
self.instances.len(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
},
|
||||
None => {
|
||||
tracing::warn!(target: "wasm-runtime", "Ran out of free WASM instances");
|
||||
|
||||
// Allocate a new instance
|
||||
let mut instance = self.module.new_instance()?;
|
||||
|
||||
f(&*self.module, &mut *instance, self.version.as_ref(), ext)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cache for the runtimes.
|
||||
///
|
||||
/// When an instance is requested for the first time it is added to this cache. Metadata is kept
|
||||
/// with the instance so that it can be efficiently reinitialized.
|
||||
///
|
||||
/// When using the Wasmi interpreter execution method, the metadata includes the initial memory and
|
||||
/// values of mutable globals. Follow-up requests to fetch a runtime return this one instance with
|
||||
/// the memory reset to the initial memory. So, one runtime instance is reused for every fetch
|
||||
/// request.
|
||||
///
|
||||
/// The size of cache is configurable via the cli option `--runtime-cache-size`.
|
||||
pub struct RuntimeCache {
|
||||
/// A cache of runtimes along with metadata.
|
||||
///
|
||||
/// Runtimes sorted by recent usage. The most recently used is at the front.
|
||||
runtimes: Mutex<LruMap<VersionedRuntimeId, Arc<VersionedRuntime>>>,
|
||||
/// The size of the instances cache for each runtime.
|
||||
max_runtime_instances: usize,
|
||||
cache_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl RuntimeCache {
|
||||
/// Creates a new instance of a runtimes cache.
|
||||
///
|
||||
/// `max_runtime_instances` specifies the number of instances per runtime preserved in an
|
||||
/// in-memory cache.
|
||||
///
|
||||
/// `cache_path` allows to specify an optional directory where the executor can store files
|
||||
/// for caching.
|
||||
///
|
||||
/// `runtime_cache_size` specifies the number of different runtimes versions preserved in an
|
||||
/// in-memory cache, must always be at least 1.
|
||||
pub fn new(
|
||||
max_runtime_instances: usize,
|
||||
cache_path: Option<PathBuf>,
|
||||
runtime_cache_size: u8,
|
||||
) -> RuntimeCache {
|
||||
let cap = ByLength::new(runtime_cache_size.max(1) as u32);
|
||||
RuntimeCache { runtimes: Mutex::new(LruMap::new(cap)), max_runtime_instances, cache_path }
|
||||
}
|
||||
|
||||
/// Prepares a WASM module instance and executes given function for it.
|
||||
///
|
||||
/// This uses internal cache to find available instance or create a new one.
|
||||
/// # Parameters
|
||||
///
|
||||
/// `runtime_code` - The runtime wasm code used setup the runtime.
|
||||
///
|
||||
/// `ext` - The externalities to access the state.
|
||||
///
|
||||
/// `wasm_method` - Type of WASM backend to use.
|
||||
///
|
||||
/// `heap_alloc_strategy` - The heap allocation strategy to use.
|
||||
///
|
||||
/// `allow_missing_func_imports` - Ignore missing function imports.
|
||||
///
|
||||
/// `f` - Function to execute.
|
||||
///
|
||||
/// `H` - A compile-time list of host functions to expose to the runtime.
|
||||
///
|
||||
/// # Returns result of `f` wrapped in an additional result.
|
||||
/// In case of failure one of two errors can be returned:
|
||||
///
|
||||
/// `Err::RuntimeConstruction` is returned for runtime construction issues.
|
||||
///
|
||||
/// `Error::InvalidMemoryReference` is returned if no memory export with the
|
||||
/// identifier `memory` can be found in the runtime.
|
||||
pub fn with_instance<'c, H, R, F>(
|
||||
&self,
|
||||
runtime_code: &'c RuntimeCode<'c>,
|
||||
ext: &mut dyn Externalities,
|
||||
wasm_method: WasmExecutionMethod,
|
||||
heap_alloc_strategy: HeapAllocStrategy,
|
||||
allow_missing_func_imports: bool,
|
||||
f: F,
|
||||
) -> Result<Result<R, Error>, Error>
|
||||
where
|
||||
H: HostFunctions,
|
||||
F: FnOnce(
|
||||
&dyn WasmModule,
|
||||
&mut dyn WasmInstance,
|
||||
Option<&RuntimeVersion>,
|
||||
&mut dyn Externalities,
|
||||
) -> Result<R, Error>,
|
||||
{
|
||||
let code_hash = &runtime_code.hash;
|
||||
|
||||
let versioned_runtime_id =
|
||||
VersionedRuntimeId { code_hash: code_hash.clone(), heap_alloc_strategy, wasm_method };
|
||||
|
||||
let mut runtimes = self.runtimes.lock(); // this must be released prior to calling f
|
||||
let versioned_runtime = if let Some(versioned_runtime) = runtimes.get(&versioned_runtime_id)
|
||||
{
|
||||
versioned_runtime.clone()
|
||||
} else {
|
||||
let code = runtime_code.fetch_runtime_code().ok_or(WasmError::CodeNotFound)?;
|
||||
|
||||
let time = std::time::Instant::now();
|
||||
|
||||
let result = create_versioned_wasm_runtime::<H>(
|
||||
&code,
|
||||
ext,
|
||||
wasm_method,
|
||||
heap_alloc_strategy,
|
||||
allow_missing_func_imports,
|
||||
self.max_runtime_instances,
|
||||
self.cache_path.as_deref(),
|
||||
);
|
||||
|
||||
match result {
|
||||
Ok(ref result) => {
|
||||
tracing::debug!(
|
||||
target: "wasm-runtime",
|
||||
"Prepared new runtime version {:?} in {} ms.",
|
||||
result.version,
|
||||
time.elapsed().as_millis(),
|
||||
);
|
||||
},
|
||||
Err(ref err) => {
|
||||
tracing::warn!(target: "wasm-runtime", error = ?err, "Cannot create a runtime");
|
||||
},
|
||||
}
|
||||
|
||||
let versioned_runtime = Arc::new(result?);
|
||||
|
||||
// Save new versioned wasm runtime in cache
|
||||
runtimes.insert(versioned_runtime_id, versioned_runtime.clone());
|
||||
|
||||
versioned_runtime
|
||||
};
|
||||
|
||||
// Lock must be released prior to calling f
|
||||
drop(runtimes);
|
||||
|
||||
Ok(versioned_runtime.with_instance(ext, f))
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a wasm runtime with the given `code`.
|
||||
pub fn create_wasm_runtime_with_code<H>(
|
||||
wasm_method: WasmExecutionMethod,
|
||||
heap_alloc_strategy: HeapAllocStrategy,
|
||||
blob: RuntimeBlob,
|
||||
allow_missing_func_imports: bool,
|
||||
cache_path: Option<&Path>,
|
||||
) -> Result<Box<dyn WasmModule>, WasmError>
|
||||
where
|
||||
H: HostFunctions,
|
||||
{
|
||||
if let Some(blob) = blob.as_polkavm_blob() {
|
||||
return sc_executor_polkavm::create_runtime::<H>(blob);
|
||||
}
|
||||
|
||||
match wasm_method {
|
||||
WasmExecutionMethod::Compiled { instantiation_strategy } =>
|
||||
sc_executor_wasmtime::create_runtime::<H>(
|
||||
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,
|
||||
},
|
||||
},
|
||||
)
|
||||
.map(|runtime| -> Box<dyn WasmModule> { Box::new(runtime) }),
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_version(mut version: &[u8]) -> Result<RuntimeVersion, WasmError> {
|
||||
Decode::decode(&mut version).map_err(|_| {
|
||||
WasmError::Instantiation(
|
||||
"failed to decode \"Core_version\" result using old runtime version".into(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn decode_runtime_apis(apis: &[u8]) -> Result<Vec<([u8; 8], u32)>, WasmError> {
|
||||
use sp_api::RUNTIME_API_INFO_SIZE;
|
||||
|
||||
apis.chunks(RUNTIME_API_INFO_SIZE)
|
||||
.map(|chunk| {
|
||||
// `chunk` can be less than `RUNTIME_API_INFO_SIZE` if the total length of `apis`
|
||||
// doesn't completely divide by `RUNTIME_API_INFO_SIZE`.
|
||||
<[u8; RUNTIME_API_INFO_SIZE]>::try_from(chunk)
|
||||
.map(sp_api::deserialize_runtime_api_info)
|
||||
.map_err(|_| WasmError::Other("a clipped runtime api info declaration".to_owned()))
|
||||
})
|
||||
.collect::<Result<Vec<_>, WasmError>>()
|
||||
}
|
||||
|
||||
/// Take the runtime blob and scan it for the custom wasm sections containing the version
|
||||
/// information and construct the `RuntimeVersion` from them.
|
||||
///
|
||||
/// If there are no such sections, it returns `None`. If there is an error during decoding those
|
||||
/// sections, `Err` will be returned.
|
||||
pub fn read_embedded_version(blob: &RuntimeBlob) -> Result<Option<RuntimeVersion>, WasmError> {
|
||||
if let Some(mut version_section) = blob.custom_section_contents("runtime_version") {
|
||||
let apis = blob
|
||||
.custom_section_contents("runtime_apis")
|
||||
.map(decode_runtime_apis)
|
||||
.transpose()?
|
||||
.map(Into::into);
|
||||
|
||||
let core_version = apis.as_ref().and_then(sp_version::core_version_from_apis);
|
||||
// We do not use `RuntimeVersion::decode` here because that `decode_version` relies on
|
||||
// presence of a special API in the `apis` field to treat the input as a non-legacy version.
|
||||
// However the structure found in the `runtime_version` always contain an empty `apis`
|
||||
// field. Therefore the version read will be mistakenly treated as an legacy one.
|
||||
let mut decoded_version = sp_version::RuntimeVersion::decode_with_version_hint(
|
||||
&mut version_section,
|
||||
core_version,
|
||||
)
|
||||
.map_err(|_| WasmError::Instantiation("failed to decode version section".into()))?;
|
||||
|
||||
if let Some(apis) = apis {
|
||||
decoded_version.apis = apis;
|
||||
}
|
||||
|
||||
Ok(Some(decoded_version))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn create_versioned_wasm_runtime<H>(
|
||||
code: &[u8],
|
||||
ext: &mut dyn Externalities,
|
||||
wasm_method: WasmExecutionMethod,
|
||||
heap_alloc_strategy: HeapAllocStrategy,
|
||||
allow_missing_func_imports: bool,
|
||||
max_instances: usize,
|
||||
cache_path: Option<&Path>,
|
||||
) -> Result<VersionedRuntime, WasmError>
|
||||
where
|
||||
H: HostFunctions,
|
||||
{
|
||||
// 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)?;
|
||||
|
||||
// Use the runtime blob to scan if there is any metadata embedded into the wasm binary
|
||||
// pertaining to runtime version. We do it before consuming the runtime blob for creating the
|
||||
// runtime.
|
||||
let mut version = read_embedded_version(&blob)?;
|
||||
|
||||
let runtime = create_wasm_runtime_with_code::<H>(
|
||||
wasm_method,
|
||||
heap_alloc_strategy,
|
||||
blob,
|
||||
allow_missing_func_imports,
|
||||
cache_path,
|
||||
)?;
|
||||
|
||||
// If the runtime blob doesn't embed the runtime version then use the legacy version query
|
||||
// mechanism: call the runtime.
|
||||
if version.is_none() {
|
||||
// Call to determine runtime version.
|
||||
let version_result = {
|
||||
// `ext` is already implicitly handled as unwind safe, as we store it in a global
|
||||
// variable.
|
||||
let mut ext = AssertUnwindSafe(ext);
|
||||
|
||||
// The following unwind safety assertion is OK because if the method call panics, the
|
||||
// runtime will be dropped.
|
||||
let runtime = AssertUnwindSafe(runtime.as_ref());
|
||||
crate::executor::with_externalities_safe(&mut **ext, move || {
|
||||
runtime.new_instance()?.call("Core_version".into(), &[])
|
||||
})
|
||||
.map_err(|_| WasmError::Instantiation("panic in call to get runtime version".into()))?
|
||||
};
|
||||
|
||||
if let Ok(version_buf) = version_result {
|
||||
version = Some(decode_version(&version_buf)?)
|
||||
}
|
||||
}
|
||||
|
||||
let mut instances = Vec::with_capacity(max_instances);
|
||||
instances.resize_with(max_instances, || Mutex::new(None));
|
||||
|
||||
Ok(VersionedRuntime { module: runtime, version, instances })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
extern crate alloc;
|
||||
|
||||
use super::*;
|
||||
use alloc::borrow::Cow;
|
||||
use codec::Encode;
|
||||
use sp_api::{Core, RuntimeApiInfo};
|
||||
use sp_version::{create_apis_vec, RuntimeVersion};
|
||||
use sp_wasm_interface::HostFunctions;
|
||||
use substrate_test_runtime::Block;
|
||||
|
||||
#[derive(Encode)]
|
||||
pub struct OldRuntimeVersion {
|
||||
pub spec_name: Cow<'static, str>,
|
||||
pub impl_name: Cow<'static, str>,
|
||||
pub authoring_version: u32,
|
||||
pub spec_version: u32,
|
||||
pub impl_version: u32,
|
||||
pub apis: sp_version::ApisVec,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_functions_are_equal() {
|
||||
let host_functions = sp_io::SubstrateHostFunctions::host_functions();
|
||||
|
||||
let equal = &host_functions[..] == &host_functions[..];
|
||||
assert!(equal, "Host functions are not equal");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn old_runtime_version_decodes() {
|
||||
let old_runtime_version = OldRuntimeVersion {
|
||||
spec_name: "test".into(),
|
||||
impl_name: "test".into(),
|
||||
authoring_version: 1,
|
||||
spec_version: 1,
|
||||
impl_version: 1,
|
||||
apis: create_apis_vec!([(<dyn Core::<Block>>::ID, 1)]),
|
||||
};
|
||||
|
||||
let version = decode_version(&old_runtime_version.encode()).unwrap();
|
||||
assert_eq!(1, version.transaction_version);
|
||||
assert_eq!(0, version.system_version);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn old_runtime_version_decodes_fails_with_version_3() {
|
||||
let old_runtime_version = OldRuntimeVersion {
|
||||
spec_name: "test".into(),
|
||||
impl_name: "test".into(),
|
||||
authoring_version: 1,
|
||||
spec_version: 1,
|
||||
impl_version: 1,
|
||||
apis: create_apis_vec!([(<dyn Core::<Block>>::ID, 3)]),
|
||||
};
|
||||
|
||||
decode_version(&old_runtime_version.encode()).unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_runtime_version_decodes() {
|
||||
let old_runtime_version = RuntimeVersion {
|
||||
spec_name: "test".into(),
|
||||
impl_name: "test".into(),
|
||||
authoring_version: 1,
|
||||
spec_version: 1,
|
||||
impl_version: 1,
|
||||
apis: create_apis_vec!([(<dyn Core::<Block>>::ID, 3)]),
|
||||
transaction_version: 3,
|
||||
system_version: 4,
|
||||
};
|
||||
|
||||
let version = decode_version(&old_runtime_version.encode()).unwrap();
|
||||
assert_eq!(3, version.transaction_version);
|
||||
assert_eq!(0, version.system_version);
|
||||
|
||||
let old_runtime_version = RuntimeVersion {
|
||||
spec_name: "test".into(),
|
||||
impl_name: "test".into(),
|
||||
authoring_version: 1,
|
||||
spec_version: 1,
|
||||
impl_version: 1,
|
||||
apis: create_apis_vec!([(<dyn Core::<Block>>::ID, 4)]),
|
||||
transaction_version: 3,
|
||||
system_version: 4,
|
||||
};
|
||||
|
||||
let version = decode_version(&old_runtime_version.encode()).unwrap();
|
||||
assert_eq!(3, version.transaction_version);
|
||||
assert_eq!(4, version.system_version);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embed_runtime_version_works() {
|
||||
let wasm = sp_maybe_compressed_blob::decompress(
|
||||
substrate_test_runtime::wasm_binary_unwrap(),
|
||||
sp_maybe_compressed_blob::CODE_BLOB_BOMB_LIMIT,
|
||||
)
|
||||
.expect("Decompressing works");
|
||||
let runtime_version = RuntimeVersion {
|
||||
spec_name: "test_replace".into(),
|
||||
impl_name: "test_replace".into(),
|
||||
authoring_version: 100,
|
||||
spec_version: 100,
|
||||
impl_version: 100,
|
||||
apis: create_apis_vec!([(<dyn Core::<Block>>::ID, 4)]),
|
||||
transaction_version: 100,
|
||||
system_version: 1,
|
||||
};
|
||||
|
||||
let embedded = sp_version::embed::embed_runtime_version(&wasm, runtime_version.clone())
|
||||
.expect("Embedding works");
|
||||
|
||||
let blob = RuntimeBlob::new(&embedded).expect("Embedded blob is valid");
|
||||
let read_version = read_embedded_version(&blob)
|
||||
.ok()
|
||||
.flatten()
|
||||
.expect("Reading embedded version works");
|
||||
|
||||
assert_eq!(runtime_version, read_version);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
[package]
|
||||
name = "sc-executor-wasmtime"
|
||||
version = "0.29.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Defines a `WasmRuntime` that uses the Wasmtime JIT to execute."
|
||||
readme = "README.md"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
log = { workspace = true, default-features = true }
|
||||
parking_lot = { workspace = true, default-features = true }
|
||||
|
||||
# When bumping wasmtime do not forget to also bump rustix
|
||||
# to exactly the same version as used by wasmtime!
|
||||
anyhow = { workspace = true }
|
||||
sc-allocator = { workspace = true, default-features = true }
|
||||
sc-executor-common = { workspace = true, default-features = true }
|
||||
sp-runtime-interface = { workspace = true, default-features = true }
|
||||
sp-wasm-interface = { features = [
|
||||
"wasmtime",
|
||||
], workspace = true, default-features = true }
|
||||
wasmtime = { features = [
|
||||
"addr2line",
|
||||
"cache",
|
||||
"cranelift",
|
||||
"demangle",
|
||||
"gc",
|
||||
"gc-null",
|
||||
"parallel-compilation",
|
||||
"pooling-allocator",
|
||||
"profiling",
|
||||
"threads",
|
||||
], workspace = true }
|
||||
|
||||
# Here we include the rustix crate in the exactly same semver-compatible version as used by
|
||||
# wasmtime and enable its 'use-libc' flag.
|
||||
#
|
||||
# By default rustix directly calls the appropriate syscalls completely bypassing libc;
|
||||
# this doesn't have any actual benefits for us besides making it harder to debug memory
|
||||
# problems (since then `mmap` etc. cannot be easily hooked into).
|
||||
rustix = { features = [
|
||||
"fs",
|
||||
"mm",
|
||||
"param",
|
||||
"std",
|
||||
"use-libc",
|
||||
], workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
cargo_metadata = { workspace = true }
|
||||
codec = { workspace = true, default-features = true }
|
||||
paste = { workspace = true, default-features = true }
|
||||
sc-runtime-test = { workspace = true }
|
||||
sp-io = { workspace = true, default-features = true }
|
||||
tempfile = { workspace = true }
|
||||
wat = { workspace = true }
|
||||
|
||||
[features]
|
||||
runtime-benchmarks = [
|
||||
"sc-runtime-test/runtime-benchmarks",
|
||||
"sp-io/runtime-benchmarks",
|
||||
"sp-runtime-interface/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
License: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
@@ -0,0 +1,25 @@
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::env;
|
||||
|
||||
fn main() {
|
||||
if let Ok(profile) = env::var("PROFILE") {
|
||||
println!("cargo:rustc-cfg=build_profile=\"{}\"", profile);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! This module defines `HostState` and `HostContext` structs which provide logic and state
|
||||
//! required for execution of host.
|
||||
|
||||
use wasmtime::Caller;
|
||||
|
||||
use sc_allocator::{AllocationStats, FreeingBumpHeapAllocator};
|
||||
use sp_wasm_interface::{Pointer, WordSize};
|
||||
|
||||
use crate::{instance_wrapper::MemoryWrapper, runtime::StoreData, util};
|
||||
|
||||
/// The state required to construct a HostContext context. The context only lasts for one host
|
||||
/// call, whereas the state is maintained for the duration of a Wasm runtime call, which may make
|
||||
/// many different host calls that must share state.
|
||||
pub struct HostState {
|
||||
/// The allocator instance to keep track of allocated memory.
|
||||
///
|
||||
/// This is stored as an `Option` as we need to temporarily set this to `None` when we are
|
||||
/// allocating/deallocating memory. The problem being that we can only mutable access `caller`
|
||||
/// once.
|
||||
allocator: Option<FreeingBumpHeapAllocator>,
|
||||
panic_message: Option<String>,
|
||||
}
|
||||
|
||||
impl HostState {
|
||||
/// Constructs a new `HostState`.
|
||||
pub fn new(allocator: FreeingBumpHeapAllocator) -> Self {
|
||||
HostState { allocator: Some(allocator), panic_message: None }
|
||||
}
|
||||
|
||||
/// Takes the error message out of the host state, leaving a `None` in its place.
|
||||
pub fn take_panic_message(&mut self) -> Option<String> {
|
||||
self.panic_message.take()
|
||||
}
|
||||
|
||||
pub(crate) fn allocation_stats(&self) -> AllocationStats {
|
||||
self.allocator.as_ref()
|
||||
.expect("Allocator is always set and only unavailable when doing an allocation/deallocation; qed")
|
||||
.stats()
|
||||
}
|
||||
}
|
||||
|
||||
/// A `HostContext` implements `FunctionContext` for making host calls from a Wasmtime
|
||||
/// runtime. The `HostContext` exists only for the lifetime of the call and borrows state from
|
||||
/// a longer-living `HostState`.
|
||||
pub(crate) struct HostContext<'a> {
|
||||
pub(crate) caller: Caller<'a, StoreData>,
|
||||
}
|
||||
|
||||
impl<'a> HostContext<'a> {
|
||||
fn host_state_mut(&mut self) -> &mut HostState {
|
||||
self.caller
|
||||
.data_mut()
|
||||
.host_state_mut()
|
||||
.expect("host state is not empty when calling a function in wasm; qed")
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sp_wasm_interface::FunctionContext for HostContext<'a> {
|
||||
fn read_memory_into(
|
||||
&self,
|
||||
address: Pointer<u8>,
|
||||
dest: &mut [u8],
|
||||
) -> sp_wasm_interface::Result<()> {
|
||||
util::read_memory_into(&self.caller, address, dest).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn write_memory(&mut self, address: Pointer<u8>, data: &[u8]) -> sp_wasm_interface::Result<()> {
|
||||
util::write_memory_from(&mut self.caller, address, data).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn allocate_memory(&mut self, size: WordSize) -> sp_wasm_interface::Result<Pointer<u8>> {
|
||||
let memory = self.caller.data().memory();
|
||||
let mut allocator = self
|
||||
.host_state_mut()
|
||||
.allocator
|
||||
.take()
|
||||
.expect("allocator is not empty when calling a function in wasm; qed");
|
||||
|
||||
// We can not return on error early, as we need to store back allocator.
|
||||
let res = allocator
|
||||
.allocate(&mut MemoryWrapper(&memory, &mut self.caller), size)
|
||||
.map_err(|e| e.to_string());
|
||||
|
||||
self.host_state_mut().allocator = Some(allocator);
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
fn deallocate_memory(&mut self, ptr: Pointer<u8>) -> sp_wasm_interface::Result<()> {
|
||||
let memory = self.caller.data().memory();
|
||||
let mut allocator = self
|
||||
.host_state_mut()
|
||||
.allocator
|
||||
.take()
|
||||
.expect("allocator is not empty when calling a function in wasm; qed");
|
||||
|
||||
// We can not return on error early, as we need to store back allocator.
|
||||
let res = allocator
|
||||
.deallocate(&mut MemoryWrapper(&memory, &mut self.caller), ptr)
|
||||
.map_err(|e| e.to_string());
|
||||
|
||||
self.host_state_mut().allocator = Some(allocator);
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
fn register_panic_error_message(&mut self, message: &str) {
|
||||
self.host_state_mut().panic_message = Some(message.to_owned());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::{host::HostContext, runtime::StoreData};
|
||||
use sc_executor_common::error::WasmError;
|
||||
use sp_wasm_interface::{FunctionContext, HostFunctions};
|
||||
use std::collections::HashMap;
|
||||
use wasmtime::{ExternType, FuncType, ImportType, Linker, Module};
|
||||
|
||||
/// Goes over all imports of a module and prepares the given linker for instantiation of the module.
|
||||
/// Returns an error if there are imports that cannot be satisfied.
|
||||
pub(crate) fn prepare_imports<H>(
|
||||
linker: &mut Linker<StoreData>,
|
||||
module: &Module,
|
||||
allow_missing_func_imports: bool,
|
||||
) -> Result<(), WasmError>
|
||||
where
|
||||
H: HostFunctions,
|
||||
{
|
||||
let mut pending_func_imports = HashMap::new();
|
||||
for import_ty in module.imports() {
|
||||
let name = import_ty.name();
|
||||
|
||||
if import_ty.module() != "env" {
|
||||
return Err(WasmError::Other(format!(
|
||||
"host doesn't provide any imports from non-env module: {}:{}",
|
||||
import_ty.module(),
|
||||
name,
|
||||
)));
|
||||
}
|
||||
|
||||
match import_ty.ty() {
|
||||
ExternType::Func(func_ty) => {
|
||||
pending_func_imports.insert(name.to_owned(), (import_ty, func_ty));
|
||||
},
|
||||
_ =>
|
||||
return Err(WasmError::Other(format!(
|
||||
"host doesn't provide any non function imports: {}:{}",
|
||||
import_ty.module(),
|
||||
name,
|
||||
))),
|
||||
};
|
||||
}
|
||||
|
||||
let mut registry = Registry { linker, pending_func_imports };
|
||||
H::register_static(&mut registry)?;
|
||||
|
||||
if !registry.pending_func_imports.is_empty() {
|
||||
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);
|
||||
linker
|
||||
.func_new("env", &name, func_ty.clone(), move |_, _, _| {
|
||||
Err(anyhow::Error::msg(error.clone()))
|
||||
})
|
||||
.expect("adding a missing import stub can only fail when the item already exists, and it is missing here; qed");
|
||||
}
|
||||
} else {
|
||||
let mut names = Vec::new();
|
||||
for (name, (import_ty, _)) in registry.pending_func_imports {
|
||||
names.push(format!("'{}:{}'", import_ty.module(), name));
|
||||
}
|
||||
let names = names.join(", ");
|
||||
return Err(WasmError::Other(format!(
|
||||
"runtime requires function imports which are not present on the host: {}",
|
||||
names
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct Registry<'a, 'b> {
|
||||
linker: &'a mut Linker<StoreData>,
|
||||
pending_func_imports: HashMap<String, (ImportType<'b>, FuncType)>,
|
||||
}
|
||||
|
||||
impl<'a, 'b> sp_wasm_interface::HostFunctionRegistry for Registry<'a, 'b> {
|
||||
type State = StoreData;
|
||||
type Error = WasmError;
|
||||
type FunctionContext = HostContext<'a>;
|
||||
|
||||
fn with_function_context<R>(
|
||||
caller: wasmtime::Caller<Self::State>,
|
||||
callback: impl FnOnce(&mut dyn FunctionContext) -> R,
|
||||
) -> R {
|
||||
callback(&mut HostContext { caller })
|
||||
}
|
||||
|
||||
fn register_static<Params, Results>(
|
||||
&mut self,
|
||||
fn_name: &str,
|
||||
func: impl wasmtime::IntoFunc<Self::State, Params, Results>,
|
||||
) -> Result<(), Self::Error> {
|
||||
if self.pending_func_imports.remove(fn_name).is_some() {
|
||||
self.linker.func_wrap("env", fn_name, func).map_err(|error| {
|
||||
WasmError::Other(format!(
|
||||
"failed to register host function '{}' with the WASM linker: {:#}",
|
||||
fn_name, error
|
||||
))
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Defines data and logic needed for interaction with an WebAssembly instance of a substrate
|
||||
//! runtime module.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::runtime::{InstanceCounter, ReleaseInstanceHandle, Store, StoreData};
|
||||
use sc_executor_common::error::{Backtrace, Error, MessageWithBacktrace, Result, WasmError};
|
||||
use sp_wasm_interface::{Pointer, WordSize};
|
||||
use wasmtime::{AsContext, AsContextMut, Engine, Instance, InstancePre, Memory};
|
||||
|
||||
/// Wasm blob entry point.
|
||||
pub struct EntryPoint(wasmtime::TypedFunc<(u32, u32), u64>);
|
||||
|
||||
impl EntryPoint {
|
||||
/// Call this entry point.
|
||||
pub(crate) fn call(
|
||||
&self,
|
||||
store: &mut Store,
|
||||
data_ptr: Pointer<u8>,
|
||||
data_len: WordSize,
|
||||
) -> Result<u64> {
|
||||
let data_ptr = u32::from(data_ptr);
|
||||
let data_len = u32::from(data_len);
|
||||
|
||||
self.0.call(&mut *store, (data_ptr, data_len)).map_err(|trap| {
|
||||
let host_state = store
|
||||
.data_mut()
|
||||
.host_state
|
||||
.as_mut()
|
||||
.expect("host state cannot be empty while a function is being called; qed");
|
||||
|
||||
let backtrace = trap.downcast_ref::<wasmtime::WasmBacktrace>().map(|backtrace| {
|
||||
// The logic to print out a backtrace is somewhat complicated,
|
||||
// so let's get wasmtime to print it out for us.
|
||||
Backtrace { backtrace_string: backtrace.to_string() }
|
||||
});
|
||||
|
||||
if let Some(message) = host_state.take_panic_message() {
|
||||
Error::AbortedDueToPanic(MessageWithBacktrace { message, backtrace })
|
||||
} else {
|
||||
let message = trap.root_cause().to_string();
|
||||
Error::AbortedDueToTrap(MessageWithBacktrace { message, backtrace })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn direct(
|
||||
func: wasmtime::Func,
|
||||
ctx: impl AsContext,
|
||||
) -> std::result::Result<Self, &'static str> {
|
||||
let entrypoint = func
|
||||
.typed::<(u32, u32), u64>(ctx)
|
||||
.map_err(|_| "Invalid signature for direct entry point")?;
|
||||
Ok(Self(entrypoint))
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper around [`Memory`] that implements [`sc_allocator::Memory`].
|
||||
pub(crate) struct MemoryWrapper<'a, C>(pub &'a wasmtime::Memory, pub &'a mut C);
|
||||
|
||||
impl<C: AsContextMut> sc_allocator::Memory for MemoryWrapper<'_, C> {
|
||||
fn with_access_mut<R>(&mut self, run: impl FnOnce(&mut [u8]) -> R) -> R {
|
||||
run(self.0.data_mut(&mut self.1))
|
||||
}
|
||||
|
||||
fn with_access<R>(&self, run: impl FnOnce(&[u8]) -> R) -> R {
|
||||
run(self.0.data(&self.1))
|
||||
}
|
||||
|
||||
fn grow(&mut self, additional: u32) -> std::result::Result<(), ()> {
|
||||
self.0
|
||||
.grow(&mut self.1, additional as u64)
|
||||
.map_err(|e| {
|
||||
log::error!(
|
||||
target: "wasm-executor",
|
||||
"Failed to grow memory by {} pages: {}",
|
||||
additional,
|
||||
e,
|
||||
)
|
||||
})
|
||||
.map(drop)
|
||||
}
|
||||
|
||||
fn pages(&self) -> u32 {
|
||||
self.0.size(&self.1) as u32
|
||||
}
|
||||
|
||||
fn max_pages(&self) -> Option<u32> {
|
||||
self.0.ty(&self.1).maximum().map(|p| p as _)
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrap the given WebAssembly Instance of a wasm module with Substrate-runtime.
|
||||
///
|
||||
/// This struct is a handy wrapper around a wasmtime `Instance` that provides substrate specific
|
||||
/// routines.
|
||||
pub struct InstanceWrapper {
|
||||
instance: Instance,
|
||||
store: Store,
|
||||
// NOTE: We want to decrement the instance counter *after* the store has been dropped
|
||||
// to avoid a potential race condition, so this field must always be kept
|
||||
// as the last field in the struct!
|
||||
_release_instance_handle: ReleaseInstanceHandle,
|
||||
}
|
||||
|
||||
impl InstanceWrapper {
|
||||
pub(crate) fn new(
|
||||
engine: &Engine,
|
||||
instance_pre: &InstancePre<StoreData>,
|
||||
instance_counter: Arc<InstanceCounter>,
|
||||
) -> Result<Self> {
|
||||
let _release_instance_handle = instance_counter.acquire_instance();
|
||||
let mut store = Store::new(engine, Default::default());
|
||||
let instance = instance_pre.instantiate(&mut store).map_err(|error| {
|
||||
WasmError::Other(format!(
|
||||
"failed to instantiate a new WASM module instance: {:#}",
|
||||
error,
|
||||
))
|
||||
})?;
|
||||
|
||||
let memory = get_linear_memory(&instance, &mut store)?;
|
||||
|
||||
store.data_mut().memory = Some(memory);
|
||||
|
||||
Ok(InstanceWrapper { instance, store, _release_instance_handle })
|
||||
}
|
||||
|
||||
/// Resolves a substrate entrypoint by the given name.
|
||||
///
|
||||
/// An entrypoint must have a signature `(i32, i32) -> i64`, otherwise this function will return
|
||||
/// an error.
|
||||
pub fn resolve_entrypoint(&mut self, method: &str) -> Result<EntryPoint> {
|
||||
// Resolve the requested method and verify that it has a proper signature.
|
||||
let export = self
|
||||
.instance
|
||||
.get_export(&mut self.store, method)
|
||||
.ok_or_else(|| Error::from(format!("Exported method {} is not found", method)))?;
|
||||
let func = export
|
||||
.into_func()
|
||||
.ok_or_else(|| Error::from(format!("Export {} is not a function", method)))?;
|
||||
EntryPoint::direct(func, &self.store).map_err(|_| {
|
||||
Error::from(format!("Exported function '{}' has invalid signature.", method))
|
||||
})
|
||||
}
|
||||
|
||||
/// Reads `__heap_base: i32` global variable and returns it.
|
||||
///
|
||||
/// If it doesn't exist, not a global or of not i32 type returns an error.
|
||||
pub fn extract_heap_base(&mut self) -> Result<u32> {
|
||||
let heap_base_export = self
|
||||
.instance
|
||||
.get_export(&mut self.store, "__heap_base")
|
||||
.ok_or_else(|| Error::from("__heap_base is not found"))?;
|
||||
|
||||
let heap_base_global = heap_base_export
|
||||
.into_global()
|
||||
.ok_or_else(|| Error::from("__heap_base is not a global"))?;
|
||||
|
||||
let heap_base = heap_base_global
|
||||
.get(&mut self.store)
|
||||
.i32()
|
||||
.ok_or_else(|| Error::from("__heap_base is not a i32"))?;
|
||||
|
||||
Ok(heap_base as u32)
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract linear memory instance from the given instance.
|
||||
fn get_linear_memory(instance: &Instance, ctx: impl AsContextMut) -> Result<Memory> {
|
||||
let memory_export = instance
|
||||
.get_export(ctx, "memory")
|
||||
.ok_or_else(|| Error::from("memory is not exported under `memory` name"))?;
|
||||
|
||||
let memory = memory_export
|
||||
.into_memory()
|
||||
.ok_or_else(|| Error::from("the `memory` export should have memory type"))?;
|
||||
|
||||
Ok(memory)
|
||||
}
|
||||
|
||||
/// Functions related to memory.
|
||||
impl InstanceWrapper {
|
||||
pub(crate) fn store(&self) -> &Store {
|
||||
&self.store
|
||||
}
|
||||
|
||||
pub(crate) fn store_mut(&mut self) -> &mut Store {
|
||||
&mut self.store
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Defines a `WasmRuntime` that uses the Wasmtime JIT to execute.
|
||||
//!
|
||||
//! You can choose a profiling strategy at runtime with
|
||||
//! environment variable `WASMTIME_PROFILING_STRATEGY`:
|
||||
//!
|
||||
//! | `WASMTIME_PROFILING_STRATEGY` | Effect |
|
||||
//! |-------------|-------------------------|
|
||||
//! | undefined | No profiling |
|
||||
//! | `"jitdump"` | jitdump profiling |
|
||||
//! | `"perfmap"` | perfmap profiling |
|
||||
//! | other value | No profiling (warning) |
|
||||
|
||||
mod host;
|
||||
mod imports;
|
||||
mod instance_wrapper;
|
||||
mod runtime;
|
||||
mod util;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use runtime::{
|
||||
create_runtime, create_runtime_from_artifact, create_runtime_from_artifact_bytes,
|
||||
prepare_runtime_artifact, Config, DeterministicStackLimit, InstantiationStrategy, Semantics,
|
||||
WasmtimeRuntime,
|
||||
};
|
||||
pub use sc_executor_common::{
|
||||
runtime_blob::RuntimeBlob,
|
||||
wasm_runtime::{HeapAllocStrategy, WasmModule},
|
||||
};
|
||||
@@ -0,0 +1,726 @@
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Defines the compiled Wasm runtime that uses Wasmtime internally.
|
||||
|
||||
use crate::{
|
||||
host::HostState,
|
||||
instance_wrapper::{EntryPoint, InstanceWrapper, MemoryWrapper},
|
||||
util::{self, replace_strategy_if_broken},
|
||||
};
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use sc_allocator::{AllocationStats, FreeingBumpHeapAllocator};
|
||||
use sc_executor_common::{
|
||||
error::{Error, Result, WasmError},
|
||||
runtime_blob::RuntimeBlob,
|
||||
util::checked_range,
|
||||
wasm_runtime::{HeapAllocStrategy, WasmInstance, WasmModule},
|
||||
};
|
||||
use sp_runtime_interface::unpack_ptr_and_len;
|
||||
use sp_wasm_interface::{HostFunctions, Pointer, WordSize};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use wasmtime::{AsContext, Cache, CacheConfig, Engine, Memory};
|
||||
|
||||
const MAX_INSTANCE_COUNT: u32 = 64;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct StoreData {
|
||||
/// This will only be set when we call into the runtime.
|
||||
pub(crate) host_state: Option<HostState>,
|
||||
/// This will be always set once the store is initialized.
|
||||
pub(crate) memory: Option<Memory>,
|
||||
}
|
||||
|
||||
impl StoreData {
|
||||
/// Returns a mutable reference to the host state.
|
||||
pub fn host_state_mut(&mut self) -> Option<&mut HostState> {
|
||||
self.host_state.as_mut()
|
||||
}
|
||||
|
||||
/// Returns the host memory.
|
||||
pub fn memory(&self) -> Memory {
|
||||
self.memory.expect("memory is always set; qed")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type Store = wasmtime::Store<StoreData>;
|
||||
|
||||
enum Strategy {
|
||||
RecreateInstance(InstanceCreator),
|
||||
}
|
||||
|
||||
struct InstanceCreator {
|
||||
engine: Engine,
|
||||
instance_pre: Arc<wasmtime::InstancePre<StoreData>>,
|
||||
instance_counter: Arc<InstanceCounter>,
|
||||
}
|
||||
|
||||
impl InstanceCreator {
|
||||
fn instantiate(&mut self) -> Result<InstanceWrapper> {
|
||||
InstanceWrapper::new(&self.engine, &self.instance_pre, self.instance_counter.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// A handle for releasing an instance acquired by [`InstanceCounter::acquire_instance`].
|
||||
pub(crate) struct ReleaseInstanceHandle {
|
||||
counter: Arc<InstanceCounter>,
|
||||
}
|
||||
|
||||
impl Drop for ReleaseInstanceHandle {
|
||||
fn drop(&mut self) {
|
||||
{
|
||||
let mut counter = self.counter.counter.lock();
|
||||
*counter = counter.saturating_sub(1);
|
||||
}
|
||||
|
||||
self.counter.wait_for_instance.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
/// Keeps track on the number of parallel instances.
|
||||
///
|
||||
/// The runtime cache keeps track on the number of parallel instances. The maximum number in the
|
||||
/// cache is less than what we have configured as [`MAX_INSTANCE_COUNT`] for wasmtime. However, the
|
||||
/// cache will create on demand instances if required. This instance counter will ensure that we are
|
||||
/// blocking when we are trying to create too many instances.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct InstanceCounter {
|
||||
counter: Mutex<u32>,
|
||||
wait_for_instance: parking_lot::Condvar,
|
||||
}
|
||||
|
||||
impl InstanceCounter {
|
||||
/// Acquire an instance.
|
||||
///
|
||||
/// Blocks if there is no free instance available.
|
||||
///
|
||||
/// The returned [`ReleaseInstanceHandle`] should be dropped when the instance isn't used
|
||||
/// anymore.
|
||||
pub fn acquire_instance(self: Arc<Self>) -> ReleaseInstanceHandle {
|
||||
let mut counter = self.counter.lock();
|
||||
|
||||
while *counter >= MAX_INSTANCE_COUNT {
|
||||
self.wait_for_instance.wait(&mut counter);
|
||||
}
|
||||
*counter += 1;
|
||||
|
||||
ReleaseInstanceHandle { counter: self.clone() }
|
||||
}
|
||||
}
|
||||
|
||||
/// A `WasmModule` implementation using wasmtime to compile the runtime module to machine code
|
||||
/// and execute the compiled code.
|
||||
pub struct WasmtimeRuntime {
|
||||
engine: Engine,
|
||||
instance_pre: Arc<wasmtime::InstancePre<StoreData>>,
|
||||
instantiation_strategy: InternalInstantiationStrategy,
|
||||
instance_counter: Arc<InstanceCounter>,
|
||||
}
|
||||
|
||||
impl WasmModule for WasmtimeRuntime {
|
||||
fn new_instance(&self) -> Result<Box<dyn WasmInstance>> {
|
||||
let strategy = match self.instantiation_strategy {
|
||||
InternalInstantiationStrategy::Builtin => Strategy::RecreateInstance(InstanceCreator {
|
||||
engine: self.engine.clone(),
|
||||
instance_pre: self.instance_pre.clone(),
|
||||
instance_counter: self.instance_counter.clone(),
|
||||
}),
|
||||
};
|
||||
|
||||
Ok(Box::new(WasmtimeInstance { strategy }))
|
||||
}
|
||||
}
|
||||
|
||||
/// A `WasmInstance` implementation that reuses compiled module and spawns instances
|
||||
/// to execute the compiled code.
|
||||
pub struct WasmtimeInstance {
|
||||
strategy: Strategy,
|
||||
}
|
||||
|
||||
impl WasmtimeInstance {
|
||||
fn call_impl(
|
||||
&mut self,
|
||||
method: &str,
|
||||
data: &[u8],
|
||||
allocation_stats: &mut Option<AllocationStats>,
|
||||
) -> Result<Vec<u8>> {
|
||||
match &mut self.strategy {
|
||||
Strategy::RecreateInstance(ref mut instance_creator) => {
|
||||
let mut instance_wrapper = instance_creator.instantiate()?;
|
||||
let heap_base = instance_wrapper.extract_heap_base()?;
|
||||
let entrypoint = instance_wrapper.resolve_entrypoint(method)?;
|
||||
let allocator = FreeingBumpHeapAllocator::new(heap_base);
|
||||
|
||||
perform_call(data, &mut instance_wrapper, entrypoint, allocator, allocation_stats)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WasmInstance for WasmtimeInstance {
|
||||
fn call_with_allocation_stats(
|
||||
&mut self,
|
||||
method: &str,
|
||||
data: &[u8],
|
||||
) -> (Result<Vec<u8>>, Option<AllocationStats>) {
|
||||
let mut allocation_stats = None;
|
||||
let result = self.call_impl(method, data, &mut allocation_stats);
|
||||
(result, allocation_stats)
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepare a directory structure and a config file to enable wasmtime caching.
|
||||
///
|
||||
/// In case of an error the caching will not be enabled.
|
||||
fn setup_wasmtime_caching(
|
||||
cache_path: &Path,
|
||||
config: &mut wasmtime::Config,
|
||||
) -> std::result::Result<(), String> {
|
||||
use std::fs;
|
||||
|
||||
let wasmtime_cache_root = cache_path.join("wasmtime");
|
||||
fs::create_dir_all(&wasmtime_cache_root)
|
||||
.map_err(|err| format!("cannot create the dirs to cache: {}", err))?;
|
||||
|
||||
let mut cache_config = CacheConfig::new();
|
||||
cache_config.with_directory(cache_path);
|
||||
|
||||
let cache =
|
||||
Cache::new(cache_config).map_err(|err| format!("failed to initiate Cache: {err:?}"))?;
|
||||
|
||||
config.cache(Some(cache));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn common_config(semantics: &Semantics) -> std::result::Result<wasmtime::Config, WasmError> {
|
||||
let mut config = wasmtime::Config::new();
|
||||
config.cranelift_opt_level(wasmtime::OptLevel::SpeedAndSize);
|
||||
config.cranelift_nan_canonicalization(semantics.canonicalize_nans);
|
||||
|
||||
let profiler = match std::env::var_os("WASMTIME_PROFILING_STRATEGY") {
|
||||
Some(os_string) if os_string == "jitdump" => wasmtime::ProfilingStrategy::JitDump,
|
||||
Some(os_string) if os_string == "perfmap" => wasmtime::ProfilingStrategy::PerfMap,
|
||||
None => wasmtime::ProfilingStrategy::None,
|
||||
Some(_) => {
|
||||
// Remember if we have already logged a warning due to an unknown profiling strategy.
|
||||
static UNKNOWN_PROFILING_STRATEGY: AtomicBool = AtomicBool::new(false);
|
||||
// Make sure that the warning will not be relogged regularly.
|
||||
if !UNKNOWN_PROFILING_STRATEGY.swap(true, Ordering::Relaxed) {
|
||||
log::warn!("WASMTIME_PROFILING_STRATEGY is set to unknown value, ignored.");
|
||||
}
|
||||
wasmtime::ProfilingStrategy::None
|
||||
},
|
||||
};
|
||||
config.profiler(profiler);
|
||||
|
||||
let native_stack_max = match semantics.deterministic_stack_limit {
|
||||
Some(DeterministicStackLimit { native_stack_max, .. }) => native_stack_max,
|
||||
|
||||
// In `wasmtime` 0.35 the default stack size limit was changed from 1MB to 512KB.
|
||||
//
|
||||
// This broke at least one teyrchain which depended on the original 1MB limit,
|
||||
// so here we restore it to what it was originally.
|
||||
None => 1024 * 1024,
|
||||
};
|
||||
|
||||
config.max_wasm_stack(native_stack_max as usize);
|
||||
|
||||
config.parallel_compilation(semantics.parallel_compilation);
|
||||
|
||||
// Be clear and specific about the extensions we support. If an update brings new features
|
||||
// they should be introduced here as well.
|
||||
config.wasm_reference_types(semantics.wasm_reference_types);
|
||||
config.wasm_simd(semantics.wasm_simd);
|
||||
config.wasm_relaxed_simd(semantics.wasm_simd);
|
||||
config.wasm_bulk_memory(semantics.wasm_bulk_memory);
|
||||
config.wasm_multi_value(semantics.wasm_multi_value);
|
||||
config.wasm_multi_memory(false);
|
||||
config.wasm_threads(false);
|
||||
config.wasm_memory64(false);
|
||||
config.wasm_tail_call(false);
|
||||
config.wasm_extended_const(false);
|
||||
|
||||
let (use_pooling, use_cow) = match semantics.instantiation_strategy {
|
||||
InstantiationStrategy::PoolingCopyOnWrite => (true, true),
|
||||
InstantiationStrategy::Pooling => (true, false),
|
||||
InstantiationStrategy::RecreateInstanceCopyOnWrite => (false, true),
|
||||
InstantiationStrategy::RecreateInstance => (false, false),
|
||||
};
|
||||
|
||||
const WASM_PAGE_SIZE: u64 = 65536;
|
||||
|
||||
config.memory_init_cow(use_cow);
|
||||
config.memory_guaranteed_dense_image_size(match semantics.heap_alloc_strategy {
|
||||
HeapAllocStrategy::Dynamic { maximum_pages } =>
|
||||
maximum_pages.map(|p| p as u64 * WASM_PAGE_SIZE).unwrap_or(u64::MAX),
|
||||
HeapAllocStrategy::Static { .. } => u64::MAX,
|
||||
});
|
||||
|
||||
if use_pooling {
|
||||
const MAX_WASM_PAGES: u64 = 0x10000;
|
||||
|
||||
let memory_pages = match semantics.heap_alloc_strategy {
|
||||
HeapAllocStrategy::Dynamic { maximum_pages } =>
|
||||
maximum_pages.map(|p| p as u64).unwrap_or(MAX_WASM_PAGES),
|
||||
HeapAllocStrategy::Static { .. } => MAX_WASM_PAGES,
|
||||
};
|
||||
|
||||
let mut pooling_config = wasmtime::PoolingAllocationConfig::default();
|
||||
pooling_config
|
||||
.max_unused_warm_slots(4)
|
||||
// Pooling needs a bunch of hard limits to be set; if we go over
|
||||
// any of these then the instantiation will fail.
|
||||
//
|
||||
// Current minimum values for kusama (as of 2022-04-14):
|
||||
// size: 32384
|
||||
// table_elements: 1249
|
||||
// memory_pages: 2070
|
||||
.max_core_instance_size(512 * 1024)
|
||||
.table_elements(8192)
|
||||
.max_memory_size(memory_pages as usize * WASM_PAGE_SIZE as usize)
|
||||
.total_tables(MAX_INSTANCE_COUNT)
|
||||
.total_memories(MAX_INSTANCE_COUNT)
|
||||
// This determines how many instances of the module can be
|
||||
// instantiated in parallel from the same `Module`.
|
||||
.total_core_instances(MAX_INSTANCE_COUNT);
|
||||
|
||||
config.allocation_strategy(wasmtime::InstanceAllocationStrategy::Pooling(pooling_config));
|
||||
}
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Knobs for deterministic stack height limiting.
|
||||
///
|
||||
/// The WebAssembly standard defines a call/value stack but it doesn't say anything about its
|
||||
/// size except that it has to be finite. The implementations are free to choose their own notion
|
||||
/// of limit: some may count the number of calls or values, others would rely on the host machine
|
||||
/// stack and trap on reaching a guard page.
|
||||
///
|
||||
/// This obviously is a source of non-determinism during execution. This feature can be used
|
||||
/// to instrument the code so that it will count the depth of execution in some deterministic
|
||||
/// way (the machine stack limit should be so high that the deterministic limit always triggers
|
||||
/// first).
|
||||
///
|
||||
/// The deterministic stack height limiting feature allows to instrument the code so that it will
|
||||
/// count the number of items that may be on the stack. This counting will only act as an rough
|
||||
/// estimate of the actual stack limit in wasmtime. This is because wasmtime measures it's stack
|
||||
/// usage in bytes.
|
||||
///
|
||||
/// The actual number of bytes consumed by a function is not trivial to compute without going
|
||||
/// through full compilation. Therefore, it's expected that `native_stack_max` is greatly
|
||||
/// overestimated and thus never reached in practice. The stack overflow check introduced by the
|
||||
/// instrumentation and that relies on the logical item count should be reached first.
|
||||
///
|
||||
/// See [here][stack_height] for more details of the instrumentation
|
||||
///
|
||||
/// [stack_height]: https://github.com/paritytech/wasm-instrument/blob/master/src/stack_limiter/mod.rs
|
||||
#[derive(Clone)]
|
||||
pub struct DeterministicStackLimit {
|
||||
/// A number of logical "values" that can be pushed on the wasm stack. A trap will be triggered
|
||||
/// if exceeded.
|
||||
///
|
||||
/// A logical value is a local, an argument or a value pushed on operand stack.
|
||||
pub logical_max: u32,
|
||||
/// The maximum number of bytes for stack used by wasmtime JITed code.
|
||||
///
|
||||
/// It's not specified how much bytes will be consumed by a stack frame for a given wasm
|
||||
/// function after translation into machine code. It is also not quite trivial.
|
||||
///
|
||||
/// Therefore, this number should be chosen conservatively. It must be so large so that it can
|
||||
/// fit the [`logical_max`](Self::logical_max) logical values on the stack, according to the
|
||||
/// current instrumentation algorithm.
|
||||
///
|
||||
/// This value cannot be 0.
|
||||
pub native_stack_max: u32,
|
||||
}
|
||||
|
||||
/// The instantiation strategy to use for the WASM executor.
|
||||
///
|
||||
/// All of the CoW strategies (with `CopyOnWrite` suffix) are only supported when either:
|
||||
/// a) we're running on Linux,
|
||||
/// b) we're running on an Unix-like system and we're precompiling
|
||||
/// our module beforehand and instantiating from a file.
|
||||
///
|
||||
/// If the CoW variant of a strategy is unsupported the executor will
|
||||
/// fall back to the non-CoW equivalent.
|
||||
#[non_exhaustive]
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||
pub enum InstantiationStrategy {
|
||||
/// Pool the instances to avoid initializing everything from scratch
|
||||
/// on each instantiation. Use copy-on-write memory when possible.
|
||||
///
|
||||
/// This is the fastest instantiation strategy.
|
||||
PoolingCopyOnWrite,
|
||||
|
||||
/// Recreate the instance from scratch on every instantiation.
|
||||
/// Use copy-on-write memory when possible.
|
||||
RecreateInstanceCopyOnWrite,
|
||||
|
||||
/// Pool the instances to avoid initializing everything from scratch
|
||||
/// on each instantiation.
|
||||
Pooling,
|
||||
|
||||
/// Recreate the instance from scratch on every instantiation. Very slow.
|
||||
RecreateInstance,
|
||||
}
|
||||
|
||||
enum InternalInstantiationStrategy {
|
||||
Builtin,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Semantics {
|
||||
/// The instantiation strategy to use.
|
||||
pub instantiation_strategy: InstantiationStrategy,
|
||||
|
||||
/// Specifying `Some` will enable deterministic stack height. That is, all executor
|
||||
/// invocations will reach stack overflow at the exactly same point across different wasmtime
|
||||
/// versions and architectures.
|
||||
///
|
||||
/// This is achieved by a combination of running an instrumentation pass on input code and
|
||||
/// configuring wasmtime accordingly.
|
||||
///
|
||||
/// Since this feature depends on instrumentation, it can be set only if runtime is
|
||||
/// instantiated using the runtime blob, e.g. using [`create_runtime`].
|
||||
// I.e. if [`CodeSupplyMode::Verbatim`] is used.
|
||||
pub deterministic_stack_limit: Option<DeterministicStackLimit>,
|
||||
|
||||
/// Controls whether wasmtime should compile floating point in a way that doesn't allow for
|
||||
/// non-determinism.
|
||||
///
|
||||
/// By default, the wasm spec allows some local non-determinism wrt. certain floating point
|
||||
/// operations. Specifically, those operations that are not defined to operate on bits (e.g.
|
||||
/// fneg) can produce NaN values. The exact bit pattern for those is not specified and may
|
||||
/// depend on the particular machine that executes wasmtime generated JITed machine code. That
|
||||
/// is a source of non-deterministic values.
|
||||
///
|
||||
/// The classical runtime environment for Substrate allowed it and punted this on the runtime
|
||||
/// developers. For PVFs, we want to ensure that execution is deterministic though. Therefore,
|
||||
/// for PVF execution this flag is meant to be turned on.
|
||||
pub canonicalize_nans: bool,
|
||||
|
||||
/// Configures wasmtime to use multiple threads for compiling.
|
||||
pub parallel_compilation: bool,
|
||||
|
||||
/// The heap allocation strategy to use.
|
||||
pub heap_alloc_strategy: HeapAllocStrategy,
|
||||
|
||||
/// Enables WASM Multi-Value proposal
|
||||
pub wasm_multi_value: bool,
|
||||
|
||||
/// Enables WASM Bulk Memory Operations proposal
|
||||
pub wasm_bulk_memory: bool,
|
||||
|
||||
/// Enables WASM Reference Types proposal
|
||||
pub wasm_reference_types: bool,
|
||||
|
||||
/// Enables WASM Fixed-Width SIMD proposal
|
||||
pub wasm_simd: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Config {
|
||||
/// The WebAssembly standard requires all imports of an instantiated module to be resolved,
|
||||
/// otherwise, the instantiation fails. If this option is set to `true`, then this behavior is
|
||||
/// overridden and imports that are requested by the module and not provided by the host
|
||||
/// functions will be resolved using stubs. These stubs will trap upon a call.
|
||||
pub allow_missing_func_imports: bool,
|
||||
|
||||
/// A directory in which wasmtime can store its compiled artifacts cache.
|
||||
pub cache_path: Option<PathBuf>,
|
||||
|
||||
/// Tuning of various semantics of the wasmtime executor.
|
||||
pub semantics: Semantics,
|
||||
}
|
||||
|
||||
enum CodeSupplyMode<'a> {
|
||||
/// The runtime is instantiated using the given runtime blob.
|
||||
Fresh(RuntimeBlob),
|
||||
|
||||
/// The runtime is instantiated using a precompiled module at the given path.
|
||||
///
|
||||
/// This assumes that the code is already prepared for execution and the same `Config` was
|
||||
/// used.
|
||||
///
|
||||
/// 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),
|
||||
|
||||
/// The runtime is instantiated using a precompiled module with the given bytes.
|
||||
///
|
||||
/// This assumes that the code is already prepared for execution and the same `Config` was
|
||||
/// used.
|
||||
PrecompiledBytes(&'a [u8]),
|
||||
}
|
||||
|
||||
/// Create a new `WasmtimeRuntime` given the code. This function performs translation from Wasm to
|
||||
/// machine code, which can be computationally heavy.
|
||||
///
|
||||
/// The `H` generic parameter is used to statically pass a set of host functions which are exposed
|
||||
/// to the runtime.
|
||||
pub fn create_runtime<H>(
|
||||
blob: RuntimeBlob,
|
||||
config: Config,
|
||||
) -> std::result::Result<WasmtimeRuntime, WasmError>
|
||||
where
|
||||
H: HostFunctions,
|
||||
{
|
||||
// SAFETY: this is safe because it doesn't use `CodeSupplyMode::Precompiled`.
|
||||
unsafe { do_create_runtime::<H>(CodeSupplyMode::Fresh(blob), config) }
|
||||
}
|
||||
|
||||
/// The same as [`create_runtime`] but takes a path to a precompiled artifact,
|
||||
/// which makes this function considerably faster than [`create_runtime`].
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that the compiled artifact passed here was:
|
||||
/// 1) produced by [`prepare_runtime_artifact`],
|
||||
/// 2) written to the disk as a file,
|
||||
/// 3) was not modified,
|
||||
/// 4) will not be modified while any runtime using this artifact is alive, or is being
|
||||
/// instantiated.
|
||||
///
|
||||
/// Failure to adhere to these requirements might lead to crashes and arbitrary code execution.
|
||||
///
|
||||
/// It is ok though if the compiled artifact was created by code of another version or with
|
||||
/// different configuration flags. In such case the caller will receive an `Err` deterministically.
|
||||
pub unsafe fn create_runtime_from_artifact<H>(
|
||||
compiled_artifact_path: &Path,
|
||||
config: Config,
|
||||
) -> std::result::Result<WasmtimeRuntime, WasmError>
|
||||
where
|
||||
H: HostFunctions,
|
||||
{
|
||||
do_create_runtime::<H>(CodeSupplyMode::Precompiled(compiled_artifact_path), config)
|
||||
}
|
||||
|
||||
/// The same as [`create_runtime`] but takes the bytes of a precompiled artifact,
|
||||
/// which makes this function considerably faster than [`create_runtime`],
|
||||
/// but slower than the more optimized [`create_runtime_from_artifact`].
|
||||
/// This is especially slow on non-Linux Unix systems. Useful in very niche cases.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that the compiled artifact passed here was:
|
||||
/// 1) produced by [`prepare_runtime_artifact`],
|
||||
/// 2) was not modified,
|
||||
///
|
||||
/// Failure to adhere to these requirements might lead to crashes and arbitrary code execution.
|
||||
///
|
||||
/// It is ok though if the compiled artifact was created by code of another version or with
|
||||
/// different configuration flags. In such case the caller will receive an `Err` deterministically.
|
||||
pub unsafe fn create_runtime_from_artifact_bytes<H>(
|
||||
compiled_artifact_bytes: &[u8],
|
||||
config: Config,
|
||||
) -> std::result::Result<WasmtimeRuntime, WasmError>
|
||||
where
|
||||
H: HostFunctions,
|
||||
{
|
||||
do_create_runtime::<H>(CodeSupplyMode::PrecompiledBytes(compiled_artifact_bytes), config)
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This is only unsafe if called with [`CodeSupplyMode::Artifact`]. See
|
||||
/// [`create_runtime_from_artifact`] to get more details.
|
||||
unsafe fn do_create_runtime<H>(
|
||||
code_supply_mode: CodeSupplyMode<'_>,
|
||||
mut config: Config,
|
||||
) -> std::result::Result<WasmtimeRuntime, WasmError>
|
||||
where
|
||||
H: HostFunctions,
|
||||
{
|
||||
replace_strategy_if_broken(&mut config.semantics.instantiation_strategy);
|
||||
|
||||
let mut wasmtime_config = common_config(&config.semantics)?;
|
||||
if let Some(ref cache_path) = config.cache_path {
|
||||
if let Err(reason) = setup_wasmtime_caching(cache_path, &mut wasmtime_config) {
|
||||
log::warn!(
|
||||
"failed to setup wasmtime cache. Performance may degrade significantly: {}.",
|
||||
reason,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let engine = Engine::new(&wasmtime_config)
|
||||
.map_err(|e| WasmError::Other(format!("cannot create the wasmtime engine: {:#}", e)))?;
|
||||
|
||||
let (module, instantiation_strategy) = match code_supply_mode {
|
||||
CodeSupplyMode::Fresh(blob) => {
|
||||
let blob = prepare_blob_for_compilation(blob, &config.semantics)?;
|
||||
let serialized_blob = blob.clone().serialize();
|
||||
|
||||
let module = wasmtime::Module::new(&engine, &serialized_blob)
|
||||
.map_err(|e| WasmError::Other(format!("cannot create module: {:#}", e)))?;
|
||||
|
||||
match config.semantics.instantiation_strategy {
|
||||
InstantiationStrategy::Pooling |
|
||||
InstantiationStrategy::PoolingCopyOnWrite |
|
||||
InstantiationStrategy::RecreateInstance |
|
||||
InstantiationStrategy::RecreateInstanceCopyOnWrite =>
|
||||
(module, InternalInstantiationStrategy::Builtin),
|
||||
}
|
||||
},
|
||||
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.
|
||||
//
|
||||
// See [`create_runtime_from_artifact`] for more details.
|
||||
let module = wasmtime::Module::deserialize_file(&engine, compiled_artifact_path)
|
||||
.map_err(|e| WasmError::Other(format!("cannot deserialize module: {:#}", e)))?;
|
||||
|
||||
(module, InternalInstantiationStrategy::Builtin)
|
||||
},
|
||||
CodeSupplyMode::PrecompiledBytes(compiled_artifact_bytes) => {
|
||||
// SAFETY: The unsafety of `deserialize` is covered by this function. The
|
||||
// responsibilities to maintain the invariants are passed to the caller.
|
||||
//
|
||||
// See [`create_runtime_from_artifact_bytes`] for more details.
|
||||
let module = wasmtime::Module::deserialize(&engine, compiled_artifact_bytes)
|
||||
.map_err(|e| WasmError::Other(format!("cannot deserialize module: {:#}", e)))?;
|
||||
|
||||
(module, InternalInstantiationStrategy::Builtin)
|
||||
},
|
||||
};
|
||||
|
||||
let mut linker = wasmtime::Linker::new(&engine);
|
||||
crate::imports::prepare_imports::<H>(&mut linker, &module, config.allow_missing_func_imports)?;
|
||||
|
||||
let instance_pre = linker
|
||||
.instantiate_pre(&module)
|
||||
.map_err(|e| WasmError::Other(format!("cannot preinstantiate module: {:#}", e)))?;
|
||||
|
||||
Ok(WasmtimeRuntime {
|
||||
engine,
|
||||
instance_pre: Arc::new(instance_pre),
|
||||
instantiation_strategy,
|
||||
instance_counter: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn prepare_blob_for_compilation(
|
||||
mut blob: RuntimeBlob,
|
||||
semantics: &Semantics,
|
||||
) -> std::result::Result<RuntimeBlob, WasmError> {
|
||||
if let Some(DeterministicStackLimit { logical_max, .. }) = semantics.deterministic_stack_limit {
|
||||
blob = blob.inject_stack_depth_metering(logical_max)?;
|
||||
}
|
||||
|
||||
// We don't actually need the memory to be imported so we can just convert any memory
|
||||
// import into an export with impunity. This simplifies our code since `wasmtime` will
|
||||
// now automatically take care of creating the memory for us, and it is also necessary
|
||||
// to enable `wasmtime`'s instance pooling. (Imported memories are ineligible for pooling.)
|
||||
blob.convert_memory_import_into_export()?;
|
||||
blob.setup_memory_according_to_heap_alloc_strategy(semantics.heap_alloc_strategy)?;
|
||||
|
||||
Ok(blob)
|
||||
}
|
||||
|
||||
/// Takes a [`RuntimeBlob`] and precompiles it returning the serialized result of compilation. It
|
||||
/// can then be used for calling [`create_runtime`] avoiding long compilation times.
|
||||
pub fn prepare_runtime_artifact(
|
||||
blob: RuntimeBlob,
|
||||
semantics: &Semantics,
|
||||
) -> std::result::Result<Vec<u8>, WasmError> {
|
||||
let mut semantics = semantics.clone();
|
||||
replace_strategy_if_broken(&mut semantics.instantiation_strategy);
|
||||
|
||||
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)))?;
|
||||
|
||||
engine
|
||||
.precompile_module(&blob.serialize())
|
||||
.map_err(|e| WasmError::Other(format!("cannot precompile module: {:#}", e)))
|
||||
}
|
||||
|
||||
fn perform_call(
|
||||
data: &[u8],
|
||||
instance_wrapper: &mut InstanceWrapper,
|
||||
entrypoint: EntryPoint,
|
||||
mut allocator: FreeingBumpHeapAllocator,
|
||||
allocation_stats: &mut Option<AllocationStats>,
|
||||
) -> Result<Vec<u8>> {
|
||||
let (data_ptr, data_len) = inject_input_data(instance_wrapper, &mut allocator, data)?;
|
||||
|
||||
let host_state = HostState::new(allocator);
|
||||
|
||||
// Set the host state before calling into wasm.
|
||||
instance_wrapper.store_mut().data_mut().host_state = Some(host_state);
|
||||
|
||||
let ret = entrypoint
|
||||
.call(instance_wrapper.store_mut(), data_ptr, data_len)
|
||||
.map(unpack_ptr_and_len);
|
||||
|
||||
// Reset the host state
|
||||
let host_state = instance_wrapper.store_mut().data_mut().host_state.take().expect(
|
||||
"the host state is always set before calling into WASM so it can't be None here; qed",
|
||||
);
|
||||
*allocation_stats = Some(host_state.allocation_stats());
|
||||
|
||||
let (output_ptr, output_len) = ret?;
|
||||
let output = extract_output_data(instance_wrapper, output_ptr, output_len)?;
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn inject_input_data(
|
||||
instance: &mut InstanceWrapper,
|
||||
allocator: &mut FreeingBumpHeapAllocator,
|
||||
data: &[u8],
|
||||
) -> Result<(Pointer<u8>, WordSize)> {
|
||||
let mut ctx = instance.store_mut();
|
||||
let memory = ctx.data().memory();
|
||||
let data_len = data.len() as WordSize;
|
||||
let data_ptr = allocator.allocate(&mut MemoryWrapper(&memory, &mut ctx), data_len)?;
|
||||
util::write_memory_from(instance.store_mut(), data_ptr, data)?;
|
||||
Ok((data_ptr, data_len))
|
||||
}
|
||||
|
||||
fn extract_output_data(
|
||||
instance: &InstanceWrapper,
|
||||
output_ptr: u32,
|
||||
output_len: u32,
|
||||
) -> Result<Vec<u8>> {
|
||||
let ctx = instance.store();
|
||||
|
||||
// Do a length check before allocating. The returned output should not be bigger than the
|
||||
// available WASM memory. Otherwise, a malicious teyrchain can trigger a large allocation,
|
||||
// potentially causing memory exhaustion.
|
||||
//
|
||||
// Get the size of the WASM memory in bytes.
|
||||
let memory_size = ctx.as_context().data().memory().data_size(ctx);
|
||||
if checked_range(output_ptr as usize, output_len as usize, memory_size).is_none() {
|
||||
Err(Error::OutputExceedsBounds)?
|
||||
}
|
||||
let mut output = vec![0; output_len as usize];
|
||||
|
||||
util::read_memory_into(ctx, Pointer::new(output_ptr), &mut output)?;
|
||||
Ok(output)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,522 @@
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
use codec::{Decode as _, Encode as _};
|
||||
use sc_executor_common::{
|
||||
error::Error,
|
||||
runtime_blob::RuntimeBlob,
|
||||
wasm_runtime::{HeapAllocStrategy, WasmModule, DEFAULT_HEAP_ALLOC_STRATEGY},
|
||||
};
|
||||
use sc_runtime_test::wasm_binary_unwrap;
|
||||
|
||||
use crate::InstantiationStrategy;
|
||||
|
||||
type HostFunctions = sp_io::SubstrateHostFunctions;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! test_wasm_execution {
|
||||
($method_name:ident) => {
|
||||
paste::item! {
|
||||
#[test]
|
||||
fn [<$method_name _recreate_instance_cow>]() {
|
||||
$method_name(
|
||||
InstantiationStrategy::RecreateInstanceCopyOnWrite
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn [<$method_name _recreate_instance_vanilla>]() {
|
||||
$method_name(
|
||||
InstantiationStrategy::RecreateInstance
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn [<$method_name _pooling_cow>]() {
|
||||
$method_name(
|
||||
InstantiationStrategy::PoolingCopyOnWrite
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn [<$method_name _pooling_vanilla>]() {
|
||||
$method_name(
|
||||
InstantiationStrategy::Pooling
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
struct RuntimeBuilder {
|
||||
code: Option<String>,
|
||||
instantiation_strategy: InstantiationStrategy,
|
||||
canonicalize_nans: bool,
|
||||
deterministic_stack: bool,
|
||||
heap_pages: HeapAllocStrategy,
|
||||
precompile_runtime: bool,
|
||||
tmpdir: Option<tempfile::TempDir>,
|
||||
}
|
||||
|
||||
impl RuntimeBuilder {
|
||||
fn new(instantiation_strategy: InstantiationStrategy) -> Self {
|
||||
Self {
|
||||
code: None,
|
||||
instantiation_strategy,
|
||||
canonicalize_nans: false,
|
||||
deterministic_stack: false,
|
||||
heap_pages: DEFAULT_HEAP_ALLOC_STRATEGY,
|
||||
precompile_runtime: false,
|
||||
tmpdir: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn use_wat(mut self, code: String) -> Self {
|
||||
self.code = Some(code);
|
||||
self
|
||||
}
|
||||
|
||||
fn canonicalize_nans(mut self, canonicalize_nans: bool) -> Self {
|
||||
self.canonicalize_nans = canonicalize_nans;
|
||||
self
|
||||
}
|
||||
|
||||
fn deterministic_stack(mut self, deterministic_stack: bool) -> Self {
|
||||
self.deterministic_stack = deterministic_stack;
|
||||
self
|
||||
}
|
||||
|
||||
fn precompile_runtime(mut self, precompile_runtime: bool) -> Self {
|
||||
self.precompile_runtime = precompile_runtime;
|
||||
self
|
||||
}
|
||||
|
||||
fn heap_alloc_strategy(mut self, heap_pages: HeapAllocStrategy) -> Self {
|
||||
self.heap_pages = heap_pages;
|
||||
self
|
||||
}
|
||||
|
||||
fn build(&mut self) -> impl WasmModule + '_ {
|
||||
let blob = {
|
||||
let wasm: Vec<u8>;
|
||||
|
||||
let wasm = match self.code {
|
||||
None => wasm_binary_unwrap(),
|
||||
Some(ref wat) => {
|
||||
wasm = wat::parse_str(wat).expect("wat parsing failed");
|
||||
&wasm
|
||||
},
|
||||
};
|
||||
|
||||
RuntimeBlob::uncompress_if_needed(&wasm)
|
||||
.expect("failed to create a runtime blob out of test runtime")
|
||||
};
|
||||
|
||||
let config = crate::Config {
|
||||
allow_missing_func_imports: true,
|
||||
cache_path: None,
|
||||
semantics: crate::Semantics {
|
||||
instantiation_strategy: self.instantiation_strategy,
|
||||
deterministic_stack_limit: match self.deterministic_stack {
|
||||
true => Some(crate::DeterministicStackLimit {
|
||||
logical_max: 65536,
|
||||
native_stack_max: 256 * 1024 * 1024,
|
||||
}),
|
||||
false => None,
|
||||
},
|
||||
canonicalize_nans: self.canonicalize_nans,
|
||||
parallel_compilation: true,
|
||||
heap_alloc_strategy: self.heap_pages,
|
||||
wasm_multi_value: false,
|
||||
wasm_bulk_memory: false,
|
||||
wasm_reference_types: false,
|
||||
wasm_simd: false,
|
||||
},
|
||||
};
|
||||
|
||||
if self.precompile_runtime {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("runtime.bin");
|
||||
|
||||
// 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();
|
||||
std::fs::write(&path, artifact).unwrap();
|
||||
unsafe { crate::create_runtime_from_artifact::<HostFunctions>(&path, config) }
|
||||
} else {
|
||||
crate::create_runtime::<HostFunctions>(blob, config)
|
||||
}
|
||||
.expect("cannot create runtime")
|
||||
}
|
||||
}
|
||||
|
||||
fn deep_call_stack_wat(depth: usize) -> String {
|
||||
format!(
|
||||
r#"
|
||||
(module
|
||||
(memory $0 32)
|
||||
(export "memory" (memory $0))
|
||||
(global (export "__heap_base") i32 (i32.const 0))
|
||||
(func (export "overflow") call 0)
|
||||
|
||||
(func $overflow (param $0 i32)
|
||||
(block $label$1
|
||||
(br_if $label$1
|
||||
(i32.ge_u
|
||||
(local.get $0)
|
||||
(i32.const {depth})
|
||||
)
|
||||
)
|
||||
(call $overflow
|
||||
(i32.add
|
||||
(local.get $0)
|
||||
(i32.const 1)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
(func (export "main")
|
||||
(param i32 i32) (result i64)
|
||||
(call $overflow (i32.const 0))
|
||||
(i64.const 0)
|
||||
)
|
||||
)
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
// These two tests ensure that the `wasmtime`'s stack size limit and the amount of
|
||||
// stack space used by a single stack frame doesn't suddenly change without us noticing.
|
||||
//
|
||||
// If they do (e.g. because we've pulled in a new version of `wasmtime`) we want to know
|
||||
// that it did, regardless of how small the change was.
|
||||
//
|
||||
// If these tests starting failing it doesn't necessarily mean that something is broken;
|
||||
// what it means is that one (or multiple) of the following has to be done:
|
||||
// a) the tests may need to be updated for the new call depth,
|
||||
// b) the stack limit may need to be changed to maintain backwards compatibility,
|
||||
// c) the root cause of the new call depth limit determined, and potentially fixed,
|
||||
// d) the new call depth limit may need to be validated to ensure it doesn't prevent any
|
||||
// existing chain from syncing (if it was effectively decreased)
|
||||
|
||||
// We need two limits here since depending on whether the code is compiled in debug
|
||||
// or in release mode the maximum call depth is slightly different.
|
||||
const CALL_DEPTH_LOWER_LIMIT: usize = 65451;
|
||||
const CALL_DEPTH_UPPER_LIMIT: usize = 65506;
|
||||
|
||||
test_wasm_execution!(test_consume_under_1mb_of_stack_does_not_trap);
|
||||
fn test_consume_under_1mb_of_stack_does_not_trap(instantiation_strategy: InstantiationStrategy) {
|
||||
let wat = deep_call_stack_wat(CALL_DEPTH_LOWER_LIMIT);
|
||||
let mut builder = RuntimeBuilder::new(instantiation_strategy).use_wat(wat);
|
||||
let runtime = builder.build();
|
||||
let mut instance = runtime.new_instance().expect("failed to instantiate a runtime");
|
||||
instance.call_export("main", &[]).unwrap();
|
||||
}
|
||||
|
||||
test_wasm_execution!(test_consume_over_1mb_of_stack_does_trap);
|
||||
fn test_consume_over_1mb_of_stack_does_trap(instantiation_strategy: InstantiationStrategy) {
|
||||
let wat = deep_call_stack_wat(CALL_DEPTH_UPPER_LIMIT + 1);
|
||||
let mut builder = RuntimeBuilder::new(instantiation_strategy).use_wat(wat);
|
||||
let runtime = builder.build();
|
||||
let mut instance = runtime.new_instance().expect("failed to instantiate a runtime");
|
||||
match instance.call_export("main", &[]).unwrap_err() {
|
||||
Error::AbortedDueToTrap(error) => {
|
||||
let expected = "wasm trap: call stack exhausted";
|
||||
assert_eq!(error.message, expected);
|
||||
},
|
||||
error => panic!("unexpected error: {:?}", error),
|
||||
}
|
||||
}
|
||||
|
||||
test_wasm_execution!(test_nan_canonicalization);
|
||||
fn test_nan_canonicalization(instantiation_strategy: InstantiationStrategy) {
|
||||
let mut builder = RuntimeBuilder::new(instantiation_strategy).canonicalize_nans(true);
|
||||
let runtime = builder.build();
|
||||
|
||||
let mut instance = runtime.new_instance().expect("failed to instantiate a runtime");
|
||||
|
||||
/// A NaN with canonical payload bits.
|
||||
const CANONICAL_NAN_BITS: u32 = 0x7fc00000;
|
||||
/// A NaN value with an arbitrary payload.
|
||||
const ARBITRARY_NAN_BITS: u32 = 0x7f812345;
|
||||
|
||||
// This test works like this: we essentially do
|
||||
//
|
||||
// a + b
|
||||
//
|
||||
// where
|
||||
//
|
||||
// * a is a nan with arbitrary bits in its payload
|
||||
// * b is 1.
|
||||
//
|
||||
// according to the wasm spec, if one of the inputs to the operation is a non-canonical NaN
|
||||
// then the value be a NaN with non-deterministic payload bits.
|
||||
//
|
||||
// However, with the `canonicalize_nans` option turned on above, we expect that the output will
|
||||
// be a canonical NaN.
|
||||
//
|
||||
// We extrapolate the results of this tests so that we assume that all intermediate computations
|
||||
// that involve floats are sanitized and cannot produce a non-deterministic NaN.
|
||||
|
||||
let params = (u32::to_le_bytes(ARBITRARY_NAN_BITS), u32::to_le_bytes(1)).encode();
|
||||
let res = {
|
||||
let raw_result = instance.call_export("test_fp_f32add", ¶ms).unwrap();
|
||||
u32::from_le_bytes(<[u8; 4]>::decode(&mut &raw_result[..]).unwrap())
|
||||
};
|
||||
assert_eq!(res, CANONICAL_NAN_BITS);
|
||||
}
|
||||
|
||||
test_wasm_execution!(test_stack_depth_reaching);
|
||||
fn test_stack_depth_reaching(instantiation_strategy: InstantiationStrategy) {
|
||||
const TEST_GUARD_PAGE_SKIP: &str = include_str!("test-guard-page-skip.wat");
|
||||
|
||||
let mut builder = RuntimeBuilder::new(instantiation_strategy)
|
||||
.use_wat(TEST_GUARD_PAGE_SKIP.to_string())
|
||||
.deterministic_stack(true);
|
||||
|
||||
let runtime = builder.build();
|
||||
let mut instance = runtime.new_instance().expect("failed to instantiate a runtime");
|
||||
|
||||
match instance.call_export("test-many-locals", &[]).unwrap_err() {
|
||||
Error::AbortedDueToTrap(error) => {
|
||||
let expected = "wasm trap: wasm `unreachable` instruction executed";
|
||||
assert_eq!(error.message, expected);
|
||||
},
|
||||
error => panic!("unexpected error: {:?}", error),
|
||||
}
|
||||
}
|
||||
|
||||
test_wasm_execution!(test_max_memory_pages_imported_memory_without_precompilation);
|
||||
fn test_max_memory_pages_imported_memory_without_precompilation(
|
||||
instantiation_strategy: InstantiationStrategy,
|
||||
) {
|
||||
test_max_memory_pages(instantiation_strategy, true, false);
|
||||
}
|
||||
|
||||
test_wasm_execution!(test_max_memory_pages_exported_memory_without_precompilation);
|
||||
fn test_max_memory_pages_exported_memory_without_precompilation(
|
||||
instantiation_strategy: InstantiationStrategy,
|
||||
) {
|
||||
test_max_memory_pages(instantiation_strategy, false, false);
|
||||
}
|
||||
|
||||
test_wasm_execution!(test_max_memory_pages_imported_memory_with_precompilation);
|
||||
fn test_max_memory_pages_imported_memory_with_precompilation(
|
||||
instantiation_strategy: InstantiationStrategy,
|
||||
) {
|
||||
test_max_memory_pages(instantiation_strategy, true, true);
|
||||
}
|
||||
|
||||
test_wasm_execution!(test_max_memory_pages_exported_memory_with_precompilation);
|
||||
fn test_max_memory_pages_exported_memory_with_precompilation(
|
||||
instantiation_strategy: InstantiationStrategy,
|
||||
) {
|
||||
test_max_memory_pages(instantiation_strategy, false, true);
|
||||
}
|
||||
|
||||
fn test_max_memory_pages(
|
||||
instantiation_strategy: InstantiationStrategy,
|
||||
import_memory: bool,
|
||||
precompile_runtime: bool,
|
||||
) {
|
||||
fn call(
|
||||
heap_alloc_strategy: HeapAllocStrategy,
|
||||
wat: String,
|
||||
instantiation_strategy: InstantiationStrategy,
|
||||
precompile_runtime: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut builder = RuntimeBuilder::new(instantiation_strategy)
|
||||
.use_wat(wat)
|
||||
.heap_alloc_strategy(heap_alloc_strategy)
|
||||
.precompile_runtime(precompile_runtime);
|
||||
|
||||
let runtime = builder.build();
|
||||
let mut instance = runtime.new_instance().unwrap();
|
||||
instance.call_export("main", &[])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn memory(initial: u32, maximum: u32, import: bool) -> String {
|
||||
let memory = format!("(memory $0 {} {})", initial, maximum);
|
||||
|
||||
if import {
|
||||
format!("(import \"env\" \"memory\" {})", memory)
|
||||
} else {
|
||||
format!("{}\n(export \"memory\" (memory $0))", memory)
|
||||
}
|
||||
}
|
||||
|
||||
let assert_grow_ok = |alloc_strategy: HeapAllocStrategy, initial_pages: u32, max_pages: u32| {
|
||||
eprintln!("assert_grow_ok({alloc_strategy:?}, {initial_pages}, {max_pages})");
|
||||
|
||||
call(
|
||||
alloc_strategy,
|
||||
format!(
|
||||
r#"
|
||||
(module
|
||||
{}
|
||||
(global (export "__heap_base") i32 (i32.const 0))
|
||||
(func (export "main")
|
||||
(param i32 i32) (result i64)
|
||||
|
||||
;; assert(memory.grow returns != -1)
|
||||
(if
|
||||
(i32.eq
|
||||
(memory.grow
|
||||
(i32.const 1)
|
||||
)
|
||||
(i32.const -1)
|
||||
)
|
||||
(then
|
||||
(unreachable)
|
||||
)
|
||||
)
|
||||
|
||||
(i64.const 0)
|
||||
)
|
||||
)
|
||||
"#,
|
||||
memory(initial_pages, max_pages, import_memory)
|
||||
),
|
||||
instantiation_strategy,
|
||||
precompile_runtime,
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
let assert_grow_fail =
|
||||
|alloc_strategy: HeapAllocStrategy, initial_pages: u32, max_pages: u32| {
|
||||
eprintln!("assert_grow_fail({alloc_strategy:?}, {initial_pages}, {max_pages})");
|
||||
|
||||
call(
|
||||
alloc_strategy,
|
||||
format!(
|
||||
r#"
|
||||
(module
|
||||
{}
|
||||
(global (export "__heap_base") i32 (i32.const 0))
|
||||
(func (export "main")
|
||||
(param i32 i32) (result i64)
|
||||
|
||||
;; assert(memory.grow returns == -1)
|
||||
(if
|
||||
(i32.ne
|
||||
(memory.grow
|
||||
(i32.const 1)
|
||||
)
|
||||
(i32.const -1)
|
||||
)
|
||||
(then
|
||||
(unreachable)
|
||||
)
|
||||
)
|
||||
|
||||
(i64.const 0)
|
||||
)
|
||||
)
|
||||
"#,
|
||||
memory(initial_pages, max_pages, import_memory)
|
||||
),
|
||||
instantiation_strategy,
|
||||
precompile_runtime,
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
assert_grow_ok(HeapAllocStrategy::Dynamic { maximum_pages: Some(10) }, 1, 10);
|
||||
assert_grow_ok(HeapAllocStrategy::Dynamic { maximum_pages: Some(10) }, 9, 10);
|
||||
assert_grow_fail(HeapAllocStrategy::Dynamic { maximum_pages: Some(10) }, 10, 10);
|
||||
|
||||
assert_grow_ok(HeapAllocStrategy::Dynamic { maximum_pages: None }, 1, 10);
|
||||
assert_grow_ok(HeapAllocStrategy::Dynamic { maximum_pages: None }, 9, 10);
|
||||
assert_grow_ok(HeapAllocStrategy::Dynamic { maximum_pages: None }, 10, 10);
|
||||
|
||||
assert_grow_fail(HeapAllocStrategy::Static { extra_pages: 10 }, 1, 10);
|
||||
assert_grow_fail(HeapAllocStrategy::Static { extra_pages: 10 }, 9, 10);
|
||||
assert_grow_fail(HeapAllocStrategy::Static { extra_pages: 10 }, 10, 10);
|
||||
}
|
||||
|
||||
// This test takes quite a while to execute in a debug build (over 6 minutes on a TR 3970x)
|
||||
// so it's ignored by default unless it was compiled with `--release`.
|
||||
#[cfg_attr(build_profile = "debug", ignore)]
|
||||
#[test]
|
||||
fn test_instances_without_reuse_are_not_leaked() {
|
||||
let runtime = crate::create_runtime::<HostFunctions>(
|
||||
RuntimeBlob::uncompress_if_needed(wasm_binary_unwrap()).unwrap(),
|
||||
crate::Config {
|
||||
allow_missing_func_imports: true,
|
||||
cache_path: None,
|
||||
semantics: crate::Semantics {
|
||||
instantiation_strategy: InstantiationStrategy::RecreateInstance,
|
||||
deterministic_stack_limit: None,
|
||||
canonicalize_nans: false,
|
||||
parallel_compilation: true,
|
||||
heap_alloc_strategy: DEFAULT_HEAP_ALLOC_STRATEGY,
|
||||
wasm_multi_value: false,
|
||||
wasm_bulk_memory: false,
|
||||
wasm_reference_types: false,
|
||||
wasm_simd: false,
|
||||
},
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// As long as the `wasmtime`'s `Store` lives the instances spawned through it
|
||||
// will live indefinitely. Currently it has a maximum limit of 10k instances,
|
||||
// so let's spawn 10k + 1 of them to make sure our code doesn't keep the `Store`
|
||||
// alive longer than it is necessary. (And since we disabled instance reuse
|
||||
// a new instance will be spawned on each call.)
|
||||
let mut instance = runtime.new_instance().unwrap();
|
||||
for _ in 0..10001 {
|
||||
instance.call_export("test_empty_return", &[0]).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rustix_version_matches_with_wasmtime() {
|
||||
let metadata = cargo_metadata::MetadataCommand::new().exec().unwrap();
|
||||
|
||||
let wasmtime_rustix = metadata
|
||||
.packages
|
||||
.iter()
|
||||
.find(|pkg| pkg.name == "wasmtime")
|
||||
.unwrap()
|
||||
.dependencies
|
||||
.iter()
|
||||
.find(|dep| dep.name == "rustix")
|
||||
.unwrap();
|
||||
let our_rustix = metadata
|
||||
.packages
|
||||
.iter()
|
||||
.find(|pkg| pkg.name == "sc-executor-wasmtime")
|
||||
.unwrap()
|
||||
.dependencies
|
||||
.iter()
|
||||
.find(|dep| dep.name == "rustix")
|
||||
.unwrap();
|
||||
|
||||
if wasmtime_rustix.req != our_rustix.req {
|
||||
panic!(
|
||||
"our version of rustix ({0}) doesn't match wasmtime's ({1}); \
|
||||
bump the version in `sc-executor-wasmtime`'s `Cargo.toml' to '{1}' and try again",
|
||||
our_rustix.req, wasmtime_rustix.req,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::{runtime::StoreData, InstantiationStrategy};
|
||||
use sc_executor_common::{
|
||||
error::{Error, Result},
|
||||
util::checked_range,
|
||||
};
|
||||
use sp_wasm_interface::Pointer;
|
||||
use wasmtime::{AsContext, AsContextMut};
|
||||
|
||||
/// Read data from the instance memory into a slice.
|
||||
///
|
||||
/// Returns an error if the read would go out of the memory bounds.
|
||||
pub(crate) fn read_memory_into(
|
||||
ctx: impl AsContext<Data = StoreData>,
|
||||
address: Pointer<u8>,
|
||||
dest: &mut [u8],
|
||||
) -> Result<()> {
|
||||
let memory = ctx.as_context().data().memory().data(&ctx);
|
||||
|
||||
let range = checked_range(address.into(), dest.len(), memory.len())
|
||||
.ok_or_else(|| Error::Other("memory read is out of bounds".into()))?;
|
||||
dest.copy_from_slice(&memory[range]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write data to the instance memory from a slice.
|
||||
///
|
||||
/// Returns an error if the write would go out of the memory bounds.
|
||||
pub(crate) fn write_memory_from(
|
||||
mut ctx: impl AsContextMut<Data = StoreData>,
|
||||
address: Pointer<u8>,
|
||||
data: &[u8],
|
||||
) -> Result<()> {
|
||||
let memory = ctx.as_context().data().memory();
|
||||
let memory = memory.data_mut(&mut ctx);
|
||||
|
||||
let range = checked_range(address.into(), data.len(), memory.len())
|
||||
.ok_or_else(|| Error::Other("memory write is out of bounds".into()))?;
|
||||
memory[range].copy_from_slice(data);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks whether the `madvise(MADV_DONTNEED)` works as expected.
|
||||
///
|
||||
/// In certain environments (e.g. when running under the QEMU user-mode emulator)
|
||||
/// this syscall is broken.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn is_madvise_working() -> std::result::Result<bool, String> {
|
||||
let page_size = rustix::param::page_size();
|
||||
|
||||
unsafe {
|
||||
// Allocate two memory pages.
|
||||
let pointer = rustix::mm::mmap_anonymous(
|
||||
std::ptr::null_mut(),
|
||||
2 * page_size,
|
||||
rustix::mm::ProtFlags::READ | rustix::mm::ProtFlags::WRITE,
|
||||
rustix::mm::MapFlags::PRIVATE,
|
||||
)
|
||||
.map_err(|error| format!("mmap failed: {}", error))?;
|
||||
|
||||
// Dirty them both.
|
||||
std::ptr::write_volatile(pointer.cast::<u8>(), b'A');
|
||||
std::ptr::write_volatile(pointer.cast::<u8>().add(page_size), b'B');
|
||||
|
||||
// Clear the first page.
|
||||
let result_madvise =
|
||||
rustix::mm::madvise(pointer, page_size, rustix::mm::Advice::LinuxDontNeed)
|
||||
.map_err(|error| format!("madvise failed: {}", error));
|
||||
|
||||
// Fetch the values.
|
||||
let value_1 = std::ptr::read_volatile(pointer.cast::<u8>());
|
||||
let value_2 = std::ptr::read_volatile(pointer.cast::<u8>().add(page_size));
|
||||
|
||||
let result_munmap = rustix::mm::munmap(pointer, 2 * page_size)
|
||||
.map_err(|error| format!("munmap failed: {}", error));
|
||||
|
||||
result_madvise?;
|
||||
result_munmap?;
|
||||
|
||||
// Verify that the first page was cleared, while the second one was not.
|
||||
Ok(value_1 == 0 && value_2 == b'B')
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn test_is_madvise_working_check_does_not_fail() {
|
||||
assert!(is_madvise_working().is_ok());
|
||||
}
|
||||
|
||||
/// Checks whether a given instantiation strategy can be safely used, and replaces
|
||||
/// it with a slower (but sound) alternative if it isn't.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) fn replace_strategy_if_broken(strategy: &mut InstantiationStrategy) {
|
||||
let replacement_strategy = match *strategy {
|
||||
// These strategies don't need working `madvise`.
|
||||
InstantiationStrategy::Pooling | InstantiationStrategy::RecreateInstance => return,
|
||||
|
||||
// These strategies require a working `madvise` to be sound.
|
||||
InstantiationStrategy::PoolingCopyOnWrite => InstantiationStrategy::Pooling,
|
||||
InstantiationStrategy::RecreateInstanceCopyOnWrite =>
|
||||
InstantiationStrategy::RecreateInstance,
|
||||
};
|
||||
|
||||
use std::sync::OnceLock;
|
||||
static IS_OK: OnceLock<bool> = OnceLock::new();
|
||||
|
||||
let is_ok = IS_OK.get_or_init(|| {
|
||||
let is_ok = match is_madvise_working() {
|
||||
Ok(is_ok) => is_ok,
|
||||
Err(error) => {
|
||||
// This should never happen.
|
||||
log::warn!("Failed to check whether `madvise(MADV_DONTNEED)` works: {}", error);
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if !is_ok {
|
||||
log::warn!("You're running on a system with a broken `madvise(MADV_DONTNEED)` implementation. This will result in lower performance.");
|
||||
}
|
||||
|
||||
is_ok
|
||||
});
|
||||
|
||||
if !is_ok {
|
||||
*strategy = replacement_strategy;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub(crate) fn replace_strategy_if_broken(_: &mut InstantiationStrategy) {}
|
||||
Reference in New Issue
Block a user