Add a PolkaVM-based executor (#3458)

This PR adds a new PolkaVM-based executor to Substrate.

- The executor can now be used to actually run a PolkaVM-based runtime,
and successfully produces blocks.
- The executor is always compiled-in, but is disabled by default.
- The `SUBSTRATE_ENABLE_POLKAVM` environment variable must be set to `1`
to enable the executor, in which case the node will accept both WASM and
PolkaVM program blobs (otherwise it'll default to WASM-only). This is
deliberately undocumented and not explicitly exposed anywhere (e.g. in
the command line arguments, or in the API) to disincentivize anyone from
enabling it in production. If/when we'll move this into production usage
I'll remove the environment variable and do it "properly".
- I did not use our legacy runtime allocator for the PolkaVM executor,
so currently every allocation inside of the runtime will leak guest
memory until that particular instance is destroyed. The idea here is
that I will work on the https://github.com/polkadot-fellows/RFCs/pull/4
which will remove the need for the legacy allocator under WASM, and that
will also allow us to use a proper non-leaking allocator under PolkaVM.
- I also did some minor cleanups of the WASM executor and deleted some
dead code.

No prdocs included since this is not intended to be an end-user feature,
but an unofficial experiment, and shouldn't affect any current
production user. Once this is production-ready a full Polkadot
Fellowship RFC will be necessary anyway.
This commit is contained in:
Koute
2024-03-12 14:23:06 +09:00
committed by GitHub
parent 7315a9b8fd
commit b0f34e4b29
20 changed files with 559 additions and 300 deletions
@@ -22,36 +22,12 @@
use std::sync::Arc;
use crate::runtime::{InstanceCounter, ReleaseInstanceHandle, Store, StoreData};
use sc_executor_common::{
error::{Backtrace, Error, MessageWithBacktrace, Result, WasmError},
wasm_runtime::InvokeMethod,
};
use sp_wasm_interface::{Pointer, Value, WordSize};
use wasmtime::{
AsContext, AsContextMut, Engine, Extern, Instance, InstancePre, Memory, Table, Val,
};
/// Invoked entrypoint format.
pub enum EntryPointType {
/// Direct call.
///
/// Call is made by providing only payload reference and length.
Direct { entrypoint: wasmtime::TypedFunc<(u32, u32), u64> },
/// Indirect call.
///
/// Call is made by providing payload reference and length, and extra argument
/// for advanced routing.
Wrapped {
/// The extra argument passed to the runtime. It is typically a wasm function pointer.
func: u32,
dispatcher: wasmtime::TypedFunc<(u32, u32, u32), u64>,
},
}
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 {
call_type: EntryPointType,
}
pub struct EntryPoint(wasmtime::TypedFunc<(u32, u32), u64>);
impl EntryPoint {
/// Call this entry point.
@@ -64,13 +40,7 @@ impl EntryPoint {
let data_ptr = u32::from(data_ptr);
let data_len = u32::from(data_len);
match self.call_type {
EntryPointType::Direct { ref entrypoint } =>
entrypoint.call(&mut *store, (data_ptr, data_len)),
EntryPointType::Wrapped { func, ref dispatcher } =>
dispatcher.call(&mut *store, (func, data_ptr, data_len)),
}
.map_err(|trap| {
self.0.call(&mut *store, (data_ptr, data_len)).map_err(|trap| {
let host_state = store
.data_mut()
.host_state
@@ -99,18 +69,7 @@ impl EntryPoint {
let entrypoint = func
.typed::<(u32, u32), u64>(ctx)
.map_err(|_| "Invalid signature for direct entry point")?;
Ok(Self { call_type: EntryPointType::Direct { entrypoint } })
}
pub fn wrapped(
dispatcher: wasmtime::Func,
func: u32,
ctx: impl AsContext,
) -> std::result::Result<Self, &'static str> {
let dispatcher = dispatcher
.typed::<(u32, u32, u32), u64>(ctx)
.map_err(|_| "Invalid signature for wrapped entry point")?;
Ok(Self { call_type: EntryPointType::Wrapped { func, dispatcher } })
Ok(Self(entrypoint))
}
}
@@ -178,10 +137,8 @@ impl InstanceWrapper {
})?;
let memory = get_linear_memory(&instance, &mut store)?;
let table = get_table(&instance, &mut store);
store.data_mut().memory = Some(memory);
store.data_mut().table = table;
Ok(InstanceWrapper { instance, store, _release_instance_handle })
}
@@ -190,61 +147,17 @@ impl InstanceWrapper {
///
/// An entrypoint must have a signature `(i32, i32) -> i64`, otherwise this function will return
/// an error.
pub fn resolve_entrypoint(&mut self, method: InvokeMethod) -> Result<EntryPoint> {
Ok(match method {
InvokeMethod::Export(method) => {
// 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))
})?
},
InvokeMethod::Table(func_ref) => {
let table = self
.instance
.get_table(&mut self.store, "__indirect_function_table")
.ok_or(Error::NoTable)?;
let val = table
.get(&mut self.store, func_ref)
.ok_or(Error::NoTableEntryWithIndex(func_ref))?;
let func = val
.funcref()
.ok_or(Error::TableElementIsNotAFunction(func_ref))?
.ok_or(Error::FunctionRefIsNull(func_ref))?;
EntryPoint::direct(*func, &self.store).map_err(|_| {
Error::from(format!(
"Function @{} in exported table has invalid signature for direct call.",
func_ref,
))
})?
},
InvokeMethod::TableWithWrapper { dispatcher_ref, func } => {
let table = self
.instance
.get_table(&mut self.store, "__indirect_function_table")
.ok_or(Error::NoTable)?;
let val = table
.get(&mut self.store, dispatcher_ref)
.ok_or(Error::NoTableEntryWithIndex(dispatcher_ref))?;
let dispatcher = val
.funcref()
.ok_or(Error::TableElementIsNotAFunction(dispatcher_ref))?
.ok_or(Error::FunctionRefIsNull(dispatcher_ref))?;
EntryPoint::wrapped(*dispatcher, func, &self.store).map_err(|_| {
Error::from(format!(
"Function @{} in exported table has invalid signature for wrapped call.",
dispatcher_ref,
))
})?
},
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))
})
}
@@ -268,24 +181,6 @@ impl InstanceWrapper {
Ok(heap_base as u32)
}
/// Get the value from a global with the given `name`.
pub fn get_global_val(&mut self, name: &str) -> Result<Option<Value>> {
let global = match self.instance.get_export(&mut self.store, name) {
Some(global) => global,
None => return Ok(None),
};
let global = global.into_global().ok_or_else(|| format!("`{}` is not a global", name))?;
match global.get(&mut self.store) {
Val::I32(val) => Ok(Some(Value::I32(val))),
Val::I64(val) => Ok(Some(Value::I64(val))),
Val::F32(val) => Ok(Some(Value::F32(val))),
Val::F64(val) => Ok(Some(Value::F64(val))),
_ => Err("Unknown value type".into()),
}
}
}
/// Extract linear memory instance from the given instance.
@@ -301,15 +196,6 @@ fn get_linear_memory(instance: &Instance, ctx: impl AsContextMut) -> Result<Memo
Ok(memory)
}
/// Extract the table from the given instance if any.
fn get_table(instance: &Instance, ctx: &mut Store) -> Option<Table> {
instance
.get_export(ctx, "__indirect_function_table")
.as_ref()
.cloned()
.and_then(Extern::into_table)
}
/// Functions related to memory.
impl InstanceWrapper {
pub(crate) fn store(&self) -> &Store {