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

Allow execution with both native & wasm #309

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion polkadot/cli/src/cli.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ args:
- pruning:
long: pruning
value_name: PRUNING_MODE
help: Specify the pruning mode. (a number of blocks to keep or "archive"). Default is 256.
help: Specify the pruning mode, a number of blocks to keep or "archive". Default is 256.
takes_value: true
- name:
long: name
Expand All @@ -95,6 +95,10 @@ args:
value_name: TELEMETRY_URL
help: The URL of the telemetry server. Implies --telemetry
takes_value: true
- execution:
long: execution
value_name: STRATEGY
help: The means of execution used when calling into the runtime. Can be either wasm, native or both.
subcommands:
- build-spec:
about: Build a spec.json file, outputing to stdout
Expand Down
16 changes: 15 additions & 1 deletion polkadot/cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,18 +218,32 @@ pub fn run<I, T, W>(args: I, worker: W) -> error::Result<()> where
if matches.is_present("collator") {
info!("Starting collator");
// TODO [rob]: collation node implementation
service::Role::FULL
// This isn't a thing. Different parachains will have their own collator executables and
// maybe link to libpolkadot to get a light-client.
service::Role::LIGHT
} else if matches.is_present("light") {
info!("Starting (light)");
config.execution_strategy = service::ExecutionStrategy::NativeWhenPossible;
service::Role::LIGHT
} else if matches.is_present("validator") || matches.is_present("dev") {
info!("Starting validator");
config.execution_strategy = service::ExecutionStrategy::Both;
service::Role::AUTHORITY
} else {
info!("Starting (heavy)");
config.execution_strategy = service::ExecutionStrategy::NativeWhenPossible;
service::Role::FULL
};

if let Some(s) = matches.value_of("pruning") {
config.execution_strategy = match s {
"both" => service::ExecutionStrategy::Both,
"native" => service::ExecutionStrategy::NativeWhenPossible,
"wasm" => service::ExecutionStrategy::AlwaysWasm,
_ => return Err(error::ErrorKind::Input("Invalid execution mode specified".to_owned()).into()),
};
}

config.roles = role;
{
config.network.boot_nodes.extend(matches
Expand Down
4 changes: 4 additions & 0 deletions polkadot/service/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

use transaction_pool;
use chain_spec::ChainSpec;
pub use substrate_executor::ExecutionStrategy;
pub use network::Role;
pub use network::NetworkConfiguration;
pub use client_db::PruningMode;
Expand All @@ -44,6 +45,8 @@ pub struct Configuration {
pub telemetry: Option<String>,
/// Node name.
pub name: String,
/// Execution strategy.
pub execution_strategy: ExecutionStrategy,
}

impl Configuration {
Expand All @@ -60,6 +63,7 @@ impl Configuration {
keys: Default::default(),
telemetry: Default::default(),
pruning: PruningMode::ArchiveAll,
execution_strategy: ExecutionStrategy::Both,
};
configuration.network.boot_nodes = configuration.chain_spec.boot_nodes().to_vec();
configuration
Expand Down
4 changes: 2 additions & 2 deletions polkadot/service/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ use tokio::runtime::TaskExecutor;

pub use self::error::{ErrorKind, Error};
pub use self::components::{Components, FullComponents, LightComponents};
pub use config::{Configuration, Role, PruningMode};
pub use config::{Configuration, Role, PruningMode, ExecutionStrategy};
pub use chain_spec::ChainSpec;

/// Polkadot service.
Expand Down Expand Up @@ -109,7 +109,7 @@ pub fn new_client(config: Configuration) -> Result<Arc<Client<
path: config.database_path.into(),
pruning: config.pruning,
};
let executor = polkadot_executor::Executor::new();
let executor = polkadot_executor::Executor::with_default_strategy(config.execution_strategy);
let is_validator = (config.roles & Role::AUTHORITY) == Role::AUTHORITY;
let components = components::FullComponents { is_validator };
let (client, _) = components.build_client(db_settings, executor, &config.chain_spec)?;
Expand Down
1 change: 1 addition & 0 deletions substrate/client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ impl<B, E, Block> Client<B, E, Block> where
&mut overlay,
"execute_block",
&<Block as BlockT>::new(header.clone(), body.clone().unwrap_or_default()).encode()
// TODO: intercept Err::ConsensusFailure, report failure wrt block and then accept wasm result.
)?;

Some(storage_update)
Expand Down
6 changes: 6 additions & 0 deletions substrate/executor/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,11 @@ error_chain! {
description("invalid memory reference"),
display("Invalid memory reference"),
}

/// Consensus failure.
ConsensusFailure(wasm_result: Box<Result<Vec<u8>>>, native_result: Box<Result<Vec<u8>>>) {
description("consensus failure"),
display("Differing results from Wasm execution and native dispatch"),
}
}
}
2 changes: 1 addition & 1 deletion substrate/executor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ mod sandbox;

