Skip to content

Commit 688c25b

Browse files
authored
Add a PolkaVM-based executor (paritytech#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 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.
1 parent fdcade6 commit 688c25b

File tree

17 files changed

+494
-281
lines changed

17 files changed

+494
-281
lines changed

substrate/client/executor/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ tracing = "0.1.29"
2323

2424
codec = { package = "parity-scale-codec", version = "3.6.1" }
2525
sc-executor-common = { path = "common" }
26+
sc-executor-polkavm = { path = "polkavm" }
2627
sc-executor-wasmtime = { path = "wasmtime" }
2728
sp-api = { path = "../../primitives/api" }
2829
sp-core = { path = "../../primitives/core" }

substrate/client/executor/common/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ wasm-instrument = "0.4"
2222
sc-allocator = { path = "../../allocator" }
2323
sp-maybe-compressed-blob = { path = "../../../primitives/maybe-compressed-blob" }
2424
sp-wasm-interface = { path = "../../../primitives/wasm-interface" }
25+
polkavm = { workspace = true }
2526

2627
[features]
2728
default = []

substrate/client/executor/common/src/error.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,24 @@ pub enum WasmError {
150150
Other(String),
151151
}
152152

153+
impl From<polkavm::ProgramParseError> for WasmError {
154+
fn from(error: polkavm::ProgramParseError) -> Self {
155+
WasmError::Other(error.to_string())
156+
}
157+
}
158+
159+
impl From<polkavm::Error> for WasmError {
160+
fn from(error: polkavm::Error) -> Self {
161+
WasmError::Other(error.to_string())
162+
}
163+
}
164+
165+
impl From<polkavm::Error> for Error {
166+
fn from(error: polkavm::Error) -> Self {
167+
Error::Other(error.to_string())
168+
}
169+
}
170+
153171
/// An error message with an attached backtrace.
154172
#[derive(Debug)]
155173
pub struct MessageWithBacktrace {

substrate/client/executor/common/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,7 @@ pub mod error;
2525
pub mod runtime_blob;
2626
pub mod util;
2727
pub mod wasm_runtime;
28+
29+
pub(crate) fn is_polkavm_enabled() -> bool {
30+
std::env::var_os("SUBSTRATE_ENABLE_POLKAVM").map_or(false, |value| value == "1")
31+
}

substrate/client/executor/common/src/runtime_blob/runtime_blob.rs

Lines changed: 90 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,23 @@
1717
// along with this program. If not, see <https://www.gnu.org/licenses/>.
1818

1919
use crate::{error::WasmError, wasm_runtime::HeapAllocStrategy};
20-
use wasm_instrument::{
21-
export_mutable_globals,
22-
parity_wasm::elements::{
23-
deserialize_buffer, serialize, ExportEntry, External, Internal, MemorySection, MemoryType,
24-
Module, Section,
25-
},
20+
use wasm_instrument::parity_wasm::elements::{
21+
deserialize_buffer, serialize, ExportEntry, External, Internal, MemorySection, MemoryType,
22+
Module, Section,
2623
};
2724

28-
/// A bunch of information collected from a WebAssembly module.
25+
/// A program blob containing a Substrate runtime.
2926
#[derive(Clone)]
30-
pub struct RuntimeBlob {
31-
raw_module: Module,
27+
pub struct RuntimeBlob(BlobKind);
28+
29+
#[derive(Clone)]
30+
enum BlobKind {
31+
WebAssembly(Module),
32+
PolkaVM(polkavm::ProgramBlob<'static>),
3233
}
3334

3435
impl RuntimeBlob {
35-
/// Create `RuntimeBlob` from the given wasm code. Will attempt to decompress the code before
36-
/// deserializing it.
36+
/// Create `RuntimeBlob` from the given WASM or PolkaVM compressed program blob.
3737
///
3838
/// See [`sp_maybe_compressed_blob`] for details about decompression.
3939
pub fn uncompress_if_needed(wasm_code: &[u8]) -> Result<Self, WasmError> {
@@ -43,31 +43,26 @@ impl RuntimeBlob {
4343
Self::new(&wasm_code)
4444
}
4545

46-
/// Create `RuntimeBlob` from the given wasm code.
46+
/// Create `RuntimeBlob` from the given WASM or PolkaVM program blob.
4747
///
48-
/// Returns `Err` if the wasm code cannot be deserialized.
49-
pub fn new(wasm_code: &[u8]) -> Result<Self, WasmError> {
50-
let raw_module: Module = deserialize_buffer(wasm_code)
51-
.map_err(|e| WasmError::Other(format!("cannot deserialize module: {:?}", e)))?;
52-
Ok(Self { raw_module })
53-
}
54-
55-
/// The number of globals defined in locally in this module.
56-
pub fn declared_globals_count(&self) -> u32 {
57-
self.raw_module
58-
.global_section()
59-
.map(|gs| gs.entries().len() as u32)
60-
.unwrap_or(0)
61-
}
62-
63-
/// The number of imports of globals.
64-
pub fn imported_globals_count(&self) -> u32 {
65-
self.raw_module.import_section().map(|is| is.globals() as u32).unwrap_or(0)
66-
}
48+
/// Returns `Err` if the blob cannot be deserialized.
49+
///
50+
/// Will only accept a PolkaVM program if the `SUBSTRATE_ENABLE_POLKAVM` environment
51+
/// variable is set to `1`.
52+
pub fn new(raw_blob: &[u8]) -> Result<Self, WasmError> {
53+
if raw_blob.starts_with(b"PVM\0") {
54+
if crate::is_polkavm_enabled() {
55+
return Ok(Self(BlobKind::PolkaVM(
56+
polkavm::ProgramBlob::parse(raw_blob)?.into_owned(),
57+
)));
58+
} else {
59+
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()));
60+
}
61+
}
6762

68-
/// Perform an instrumentation that makes sure that the mutable globals are exported.
69-
pub fn expose_mutable_globals(&mut self) {
70-
export_mutable_globals(&mut self.raw_module, "exported_internal_global");
63+
let raw_module: Module = deserialize_buffer(raw_blob)
64+
.map_err(|e| WasmError::Other(format!("cannot deserialize module: {:?}", e)))?;
65+
Ok(Self(BlobKind::WebAssembly(raw_module)))
7166
}
7267

7368
/// Run a pass that instrument this module so as to introduce a deterministic stack height
@@ -80,35 +75,28 @@ impl RuntimeBlob {
8075
///
8176
/// The stack cost of a function is computed based on how much locals there are and the maximum
8277
/// depth of the wasm operand stack.
78+
///
79+
/// Only valid for WASM programs; will return an error if the blob is a PolkaVM program.
8380
pub fn inject_stack_depth_metering(self, stack_depth_limit: u32) -> Result<Self, WasmError> {
8481
let injected_module =
85-
wasm_instrument::inject_stack_limiter(self.raw_module, stack_depth_limit).map_err(
86-
|e| WasmError::Other(format!("cannot inject the stack limiter: {:?}", e)),
87-
)?;
88-
89-
Ok(Self { raw_module: injected_module })
90-
}
82+
wasm_instrument::inject_stack_limiter(self.into_webassembly_blob()?, stack_depth_limit)
83+
.map_err(|e| {
84+
WasmError::Other(format!("cannot inject the stack limiter: {:?}", e))
85+
})?;
9186

92-
/// Perform an instrumentation that makes sure that a specific function `entry_point` is
93-
/// exported
94-
pub fn entry_point_exists(&self, entry_point: &str) -> bool {
95-
self.raw_module
96-
.export_section()
97-
.map(|e| {
98-
e.entries().iter().any(|e| {
99-
matches!(e.internal(), Internal::Function(_)) && e.field() == entry_point
100-
})
101-
})
102-
.unwrap_or_default()
87+
Ok(Self(BlobKind::WebAssembly(injected_module)))
10388
}
10489

10590
/// Converts a WASM memory import into a memory section and exports it.
10691
///
10792
/// Does nothing if there's no memory import.
10893
///
10994
/// May return an error in case the WASM module is invalid.
95+
///
96+
/// Only valid for WASM programs; will return an error if the blob is a PolkaVM program.
11097
pub fn convert_memory_import_into_export(&mut self) -> Result<(), WasmError> {
111-
let import_section = match self.raw_module.import_section_mut() {
98+
let raw_module = self.as_webassembly_blob_mut()?;
99+
let import_section = match raw_module.import_section_mut() {
112100
Some(import_section) => import_section,
113101
None => return Ok(()),
114102
};
@@ -124,7 +112,7 @@ impl RuntimeBlob {
124112
let memory_name = entry.field().to_owned();
125113
import_entries.remove(index);
126114

127-
self.raw_module
115+
raw_module
128116
.insert_section(Section::Memory(MemorySection::with_entries(vec![memory_ty])))
129117
.map_err(|error| {
130118
WasmError::Other(format!(
@@ -133,14 +121,14 @@ impl RuntimeBlob {
133121
))
134122
})?;
135123

136-
if self.raw_module.export_section_mut().is_none() {
124+
if raw_module.export_section_mut().is_none() {
137125
// A module without an export section is somewhat unrealistic, but let's do this
138126
// just in case to cover all of our bases.
139-
self.raw_module
127+
raw_module
140128
.insert_section(Section::Export(Default::default()))
141129
.expect("an export section can be always inserted if it doesn't exist; qed");
142130
}
143-
self.raw_module
131+
raw_module
144132
.export_section_mut()
145133
.expect("export section already existed or we just added it above, so it always exists; qed")
146134
.entries_mut()
@@ -156,12 +144,14 @@ impl RuntimeBlob {
156144
///
157145
/// Will return an error in case there is no memory section present,
158146
/// or if the memory section is empty.
147+
///
148+
/// Only valid for WASM programs; will return an error if the blob is a PolkaVM program.
159149
pub fn setup_memory_according_to_heap_alloc_strategy(
160150
&mut self,
161151
heap_alloc_strategy: HeapAllocStrategy,
162152
) -> Result<(), WasmError> {
163-
let memory_section = self
164-
.raw_module
153+
let raw_module = self.as_webassembly_blob_mut()?;
154+
let memory_section = raw_module
165155
.memory_section_mut()
166156
.ok_or_else(|| WasmError::Other("no memory section found".into()))?;
167157

@@ -187,20 +177,57 @@ impl RuntimeBlob {
187177

188178
/// Scans the wasm blob for the first section with the name that matches the given. Returns the
189179
/// contents of the custom section if found or `None` otherwise.
180+
///
181+
/// Only valid for WASM programs; will return an error if the blob is a PolkaVM program.
190182
pub fn custom_section_contents(&self, section_name: &str) -> Option<&[u8]> {
191-
self.raw_module
183+
self.as_webassembly_blob()
184+
.ok()?
192185
.custom_sections()
193186
.find(|cs| cs.name() == section_name)
194187
.map(|cs| cs.payload())
195188
}
196189

197190
/// Consumes this runtime blob and serializes it.
198191
pub fn serialize(self) -> Vec<u8> {
199-
serialize(self.raw_module).expect("serializing into a vec should succeed; qed")
192+
match self.0 {
193+
BlobKind::WebAssembly(raw_module) =>
194+
serialize(raw_module).expect("serializing into a vec should succeed; qed"),
195+
BlobKind::PolkaVM(ref blob) => blob.as_bytes().to_vec(),
196+
}
197+
}
198+
199+
fn as_webassembly_blob(&self) -> Result<&Module, WasmError> {
200+
match self.0 {
201+
BlobKind::WebAssembly(ref raw_module) => Ok(raw_module),
202+
BlobKind::PolkaVM(..) => Err(WasmError::Other(
203+
"expected a WebAssembly program; found a PolkaVM program blob".into(),
204+
)),
205+
}
200206
}
201207

202-
/// Destructure this structure into the underlying parity-wasm Module.
203-
pub fn into_inner(self) -> Module {
204-
self.raw_module
208+
fn as_webassembly_blob_mut(&mut self) -> Result<&mut Module, WasmError> {
209+
match self.0 {
210+
BlobKind::WebAssembly(ref mut raw_module) => Ok(raw_module),
211+
BlobKind::PolkaVM(..) => Err(WasmError::Other(
212+
"expected a WebAssembly program; found a PolkaVM program blob".into(),
213+
)),
214+
}
215+
}
216+
217+
fn into_webassembly_blob(self) -> Result<Module, WasmError> {
218+
match self.0 {
219+
BlobKind::WebAssembly(raw_module) => Ok(raw_module),
220+
BlobKind::PolkaVM(..) => Err(WasmError::Other(
221+
"expected a WebAssembly program; found a PolkaVM program blob".into(),
222+
)),
223+
}
224+
}
225+
226+
/// Gets a reference to the inner PolkaVM program blob, if this is a PolkaVM program.
227+
pub fn as_polkavm_blob(&self) -> Option<&polkavm::ProgramBlob> {
228+
match self.0 {
229+
BlobKind::WebAssembly(..) => None,
230+
BlobKind::PolkaVM(ref blob) => Some(blob),
231+
}
205232
}
206233
}

substrate/client/executor/common/src/wasm_runtime.rs

Lines changed: 2 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
//! Definitions for a wasm runtime.
2020
2121
use crate::error::Error;
22-
use sp_wasm_interface::Value;
2322

2423
pub use sc_allocator::AllocationStats;
2524

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

33-
/// A method to be used to find the entrypoint when calling into the runtime
34-
///
35-
/// Contains variants on how to resolve wasm function that will be invoked.
36-
pub enum InvokeMethod<'a> {
37-
/// Call function exported with this name.
38-
///
39-
/// Located function should have (u32, u32) -> u64 signature.
40-
Export(&'a str),
41-
/// Call a function found in the exported table found under the given index.
42-
///
43-
/// Located function should have (u32, u32) -> u64 signature.
44-
Table(u32),
45-
/// Call function by reference from table through a wrapper.
46-
///
47-
/// Invoked function (`dispatcher_ref`) function
48-
/// should have (u32, u32, u32) -> u64 signature.
49-
///
50-
/// `func` will be passed to the invoked function as a first argument.
51-
TableWithWrapper {
52-
/// Wrapper for the call.
53-
///
54-
/// Function pointer, index into runtime exported table.
55-
dispatcher_ref: u32,
56-
/// Extra argument for dispatch.
57-
///
58-
/// Common usage would be to use it as an actual wasm function pointer
59-
/// that should be invoked, but can be used as any extra argument on the
60-
/// callee side.
61-
///
62-
/// This is typically generated and invoked by the runtime itself.
63-
func: u32,
64-
},
65-
}
66-
67-
impl<'a> From<&'a str> for InvokeMethod<'a> {
68-
fn from(val: &'a str) -> InvokeMethod<'a> {
69-
InvokeMethod::Export(val)
70-
}
71-
}
72-
7332
/// A trait that defines an abstract WASM runtime module.
7433
///
7534
/// This can be implemented by an execution engine.
@@ -87,7 +46,7 @@ pub trait WasmInstance: Send {
8746
/// Before execution, instance is reset.
8847
///
8948
/// Returns the encoded result on success.
90-
fn call(&mut self, method: InvokeMethod, data: &[u8]) -> Result<Vec<u8>, Error> {
49+
fn call(&mut self, method: &str, data: &[u8]) -> Result<Vec<u8>, Error> {
9150
self.call_with_allocation_stats(method, data).0
9251
}
9352

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

@@ -110,11 +69,6 @@ pub trait WasmInstance: Send {
11069
fn call_export(&mut self, method: &str, data: &[u8]) -> Result<Vec<u8>, Error> {
11170
self.call(method.into(), data)
11271
}
113-
114-
/// Get the value from a global with the given `name`.
115-
///
116-
/// This method is only suitable for getting immutable globals.
117-
fn get_global_const(&mut self, name: &str) -> Result<Option<Value>, Error>;
11872
}
11973

12074
/// Defines the heap pages allocation strategy the wasm runtime should use.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
name = "sc-executor-polkavm"
3+
version = "0.29.0"
4+
authors.workspace = true
5+
edition.workspace = true
6+
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
7+
homepage = "https://substrate.io"
8+
repository.workspace = true
9+
description = "PolkaVM executor for Substrate"
10+
readme = "README.md"
11+
12+
[lints]
13+
workspace = true
14+
15+
[package.metadata.docs.rs]
16+
targets = ["x86_64-unknown-linux-gnu"]
17+
18+
[dependencies]
19+
log = { workspace = true }
20+
polkavm = { workspace = true }
21+
22+
sc-executor-common = { path = "../common" }
23+
sp-wasm-interface = { path = "../../../primitives/wasm-interface" }
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
License: GPL-3.0-or-later WITH Classpath-exception-2.0

0 commit comments

Comments
 (0)