// This file is part of Tetcore.
// Copyright (C) 2019-2021 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 .
//! Traits and accessor functions for calling into the Tetcore 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 std::sync::Arc;
use crate::error::{Error, WasmError};
use parking_lot::Mutex;
use codec::Decode;
use tet_core::traits::{Externalities, RuntimeCode, FetchRuntimeCode};
use tp_version::RuntimeVersion;
use std::panic::AssertUnwindSafe;
use tc_executor_common::wasm_runtime::{WasmModule, WasmInstance};
use tetcore_wasm_interface::Function;
/// Specification of different methods of executing the runtime Wasm code.
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
pub enum WasmExecutionMethod {
/// Uses the Wasmi interpreter.
Interpreted,
/// Uses the Wasmtime compiled runtime.
#[cfg(feature = "wasmtime")]
Compiled,
}
impl Default for WasmExecutionMethod {
fn default() -> WasmExecutionMethod {
WasmExecutionMethod::Interpreted
}
}
/// A Wasm runtime object along with its cached runtime version.
struct VersionedRuntime {
/// Runtime code hash.
code_hash: Vec,
/// Wasm runtime type.
wasm_method: WasmExecutionMethod,
/// Shared runtime that can spawn instances.
module: Arc,
/// The number of WebAssembly heap pages this instance was created with.
heap_pages: u64,
/// Runtime version according to `Core_version` if any.
version: Option,
/// Cached instance pool.
instances: Vec>>>,
}
impl VersionedRuntime {
/// Run the given closure `f` with an instance of this runtime.
fn with_instance<'c, R, F>(
&self,
ext: &mut dyn Externalities,
f: F,
) -> Result
where F: FnOnce(
&Arc,
&dyn WasmInstance,
Option<&RuntimeVersion>,
&mut dyn Externalities)
-> Result,
{
// 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 (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, &*instance, self.version.as_ref(), ext);
if let Err(e) = &result {
if new_inst {
log::warn!(
target: "wasm-runtime",
"Fresh runtime instance failed with {:?}",
e,
)
} else {
log::warn!(
target: "wasm-runtime",
"Evicting failed runtime instance: {:?}",
e,
);
}
} else {
*locked = Some(instance);
if new_inst {
log::debug!(
target: "wasm-runtime",
"Allocated WASM instance {}/{}",
index + 1,
self.instances.len(),
);
}
}
result
},
None => {
log::warn!(target: "wasm-runtime", "Ran out of free WASM instances");
// Allocate a new instance
let instance = self.module.new_instance()?;
f(&self.module, &*instance, self.version.as_ref(), ext)
}
}
}
}
const MAX_RUNTIMES: usize = 2;
/// 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 equal to `MAX_RUNTIMES`.
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<[Option>; MAX_RUNTIMES]>,
/// The size of the instances cache for each runtime.
max_runtime_instances: usize,
}
impl RuntimeCache {
/// Creates a new instance of a runtimes cache.
pub fn new(max_runtime_instances: usize) -> RuntimeCache {
RuntimeCache {
runtimes: Default::default(),
max_runtime_instances,
}
}
/// Prepares a WASM module instance and executes given function for it.
///
/// This uses internal cache to find avaiable instance or create a new one.
/// # Parameters
///
/// `code` - Provides external code or tells the executor to fetch it from storage.
///
/// `runtime_code` - The runtime wasm code used setup the runtime.
///
/// `default_heap_pages` - Number of 64KB pages to allocate for Wasm execution.
///
/// `wasm_method` - Type of WASM backend to use.
///
/// `host_functions` - The host functions that should be registered for the Wasm runtime.
///
/// `allow_missing_func_imports` - Ignore missing function imports.
///
/// `max_runtime_instances` - The size of the instances cache.
///
/// `f` - Function to execute.
///
/// # Returns result of `f` wrapped in an additonal result.
/// In case of failure one of two errors can be returned:
///
/// `Err::InvalidCode` is returned for runtime code issues.
///
/// `Error::InvalidMemoryReference` is returned if no memory export with the
/// identifier `memory` can be found in the runtime.
pub fn with_instance<'c, R, F>(
&self,
runtime_code: &'c RuntimeCode<'c>,
ext: &mut dyn Externalities,
wasm_method: WasmExecutionMethod,
default_heap_pages: u64,
host_functions: &[&'static dyn Function],
allow_missing_func_imports: bool,
f: F,
) -> Result, Error>
where F: FnOnce(
&Arc,
&dyn WasmInstance,
Option<&RuntimeVersion>,
&mut dyn Externalities)
-> Result,
{
let code_hash = &runtime_code.hash;
let heap_pages = runtime_code.heap_pages.unwrap_or(default_heap_pages);
let mut runtimes = self.runtimes.lock(); // this must be released prior to calling f
let pos = runtimes.iter().position(|r| r.as_ref().map_or(
false,
|r| r.wasm_method == wasm_method &&
r.code_hash == *code_hash &&
r.heap_pages == heap_pages
));
let runtime = match pos {
Some(n) => runtimes[n]
.clone()
.expect("`position` only returns `Some` for entries that are `Some`"),
None => {
let code = runtime_code.fetch_runtime_code().ok_or(WasmError::CodeNotFound)?;
let result = create_versioned_wasm_runtime(
&code,
code_hash.clone(),
ext,
wasm_method,
heap_pages,
host_functions.into(),
allow_missing_func_imports,
self.max_runtime_instances,
);
if let Err(ref err) = result {
log::warn!(target: "wasm-runtime", "Cannot create a runtime: {:?}", err);
}
Arc::new(result?)
}
};
// Rearrange runtimes by last recently used.
match pos {
Some(0) => {},
Some(n) => {
for i in (1 .. n + 1).rev() {
runtimes.swap(i, i - 1);
}
}
None => {
runtimes[MAX_RUNTIMES-1] = Some(runtime.clone());
for i in (1 .. MAX_RUNTIMES).rev() {
runtimes.swap(i, i - 1);
}
}
}
drop(runtimes);
Ok(runtime.with_instance(ext, f))
}
}
/// Create a wasm runtime with the given `code`.
pub fn create_wasm_runtime_with_code(
wasm_method: WasmExecutionMethod,
heap_pages: u64,
code: &[u8],
host_functions: Vec<&'static dyn Function>,
allow_missing_func_imports: bool,
) -> Result, WasmError> {
match wasm_method {
WasmExecutionMethod::Interpreted =>
tc_executor_twasmi::create_runtime(
code,
heap_pages,
host_functions,
allow_missing_func_imports
).map(|runtime| -> Arc { Arc::new(runtime) }),
#[cfg(feature = "wasmtime")]
WasmExecutionMethod::Compiled =>
tc_executor_wasmtime::create_runtime(
code,
heap_pages,
host_functions,
allow_missing_func_imports
).map(|runtime| -> Arc { Arc::new(runtime) }),
}
}
fn decode_version(version: &[u8]) -> Result {
let v: RuntimeVersion = tp_api::OldRuntimeVersion::decode(&mut &version[..])
.map_err(|_|
WasmError::Instantiation(
"failed to decode \"Core_version\" result using old runtime version".into(),
)
)?.into();
let core_api_id = tet_core::hashing::blake2_64(b"Core");
if v.has_api_with(&core_api_id, |v| v >= 3) {
tp_api::RuntimeVersion::decode(&mut &version[..])
.map_err(|_|
WasmError::Instantiation("failed to decode \"Core_version\" result".into())
)
} else {
Ok(v)
}
}
fn create_versioned_wasm_runtime(
code: &[u8],
code_hash: Vec,
ext: &mut dyn Externalities,
wasm_method: WasmExecutionMethod,
heap_pages: u64,
host_functions: Vec<&'static dyn Function>,
allow_missing_func_imports: bool,
max_instances: usize,
) -> Result {
#[cfg(not(target_os = "unknown"))]
let time = std::time::Instant::now();
let runtime = create_wasm_runtime_with_code(
wasm_method,
heap_pages,
&code,
host_functions,
allow_missing_func_imports,
)?;
// 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::native_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()))?
};
let version = match version_result {
Ok(version) => Some(decode_version(&version)?),
Err(_) => None,
};
#[cfg(not(target_os = "unknown"))]
log::debug!(
target: "wasm-runtime",
"Prepared new runtime version {:?} in {} ms.",
version,
time.elapsed().as_millis(),
);
let mut instances = Vec::with_capacity(max_instances);
instances.resize_with(max_instances, || Mutex::new(None));
Ok(VersionedRuntime {
code_hash,
module: runtime,
version,
heap_pages,
wasm_method,
instances,
})
}
#[cfg(test)]
mod tests {
use super::*;
use tetcore_wasm_interface::HostFunctions;
use tp_api::{Core, RuntimeApiInfo};
use tetcore_test_runtime::Block;
use codec::Encode;
#[test]
fn host_functions_are_equal() {
let host_functions = tet_io::TetcoreHostFunctions::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 = tp_api::OldRuntimeVersion {
spec_name: "test".into(),
impl_name: "test".into(),
authoring_version: 1,
spec_version: 1,
impl_version: 1,
apis: tp_api::create_apis_vec!([(Core::::ID, 1)]),
};
let version = decode_version(&old_runtime_version.encode()).unwrap();
assert_eq!(1, version.transaction_version);
}
#[test]
fn old_runtime_version_decodes_fails_with_version_3() {
let old_runtime_version = tp_api::OldRuntimeVersion {
spec_name: "test".into(),
impl_name: "test".into(),
authoring_version: 1,
spec_version: 1,
impl_version: 1,
apis: tp_api::create_apis_vec!([(Core::::ID, 3)]),
};
decode_version(&old_runtime_version.encode()).unwrap_err();
}
#[test]
fn new_runtime_version_decodes() {
let old_runtime_version = tp_api::RuntimeVersion {
spec_name: "test".into(),
impl_name: "test".into(),
authoring_version: 1,
spec_version: 1,
impl_version: 1,
apis: tp_api::create_apis_vec!([(Core::::ID, 3)]),
transaction_version: 3,
};
let version = decode_version(&old_runtime_version.encode()).unwrap();
assert_eq!(3, version.transaction_version);
}
}