Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Refactor sp-sandbox to accomodate new wasmi API #11752

Closed
2 tasks
athei opened this issue Jun 28, 2022 · 0 comments · Fixed by #12501
Closed
2 tasks

Refactor sp-sandbox to accomodate new wasmi API #11752

athei opened this issue Jun 28, 2022 · 0 comments · Fixed by #12501
Assignees
Labels
J0-enhancement An additional feature request. Z4-involved Can be fixed by an expert coder with good knowledge of the codebase.

Comments

@athei
Copy link
Member

athei commented Jun 28, 2022

Background

Within substrate we have two places where wasmi is used:

sc-executor

This is the part of substrate which is responsible for executing the runtime. Hence sc-executor is part of the client and is never compiled to wasm as it is the part that actually executes the wasm. Another responsibility of sc-executor is to provide sandboxing functionality to the runtime via a set of host functions:

/// Wasm-only interface that provides functions for interacting with the sandbox.
#[runtime_interface(wasm_only)]
pub trait Sandbox {
/// Instantiate a new sandbox instance with the given `wasm_code`.
fn instantiate(
&mut self,
dispatch_thunk: u32,
wasm_code: &[u8],
env_def: &[u8],
state_ptr: Pointer<u8>,
) -> u32 {
self.sandbox()
.instance_new(dispatch_thunk, wasm_code, env_def, state_ptr.into())
.expect("Failed to instantiate a new sandbox")
}
/// Invoke `function` in the sandbox with `sandbox_idx`.
fn invoke(
&mut self,
instance_idx: u32,
function: &str,
args: &[u8],
return_val_ptr: Pointer<u8>,
return_val_len: u32,
state_ptr: Pointer<u8>,
) -> u32 {
self.sandbox()
.invoke(instance_idx, function, args, return_val_ptr, return_val_len, state_ptr.into())
.expect("Failed to invoke function with sandbox")
}
/// Create a new memory instance with the given `initial` and `maximum` size.
fn memory_new(&mut self, initial: u32, maximum: u32) -> u32 {
self.sandbox()
.memory_new(initial, maximum)
.expect("Failed to create new memory with sandbox")
}
/// Get the memory starting at `offset` from the instance with `memory_idx` into the buffer.
fn memory_get(
&mut self,
memory_idx: u32,
offset: u32,
buf_ptr: Pointer<u8>,
buf_len: u32,
) -> u32 {
self.sandbox()
.memory_get(memory_idx, offset, buf_ptr, buf_len)
.expect("Failed to get memory with sandbox")
}
/// Set the memory in the given `memory_idx` to the given value at `offset`.
fn memory_set(
&mut self,
memory_idx: u32,
offset: u32,
val_ptr: Pointer<u8>,
val_len: u32,
) -> u32 {
self.sandbox()
.memory_set(memory_idx, offset, val_ptr, val_len)
.expect("Failed to set memory with sandbox")
}
/// Teardown the memory instance with the given `memory_idx`.
fn memory_teardown(&mut self, memory_idx: u32) {
self.sandbox()
.memory_teardown(memory_idx)
.expect("Failed to teardown memory with sandbox")
}
/// Teardown the sandbox instance with the given `instance_idx`.
fn instance_teardown(&mut self, instance_idx: u32) {
self.sandbox()
.instance_teardown(instance_idx)
.expect("Failed to teardown sandbox instance")
}
/// Get the value from a global with the given `name`. The sandbox is determined by the given
/// `instance_idx`.
///
/// Returns `Some(_)` when the requested global variable could be found.
fn get_global_val(
&mut self,
instance_idx: u32,
name: &str,
) -> Option<sp_wasm_interface::Value> {
self.sandbox()
.get_global_val(instance_idx, name)
.expect("Failed to get global from sandbox")
}
}

Please note that with sandboxing we specifically mean exposing and implementing this interface. Executing the runtime itself is not what we mean by this. These two functionalities are simply conflated within sc-executor.

Within sc-executor wasmi is used for both use cases: It can be used to execute the runtime and it can also be used to provide sandboxing. Apart from wasmi sc-executor can also execute the runtime with wasmtime (this is the default and wasmi is way too slow for production use).

