use crate::error::{Error, WasmError};
use codec::Decode;
use lru::LruCache;
use parking_lot::Mutex;
use sc_executor_common::{
runtime_blob::RuntimeBlob,
wasm_runtime::{WasmInstance, WasmModule},
};
use sp_core::traits::{Externalities, FetchRuntimeCode, RuntimeCode};
use sp_version::RuntimeVersion;
use std::{
num::NonZeroUsize,
panic::AssertUnwindSafe,
path::{Path, PathBuf},
sync::Arc,
};
use sp_wasm_interface::HostFunctions;
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
pub enum WasmExecutionMethod {
Interpreted,
Compiled {
instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy,
},
}
impl Default for WasmExecutionMethod {
fn default() -> WasmExecutionMethod {
WasmExecutionMethod::Interpreted
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
struct VersionedRuntimeId {
code_hash: Vec<u8>,
wasm_method: WasmExecutionMethod,
heap_pages: u64,
}
struct VersionedRuntime {
module: Arc<dyn WasmModule>,
version: Option<RuntimeVersion>,
instances: Arc<Vec<Mutex<Option<Box<dyn WasmInstance>>>>>,
}
impl VersionedRuntime {
fn with_instance<R, F>(&self, ext: &mut dyn Externalities, f: F) -> Result<R, Error>
where
F: FnOnce(
&Arc<dyn WasmModule>,
&mut dyn WasmInstance,
Option<&RuntimeVersion>,
&mut dyn Externalities,
) -> Result<R, Error>,
{
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");
let mut instance = self.module.new_instance()?;
f(&self.module, &mut *instance, self.version.as_ref(), ext)
},
}
}
}
pub struct RuntimeCache {
runtimes: Mutex<LruCache<VersionedRuntimeId, Arc<VersionedRuntime>>>,
max_runtime_instances: usize,
cache_path: Option<PathBuf>,
}
impl RuntimeCache {
pub fn new(
max_runtime_instances: usize,
cache_path: Option<PathBuf>,
runtime_cache_size: u8,
) -> RuntimeCache {
let cap =
NonZeroUsize::new(runtime_cache_size.max(1) as usize).expect("cache size is not zero");
RuntimeCache { runtimes: Mutex::new(LruCache::new(cap)), max_runtime_instances, cache_path }
}
pub fn with_instance<'c, H, R, F>(
&self,
runtime_code: &'c RuntimeCode<'c>,
ext: &mut dyn Externalities,
wasm_method: WasmExecutionMethod,
default_heap_pages: u64,
allow_missing_func_imports: bool,
f: F,
) -> Result<Result<R, Error>, Error>
where
H: HostFunctions,
F: FnOnce(
&Arc<dyn WasmModule>,
&mut dyn WasmInstance,
Option<&RuntimeVersion>,
&mut dyn Externalities,
) -> Result<R, Error>,
{
let code_hash = &runtime_code.hash;
let heap_pages = runtime_code.heap_pages.unwrap_or(default_heap_pages);
let versioned_runtime_id =
VersionedRuntimeId { code_hash: code_hash.clone(), heap_pages, wasm_method };
let mut runtimes = self.runtimes.lock(); 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_pages,
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?);
runtimes.put(versioned_runtime_id, versioned_runtime.clone());
versioned_runtime
};
drop(runtimes);
Ok(versioned_runtime.with_instance(ext, f))
}
}
pub fn create_wasm_runtime_with_code<H>(
wasm_method: WasmExecutionMethod,
heap_pages: u64,
blob: RuntimeBlob,
allow_missing_func_imports: bool,
cache_path: Option<&Path>,
) -> Result<Arc<dyn WasmModule>, WasmError>
where
H: HostFunctions,
{
match wasm_method {
WasmExecutionMethod::Interpreted => {
let _ = cache_path;
sc_executor_wasmi::create_runtime(
blob,
heap_pages,
H::host_functions(),
allow_missing_func_imports,
)
.map(|runtime| -> Arc<dyn WasmModule> { Arc::new(runtime) })
},
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 {
extra_heap_pages: heap_pages,
instantiation_strategy,
deterministic_stack_limit: None,
canonicalize_nans: false,
parallel_compilation: true,
max_memory_size: None,
},
},
)
.map(|runtime| -> Arc<dyn WasmModule> { Arc::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| {
<[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>>()
}
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);
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_pages: u64,
allow_missing_func_imports: bool,
max_instances: usize,
cache_path: Option<&Path>,
) -> Result<VersionedRuntime, WasmError>
where
H: HostFunctions,
{
let blob = sc_executor_common::runtime_blob::RuntimeBlob::uncompress_if_needed(code)?;
let mut version: Option<_> = read_embedded_version(&blob)?;
let runtime = create_wasm_runtime_with_code::<H>(
wasm_method,
heap_pages,
blob,
allow_missing_func_imports,
cache_path,
)?;
if version.is_none() {
let version_result = {
let mut ext = AssertUnwindSafe(ext);
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()))?
};
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: Arc::new(instances) })
}
#[cfg(test)]
mod tests {
use super::*;
use codec::Encode;
use sp_api::{Core, RuntimeApiInfo};
use sp_runtime::RuntimeString;
use sp_wasm_interface::HostFunctions;
use substrate_test_runtime::Block;
#[derive(Encode)]
pub struct OldRuntimeVersion {
pub spec_name: RuntimeString,
pub impl_name: RuntimeString,
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: sp_api::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.state_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: sp_api::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 = sp_api::RuntimeVersion {
spec_name: "test".into(),
impl_name: "test".into(),
authoring_version: 1,
spec_version: 1,
impl_version: 1,
apis: sp_api::create_apis_vec!([(<dyn Core::<Block>>::ID, 3)]),
transaction_version: 3,
state_version: 4,
};
let version = decode_version(&old_runtime_version.encode()).unwrap();
assert_eq!(3, version.transaction_version);
assert_eq!(0, version.state_version);
let old_runtime_version = sp_api::RuntimeVersion {
spec_name: "test".into(),
impl_name: "test".into(),
authoring_version: 1,
spec_version: 1,
impl_version: 1,
apis: sp_api::create_apis_vec!([(<dyn Core::<Block>>::ID, 4)]),
transaction_version: 3,
state_version: 4,
};
let version = decode_version(&old_runtime_version.encode()).unwrap();
assert_eq!(3, version.transaction_version);
assert_eq!(4, version.state_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: sp_api::create_apis_vec!([(<dyn Core::<Block>>::ID, 4)]),
transaction_version: 100,
state_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);
}
}