pub mod error;
pub use wasm_executor::WasmExecutor;
pub use native_executor::{with_native_environment, NativeExecutor, NativeExecutionDispatch};
pub use native_executor::{with_native_environment, NativeExecutor, NativeExecutionDispatch, ExecutionStrategy};
pub use state_machine::Externalities;
pub use runtime_version::RuntimeVersion;
pub use codec::Slicable;
Expand Down
87 changes: 66 additions & 21 deletions substrate/executor/src/native_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ use parking_lot::{Mutex, MutexGuard};
use RuntimeInfo;

// For the internal Runtime Cache:
// Do we run this natively or use the given WasmModule
enum RunWith {
// Is it compatible enough to run this natively or do we need to fall back on the WasmModule
enum Compatibility {
InvalidVersion(WasmModule),
NativeRuntime(RuntimeVersion),
WasmRuntime(RuntimeVersion, WasmModule)
IsCompatible(RuntimeVersion),
NotCompatible(RuntimeVersion, WasmModule)
}

type CacheType = HashMap<u64, RunWith>;
type CacheType = HashMap<u64, Compatibility>;

lazy_static! {
static ref RUNTIMES_CACHE: Mutex<CacheType> = Mutex::new(HashMap::new());
Expand All @@ -51,29 +51,28 @@ fn gen_cache_key(code: &[u8]) -> u64 {
}

/// fetch a runtime version from the cache or if there is no cached version yet, create
/// the runtime version entry for `code`, determines whether `RunWith::NativeRuntime`
/// the runtime version entry for `code`, determines whether `Compatibility::IsCompatible`
/// can be used by by comparing returned RuntimeVersion to `ref_version`
fn fetch_cached_runtime_version<'a, E: Externalities>(
cache: &'a mut MutexGuard<CacheType>,
ext: &mut E,
code: &[u8],
ref_version: RuntimeVersion
) -> &'a RunWith {
) -> &'a Compatibility {
cache.entry(gen_cache_key(code))
.or_insert_with(|| {
let module = WasmModule::from_buffer(code).expect("all modules compiled with rustc are valid wasm code; qed");
let version = WasmExecutor.call_in_wasm_module(ext, &module, "version", &[]).ok()
.and_then(|v| RuntimeVersion::decode(&mut v.as_slice()));


if let Some(v) = version {
if ref_version.can_call_with(&v) {
RunWith::NativeRuntime(v)
Compatibility::IsCompatible(v)
} else {
RunWith::WasmRuntime(v, module)
Compatibility::NotCompatible(v, module)
}
} else {
RunWith::InvalidVersion(module)
Compatibility::InvalidVersion(module)
}
})
}
Expand Down Expand Up @@ -106,31 +105,78 @@ pub trait NativeExecutionDispatch {
const VERSION: RuntimeVersion;
}

/// Strategy for executing a call into the runtime.
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum ExecutionStrategy {
/// Execute with the native equivalent if it is compatible with the given wasm module; otherwise fall back to the wasm.
NativeWhenPossible,
/// Use the given wasm module.
AlwaysWasm,
/// Run with both the wasm and the native variant (if compatible). Report any discrepency as an error.
Both,
}

/// A generic `CodeExecutor` implementation that uses a delegate to determine wasm code equivalence
/// and dispatch to native code when possible, falling back on `WasmExecutor` when not.
#[derive(Debug)]
pub struct NativeExecutor<D: NativeExecutionDispatch + Sync + Send> {
/// The strategy for execution.
strategy: ExecutionStrategy,
/// Dummy field to avoid the compiler complaining about us not using `D`.
_dummy: ::std::marker::PhantomData<D>,
}

