Skip to content

Commit

Permalink
Add a PolkaVM-based executor (paritytech#3458)
Browse files Browse the repository at this point in the history
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 polkadot-fellows/RFCs#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.
  • Loading branch information
koute authored Mar 12, 2024
1 parent fdcade6 commit 688c25b
Show file tree
Hide file tree
Showing 17 changed files with 494 additions and 281 deletions.
1 change: 1 addition & 0 deletions substrate/client/executor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ tracing = "0.1.29"

codec = { package = "parity-scale-codec", version = "3.6.1" }
sc-executor-common = { path = "common" }
sc-executor-polkavm = { path = "polkavm" }
sc-executor-wasmtime = { path = "wasmtime" }
sp-api = { path = "../../primitives/api" }
sp-core = { path = "../../primitives/core" }
Expand Down
1 change: 1 addition & 0 deletions substrate/client/executor/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ wasm-instrument = "0.4"
sc-allocator = { path = "../../allocator" }
sp-maybe-compressed-blob = { path = "../../../primitives/maybe-compressed-blob" }
sp-wasm-interface = { path = "../../../primitives/wasm-interface" }
polkavm = { workspace = true }

[features]
default = []
18 changes: 18 additions & 0 deletions substrate/client/executor/common/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,24 @@ pub enum WasmError {
Other(String),
}

impl From<polkavm::ProgramParseError> for WasmError {
fn from(error: polkavm::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 {
Expand Down
4 changes: 4 additions & 0 deletions substrate/client/executor/common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ 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")
}
153 changes: 90 additions & 63 deletions substrate/client/executor/common/src/runtime_blob/runtime_blob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,23 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.

use crate::{error::WasmError, wasm_runtime::HeapAllocStrategy};
use wasm_instrument::{
export_mutable_globals,
parity_wasm::elements::{
deserialize_buffer, serialize, ExportEntry, External, Internal, MemorySection, MemoryType,
Module, Section,
},
use wasm_instrument::parity_wasm::elements::{
deserialize_buffer, serialize, ExportEntry, External, Internal, MemorySection, MemoryType,
Module, Section,
};

/// A bunch of information collected from a WebAssembly module.
/// A program blob containing a Substrate runtime.
#[derive(Clone)]
pub struct RuntimeBlob {
raw_module: Module,
pub struct RuntimeBlob(BlobKind);

#[derive(Clone)]
enum BlobKind {
WebAssembly(Module),
PolkaVM(polkavm::ProgramBlob<'static>),
}

impl RuntimeBlob {
/// Create `RuntimeBlob` from the given wasm code. Will attempt to decompress the code before
/// deserializing it.
/// 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> {
Expand All @@ -43,31 +43,26 @@ impl RuntimeBlob {
Self::new(&wasm_code)
}

/// Create `RuntimeBlob` from the given wasm code.
/// Create `RuntimeBlob` from the given WASM or PolkaVM program blob.
///
/// Returns `Err` if the wasm code cannot be deserialized.
pub fn new(wasm_code: &[u8]) -> Result<Self, WasmError> {
let raw_module: Module = deserialize_buffer(wasm_code)
.map_err(|e| WasmError::Other(format!("cannot deserialize module: {:?}", e)))?;
Ok(Self { raw_module })
}

/// The number of globals defined in locally in this module.
pub fn declared_globals_count(&self) -> u32 {
self.raw_module
.global_section()
.map(|gs| gs.entries().len() as u32)
.unwrap_or(0)
}

/// The number of imports of globals.
pub fn imported_globals_count(&self) -> u32 {
self.raw_module.import_section().map(|is| is.globals() as u32).unwrap_or(0)
}
/// 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() {
return Ok(Self(BlobKind::PolkaVM(
polkavm::ProgramBlob::parse(raw_blob)?.into_owned(),
)));
} 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()));
}
}

/// Perform an instrumentation that makes sure that the mutable globals are exported.
pub fn expose_mutable_globals(&mut self) {
export_mutable_globals(&mut self.raw_module, "exported_internal_global");
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
Expand All @@ -80,35 +75,28 @@ impl RuntimeBlob {
///
/// 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.raw_module, stack_depth_limit).map_err(
|e| WasmError::Other(format!("cannot inject the stack limiter: {:?}", e)),
)?;

Ok(Self { raw_module: 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))
})?;

/// Perform an instrumentation that makes sure that a specific function `entry_point` is
/// exported
pub fn entry_point_exists(&self, entry_point: &str) -> bool {
self.raw_module
.export_section()
.map(|e| {
e.entries().iter().any(|e| {
matches!(e.internal(), Internal::Function(_)) && e.field() == entry_point
})
})
.unwrap_or_default()
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 import_section = match self.raw_module.import_section_mut() {
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(()),
};
Expand All @@ -124,7 +112,7 @@ impl RuntimeBlob {
let memory_name = entry.field().to_owned();
import_entries.remove(index);

self.raw_module
raw_module
.insert_section(Section::Memory(MemorySection::with_entries(vec![memory_ty])))
.map_err(|error| {
WasmError::Other(format!(
Expand All @@ -133,14 +121,14 @@ impl RuntimeBlob {
))
})?;

if self.raw_module.export_section_mut().is_none() {
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.
self.raw_module
raw_module
.insert_section(Section::Export(Default::default()))
.expect("an export section can be always inserted if it doesn't exist; qed");
}
self.raw_module
raw_module
.export_section_mut()
.expect("export section already existed or we just added it above, so it always exists; qed")
.entries_mut()
Expand All @@ -156,12 +144,14 @@ impl RuntimeBlob {
///
/// 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 memory_section = self
.raw_module
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()))?;

Expand All @@ -187,20 +177,57 @@ impl RuntimeBlob {

/// 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.raw_module
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> {
serialize(self.raw_module).expect("serializing into a vec should succeed; qed")
match self.0 {
BlobKind::WebAssembly(raw_module) =>
serialize(raw_module).expect("serializing into a vec should succeed; qed"),
BlobKind::PolkaVM(ref blob) => blob.as_bytes().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(),
)),
}
}

/// Destructure this structure into the underlying parity-wasm Module.
pub fn into_inner(self) -> Module {
self.raw_module
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),
}
}
}
50 changes: 2 additions & 48 deletions substrate/client/executor/common/src/wasm_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
//! Definitions for a wasm runtime.