Additionally, the sandboxing can also be accomplished with wasmer singlepass (by passing the experimental wasmer-sandbox feature). Sandbox does not support wasmtime as we assume the sandboxed code to be untrusted and hence cannot be compiled with an optimizing compiler (like wasmtime). Wasmtime support for sandboxing could be useful for debugging (wasmtime is more advanced in that regard) but cannot be used in production for the discussed reason.

sp-sandbox

We discussed that sc-executor provides sandboxing support via a host interface. This means it can be used by the runtime to spawn wasm instances within the client to run some code in (read contracts). sp-sandbox is the abstraction around this host interface. pallet-contracts (part of the runtime) depends on this crate in order to run its contracts through the host interface.

In addition to providing access to the host interface, sp-sandbox also includes what we call an embedded executor. This means that instead of calling into the host it uses a executor directly compiled into this crate to provide its functionality. We use wasmi as the embedded executor. Hence sp-sandbox directly depends on wasmi and becomes compiled to wasm and straight into the runtime (yes we run a wasm interpreter as wasm). The part of sp-sandbox that uses the host interface is called the host executor.

Please note that we cannot use wasmer as embedded executor as compilers cannot be part of the runtime as they are inherently platform depended (they emit native code) and the runtime needs to be independent of the platform.

As of right now we only use the embedded executor for production as we do not want to depend on the availability of the sandboxing host functions on relay chain validators and hence ossify their API, yet.

sp-sandbox provides its functionality by exporting three modules:

  • embedded_executor
  • host_executor
  • default_executor

Those modules contain functions needed to spawn and interact with sandboxes. pallet-contracts as the only user uses default_executor which is just a re-export of embedded_executor in the production case (wasmer-sandbox is experimental and forces the use of the host executor because this is where wasmer lives):

#[cfg(not(all(feature = "wasmer-sandbox", not(feature = "std"))))]
pub use self::embedded_executor as default_executor;
pub use self::env::HostError;
#[cfg(all(feature = "wasmer-sandbox", not(feature = "std")))]
pub use self::host_executor as default_executor;

As you can see the presence of std will also force us to use the embedded executor. This is because in the std case sp-sandbox is compiled for use in the native runtime where the sandboxing host interface isn't available.

Motivation

What we want to to is to upgrade the embedded executor to use the latest wasmi version. We do this because we want to improve the performance of our contracts. We only care about the embedded executor because this is the only executor we use for now for contracts.

Upgrading the wasmi within sc-executor is not required because it is not used:

  • The runtime is run with wasmtime in all production cases.
  • The host executor is only used in the wasmer case. In production the embedded executor is used.

Running the runtime with the newest wasmer could be interesting in order to perform a burnin, though.

Implementation

Refactor executor decision logic

The re-export of the default_executor is a bad way of selecting which executor is to be used by the users of sp-sandbox: The choice of the executor is made by some upstream substrate crate when doing it like this. We want to leave this decision to the users of substrate (runtime authors). Hence we want to refactor sp-sandbox (and pallet-contracts) so that the setup looks like this:

  • sp-sandbox exports a single Sandbox trait.
  • sp-sandbox exports two types implementing Sandbox: HostExecutor, EmbeddedExecutor.
  • pallet-contracts adds a type Executor: Sandbox associated type to its trait Config.
  • The runtime decides which type to pass to Executor.
  • sp-sandbox does not implement any DefaultExecutor type. The runtime needs to make this decision.

Update EmbeddedExecutor to use the latest wasmi

The latest updates to wasmi changed its API. It is now more like the wasmtime API. The current abstractions within sp-sandbox do not work with the new API. They need a refactoring. This is why we conflate it with the other refactoring. Those traits need to be re-organized anyways.

TODO

  • Refactor executor decision logic
  • Update EmbeddedExecutor to use the latest wasmi
@athei athei added J0-enhancement An additional feature request. Z4-involved Can be fixed by an expert coder with good knowledge of the codebase. labels Jun 28, 2022
@athei athei assigned athei and unassigned Robbepop Oct 16, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
J0-enhancement An additional feature request. Z4-involved Can be fixed by an expert coder with good knowledge of the codebase.
Projects
Status: Done
Development

Successfully merging a pull request may close this issue.

2 participants