impl<D: NativeExecutionDispatch + Sync + Send> NativeExecutor<D> {
/// Create new instance.
pub fn new() -> Self {
pub fn new(strategy: ExecutionStrategy) -> Self {
// FIXME: set this entry at compile time
RUNTIMES_CACHE.lock().insert(
gen_cache_key(D::native_equivalent()),
RunWith::NativeRuntime(D::VERSION));
Compatibility::IsCompatible(D::VERSION));

NativeExecutor {
strategy,
_dummy: Default::default(),
}
}

fn call_with_strategy<E: Externalities>(
&self,
ext: &mut E,
code: &[u8],
method: &str,
data: &[u8],
strategy: ExecutionStrategy,
) -> Result<Vec<u8>> {
let mut c = RUNTIMES_CACHE.lock();
match (strategy, fetch_cached_runtime_version(&mut c, ext, code, D::VERSION)) {
(_, Compatibility::NotCompatible(_, m)) => WasmExecutor.call_in_wasm_module(ext, m, method, data),
(_, Compatibility::InvalidVersion(m)) => WasmExecutor.call_in_wasm_module(ext, m, method, data),
(ExecutionStrategy::AlwaysWasm, _) => WasmExecutor.call(ext, code, method, data),
(ExecutionStrategy::NativeWhenPossible, _) => D::dispatch(ext, method, data),
_ => {
// both
let w = WasmExecutor.call(ext, code, method, data);
let n = D::dispatch(ext, method, data);
let same_ok = if let (&Ok(ref w), &Ok(ref n)) = (&w, &n) {
w == n
} else { false };
if same_ok {
return w
}
if w.is_err() && n.is_err() && format!("{:?}", w) == format!("{:?}", n) {
return w
}
Err(ErrorKind::ConsensusFailure(Box::new(w), Box::new(n)).into())
}
}
}
}

impl<D: NativeExecutionDispatch + Sync + Send> Clone for NativeExecutor<D> {
fn clone(&self) -> Self {
NativeExecutor {
strategy: self.strategy,
_dummy: Default::default(),
}
}
Expand All @@ -146,8 +192,8 @@ impl<D: NativeExecutionDispatch + Sync + Send> RuntimeInfo for NativeExecutor<D>
) -> Option<RuntimeVersion> {
let mut c = RUNTIMES_CACHE.lock();
match fetch_cached_runtime_version(&mut c, ext, code, D::VERSION) {
RunWith::NativeRuntime(v) | RunWith::WasmRuntime(v, _) => Some(v.clone()),
RunWith::InvalidVersion(_m) => None
Compatibility::IsCompatible(v) | Compatibility::NotCompatible(v, _) => Some(v.clone()),
Compatibility::InvalidVersion(_m) => None
}
}
}
Expand All @@ -162,11 +208,7 @@ impl<D: NativeExecutionDispatch + Sync + Send> CodeExecutor for NativeExecutor<D
method: &str,
data: &[u8],
) -> Result<Vec<u8>> {
let mut c = RUNTIMES_CACHE.lock();
match fetch_cached_runtime_version(&mut c, ext, code, D::VERSION) {
RunWith::NativeRuntime(_v) => D::dispatch(ext, method, data),
RunWith::WasmRuntime(_, m) | RunWith::InvalidVersion(m) => WasmExecutor.call_in_wasm_module(ext, m, method, data)
}
self.call_with_strategy(ext, code, method, data, self.strategy)
}
}

Expand Down Expand Up @@ -198,7 +240,10 @@ macro_rules! native_executor_instance {

impl $name {
pub fn new() -> $crate::NativeExecutor<$name> {
$crate::NativeExecutor::new()
$crate::NativeExecutor::new($crate::ExecutionStrategy::NativeWhenPossible)
}
pub fn with_default_strategy(strategy: $crate::ExecutionStrategy) -> $crate::NativeExecutor<$name> {
$crate::NativeExecutor::new(strategy)
}
}
}
Expand Down