use crate::error::Error;
use sp_wasm_interface::Value;

pub use sc_allocator::AllocationStats;

Expand All @@ -30,46 +29,6 @@ pub const DEFAULT_HEAP_ALLOC_STRATEGY: HeapAllocStrategy =
/// Default heap allocation pages.
pub const DEFAULT_HEAP_ALLOC_PAGES: u32 = 2048;

/// A method to be used to find the entrypoint when calling into the runtime
///
/// Contains variants on how to resolve wasm function that will be invoked.
pub enum InvokeMethod<'a> {
/// Call function exported with this name.
///
/// Located function should have (u32, u32) -> u64 signature.
Export(&'a str),
/// Call a function found in the exported table found under the given index.
///
/// Located function should have (u32, u32) -> u64 signature.
Table(u32),
/// Call function by reference from table through a wrapper.
///
/// Invoked function (`dispatcher_ref`) function
/// should have (u32, u32, u32) -> u64 signature.
///
/// `func` will be passed to the invoked function as a first argument.
TableWithWrapper {
/// Wrapper for the call.
///
/// Function pointer, index into runtime exported table.
dispatcher_ref: u32,
/// Extra argument for dispatch.
///
/// Common usage would be to use it as an actual wasm function pointer
/// that should be invoked, but can be used as any extra argument on the
/// callee side.
///
/// This is typically generated and invoked by the runtime itself.
func: u32,
},
}

impl<'a> From<&'a str> for InvokeMethod<'a> {
fn from(val: &'a str) -> InvokeMethod<'a> {
InvokeMethod::Export(val)
}
}

/// A trait that defines an abstract WASM runtime module.
///
/// This can be implemented by an execution engine.
Expand All @@ -87,7 +46,7 @@ pub trait WasmInstance: Send {
/// Before execution, instance is reset.
///
/// Returns the encoded result on success.
fn call(&mut self, method: InvokeMethod, data: &[u8]) -> Result<Vec<u8>, Error> {
fn call(&mut self, method: &str, data: &[u8]) -> Result<Vec<u8>, Error> {
self.call_with_allocation_stats(method, data).0
}

Expand All @@ -98,7 +57,7 @@ pub trait WasmInstance: Send {
/// Returns the encoded result on success.
fn call_with_allocation_stats(
&mut self,
method: InvokeMethod,
method: &str,
data: &[u8],
) -> (Result<Vec<u8>, Error>, Option<AllocationStats>);

Expand All @@ -110,11 +69,6 @@ pub trait WasmInstance: Send {
fn call_export(&mut self, method: &str, data: &[u8]) -> Result<Vec<u8>, Error> {
self.call(method.into(), data)
}

/// Get the value from a global with the given `name`.
///
/// This method is only suitable for getting immutable globals.
fn get_global_const(&mut self, name: &str) -> Result<Option<Value>, Error>;
}

/// Defines the heap pages allocation strategy the wasm runtime should use.
Expand Down
23 changes: 23 additions & 0 deletions substrate/client/executor/polkavm/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 = "https://substrate.io"
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 = { path = "../common" }
sp-wasm-interface = { path = "../../../primitives/wasm-interface" }
1 change: 1 addition & 0 deletions substrate/client/executor/polkavm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
License: GPL-3.0-or-later WITH Classpath-exception-2.0
Loading

0 comments on commit 688c25b

Please sign in to comment.