New PVF validation host (#2710)

* Implement PVF validation host

* WIP: Diener

* Increase the alloted compilation time

* Add more comments

* Minor clean up

* Apply suggestions from code review

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

* Fix pruning artifact removal

* Fix formatting and newlines

* Fix the thread pool

* Update node/core/pvf/src/executor_intf.rs

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

* Remove redundant test declaration

* Don't convert the path into an intermediate string

* Try to workaround the test failure

* Use the puppet_worker trick again

* Fix a blip

* Move `ensure_wasmtime_version` under the tests mod

* Add a macro for puppet_workers

* fix build for not real-overseer

* Rename the puppet worker for adder collator

* play it safe with the name of adder puppet worker

* Typo: triggered

* Add more comments

* Do not kill exec worker on every error

* Plumb Duration for timeouts

* typo: critical

* Add proofs

* Clean unused imports

* Revert "WIP: Diener"

This reverts commit b9f54e513366c7a6dfdd117ac19fbdc46b900b4d.

* Sync version of wasmtime

* Update cargo.lock

* Update Substrate

* Merge fixes still

* Update wasmtime version in test

* bastifmt

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

* Squash spaces

* Trailing new line for testing.rs

* Remove controversial code

* comment about biasing

* Fix suggestion

* Add comments

* make it more clear why unwrap_err

* tmpfile retry

* proper proofs for claim_idle

* Remove mutex from ValidationHost

* Add some more logging

* Extract exec timeout into a constant

* Add some clarifying logging

* Use blake2_256

* Clean up the merge

Specifically the leftovers after removing real-overseer

* Update parachain/test-parachains/adder/collator/Cargo.toml

Co-authored-by: Andronik Ordian <write@reusable.software>

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>
Co-authored-by: Andronik Ordian <write@reusable.software>
This commit is contained in:
Sergei Shulepov
2021-04-09 01:09:56 +03:00
committed by GitHub
parent 896ec8dbc3
commit 59b4d6511f
43 changed files with 5108 additions and 1991 deletions
-26
View File
@@ -14,47 +14,21 @@ parity-util-mem = { version = "0.9.0", optional = true }
sp-std = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
sp-core = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
sp-wasm-interface = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false }
polkadot-core-primitives = { path = "../core-primitives", default-features = false }
derive_more = "0.99.11"
# all optional crates.
thiserror = { version = "1.0.22", optional = true }
serde = { version = "1.0.117", default-features = false, features = [ "derive" ], optional = true }
sp-externalities = { git = "https://github.com/paritytech/substrate", branch = "master", optional = true }
sc-executor = { git = "https://github.com/paritytech/substrate", branch = "master", optional = true }
sp-io = { git = "https://github.com/paritytech/substrate", branch = "master", optional = true }
parking_lot = { version = "0.11.1", optional = true }
log = { version = "0.4.11", optional = true }
futures = { version = "0.3.8", optional = true }
static_assertions = { version = "1.1", optional = true }
libc = { version = "0.2.81", optional = true }
[target.'cfg(not(any(target_os = "android", target_os = "unknown")))'.dependencies]
shared_memory = { version = "0.11.0", optional = true }
raw_sync = { version = "0.1", optional = true }
[features]
default = ["std"]
wasmtime = [ "sc-executor/wasmtime" ]
wasm-api = []
std = [
"parity-scale-codec/std",
"thiserror",
"serde/std",
"sp-std/std",
"sp-runtime/std",
"shared_memory",
"raw_sync",
"sp-core/std",
"parking_lot",
"static_assertions",
"log",
"libc",
"parity-util-mem",
"sp-externalities",
"sc-executor",
"sp-io",
"polkadot-core-primitives/std",
"futures",
]
+2 -2
View File
@@ -14,6 +14,8 @@
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
#![warn(unused_crate_dependencies)]
//! Defines primitive types for creating or validating a parachain.
//!
//! When compiled with standard library support, this crate exports a `wasm`
@@ -43,8 +45,6 @@
#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(feature = "std")]
pub mod wasm_executor;
pub mod primitives;
mod wasm_api;
-401
View File
@@ -1,401 +0,0 @@
// Copyright 2017-2020 Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
//! WASM re-execution of a parachain candidate.
//! In the context of relay-chain candidate evaluation, there are some additional
//! steps to ensure that the provided input parameters are correct.
//! Assuming the parameters are correct, this module provides a wrapper around
//! a WASM VM for re-execution of a parachain candidate.
use std::{any::{TypeId, Any}, path::{Path, PathBuf}};
use crate::primitives::{ValidationParams, ValidationResult};
use parity_scale_codec::{Decode, Encode};
use sp_core::{storage::{ChildInfo, TrackedStorageKey}, traits::{CallInWasm, SpawnNamed}};
use sp_externalities::Extensions;
use sp_wasm_interface::HostFunctions as _;
#[cfg(not(any(target_os = "android", target_os = "unknown")))]
pub use validation_host::{run_worker, ValidationPool, EXECUTION_TIMEOUT_SEC, WORKER_ARGS};
mod validation_host;
/// The strategy we employ for isolating execution of wasm parachain validation function (PVF).
///
/// For a typical validator an external process is the default way to run PVF. The rationale is based
/// on the following observations:
///
/// (a) PVF is completely under control of parachain developers who may or may not be malicious.
/// (b) Collators are in charge of providing PoV who also may or may not be malicious.
/// (c) PVF is executed by a wasm engine based on optimizing compiler which is a very complex piece
/// of machinery.
///
/// (a) and (b) may lead to a situation where due to a combination of PVF and PoV the validation work
/// can stuck in an infinite loop, which can open up resource exhaustion or DoS attack vectors.
///
/// While some execution engines provide functionality to interrupt execution of wasm module from
/// another thread, there are also some caveats to that: there is no clean way to interrupt execution
/// if the control flow is in the host side and at the moment we haven't rigoriously vetted that all
/// host functions terminate or, at least, return in a short amount of time. Additionally, we want
/// some freedom on choosing wasm execution environment.
///
/// On top of that, execution in a separate process helps to minimize impact of (c) if exploited.
/// It's not only the risk of miscompilation, but it also includes risk of JIT-bombs, i.e. cases
/// of specially crafted code that take enourmous amounts of time and memory to compile.
///
/// At the same time, since PVF validates self-contained candidates, validation workers don't require
/// extensive communication with polkadot host, therefore there should be no observable performance penalty
/// coming from inter process communication.
///
/// All of the above should give a sense why isolation is crucial for a typical use-case.
///
/// However, in some cases, e.g. when running PVF validation on android (for whatever reason), we
/// cannot afford the luxury of process isolation and thus there is an option to run validation in
/// process. Also, running in process is convenient for testing.
#[derive(Clone, Debug)]
pub enum IsolationStrategy {
/// The validation worker is ran in a thread inside the same process.
InProcess,
/// The validation worker is ran using the process' executable and the subcommand `validation-worker` is passed
/// following by the address of the shared memory.
#[cfg(not(any(target_os = "android", target_os = "unknown")))]
ExternalProcessSelfHost {
pool: ValidationPool,
cache_base_path: Option<String>,
},
/// The validation worker is ran using the command provided and the argument provided. The address of the shared
/// memory is added at the end of the arguments.
#[cfg(not(any(target_os = "android", target_os = "unknown")))]
ExternalProcessCustomHost {
/// Validation pool.
pool: ValidationPool,
/// Path to the validation worker. The file must exists and be executable.
binary: PathBuf,
/// List of arguments passed to the validation worker. The address of the shared memory will be automatically
/// added after the arguments.
args: Vec<String>,
},
}
impl IsolationStrategy {
#[cfg(not(any(target_os = "android", target_os = "unknown")))]
pub fn external_process_with_caching(cache_base_path: Option<&Path>) -> Self {
// Convert cache path to string here so that we don't have to do that each time we launch
// validation worker.
let cache_base_path = cache_base_path.map(|path| path.display().to_string());
Self::ExternalProcessSelfHost {
pool: ValidationPool::new(),
cache_base_path,
}
}
}
#[derive(Debug, thiserror::Error)]
/// Candidate validation error.
pub enum ValidationError {
/// Validation failed due to internal reasons. The candidate might still be valid.
#[error(transparent)]
Internal(#[from] InternalError),
/// Candidate is invalid.
#[error(transparent)]
InvalidCandidate(#[from] InvalidCandidate),
}
/// Error type that indicates invalid candidate.
#[derive(Debug, thiserror::Error)]
pub enum InvalidCandidate {
/// Wasm executor error.
#[error("WASM executor error")]
WasmExecutor(#[from] sc_executor::error::Error),
/// Call data is too large.
#[error("Validation parameters are {0} bytes, max allowed is {1}")]
ParamsTooLarge(usize, usize),
/// Code size it too large.
#[error("WASM code is {0} bytes, max allowed is {1}")]
CodeTooLarge(usize, usize),
/// Error decoding returned data.
#[error("Validation function returned invalid data.")]
BadReturn,
#[error("Validation function timeout.")]
Timeout,
#[error("External WASM execution error: {0}")]
ExternalWasmExecutor(String),
}
impl core::convert::From<String> for InvalidCandidate {
fn from(s: String) -> Self {
Self::ExternalWasmExecutor(s)
}
}
/// Host error during candidate validation. This does not indicate an invalid candidate.
#[derive(Debug, thiserror::Error)]
pub enum InternalError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("System error: {0}")]
System(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
#[cfg(not(any(target_os = "android", target_os = "unknown")))]
#[error("Failed to create shared memory: {0}")]
WorkerStartTimeout(String),
#[cfg(not(any(target_os = "android", target_os = "unknown")))]
#[error("Failed to create shared memory: {0}")]
FailedToCreateSharedMemory(String),
#[cfg(not(any(target_os = "android", target_os = "unknown")))]
#[error("Failed to send a singal to worker: {0}")]
FailedToSignal(String),
#[cfg(not(any(target_os = "android", target_os = "unknown")))]
#[error("Failed to send data to worker: {0}")]
FailedToWriteData(&'static str),
#[error("WASM worker error: {0}")]
WasmWorker(String),
}
/// A cache of executors for different parachain Wasm instances.
///
/// This should be reused across candidate validation instances.
pub struct ExecutorCache(sc_executor::WasmExecutor);
impl ExecutorCache {
/// Returns a new instance of an executor cache.
///
/// `cache_base_path` allows to specify a directory where the executor is allowed to store files
/// for caching, e.g. compilation artifacts.
pub fn new(cache_base_path: Option<PathBuf>) -> ExecutorCache {
ExecutorCache(sc_executor::WasmExecutor::new(
#[cfg(all(feature = "wasmtime", not(any(target_os = "android", target_os = "unknown"))))]
sc_executor::WasmExecutionMethod::Compiled,
#[cfg(any(not(feature = "wasmtime"), target_os = "android", target_os = "unknown"))]
sc_executor::WasmExecutionMethod::Interpreted,
// TODO: Make sure we don't use more than 1GB: https://github.com/paritytech/polkadot/issues/699
Some(1024),
HostFunctions::host_functions(),
8,
cache_base_path,
))
}
}
/// Validate a candidate under the given validation code.
///
/// This will fail if the validation code is not a proper parachain validation module.
pub fn validate_candidate(
validation_code: &[u8],
params: ValidationParams,
isolation_strategy: &IsolationStrategy,
spawner: impl SpawnNamed + 'static,
) -> Result<ValidationResult, ValidationError> {
match isolation_strategy {
IsolationStrategy::InProcess => {
validate_candidate_internal(
&ExecutorCache::new(None),
validation_code,
&params.encode(),
spawner,
)
},
#[cfg(not(any(target_os = "android", target_os = "unknown")))]
IsolationStrategy::ExternalProcessSelfHost { pool, cache_base_path } => {
pool.validate_candidate(validation_code, params, cache_base_path.as_deref())
},
#[cfg(not(any(target_os = "android", target_os = "unknown")))]
IsolationStrategy::ExternalProcessCustomHost { pool, binary, args } => {
let args: Vec<&str> = args.iter().map(|x| x.as_str()).collect();
pool.validate_candidate_custom(validation_code, params, binary, &args)
},
}
}
/// The host functions provided by the wasm executor to the parachain wasm blob.
type HostFunctions = sp_io::SubstrateHostFunctions;
/// Validate a candidate under the given validation code.
///
/// This will fail if the validation code is not a proper parachain validation module.
pub fn validate_candidate_internal(
executor: &ExecutorCache,
validation_code: &[u8],
encoded_call_data: &[u8],
spawner: impl SpawnNamed + 'static,
) -> Result<ValidationResult, ValidationError> {
let executor = &executor.0;
let mut extensions = Extensions::new();
extensions.register(sp_core::traits::TaskExecutorExt::new(spawner));
extensions.register(sp_core::traits::CallInWasmExt::new(executor.clone()));
let mut ext = ValidationExternalities(extensions);
// Expensive, but not more-so than recompiling the wasm module.
// And we need this hash to access the `sc_executor` cache.
let code_hash = {
use polkadot_core_primitives::{BlakeTwo256, HashT};
BlakeTwo256::hash(validation_code)
};
let res = executor.call_in_wasm(
validation_code,
Some(code_hash.as_bytes().to_vec()),
"validate_block",
encoded_call_data,
&mut ext,
sp_core::traits::MissingHostFunctions::Allow,
).map_err(|e| ValidationError::InvalidCandidate(e.into()))?;
ValidationResult::decode(&mut &res[..])
.map_err(|_| ValidationError::InvalidCandidate(InvalidCandidate::BadReturn).into())
}
/// The validation externalities that will panic on any storage related access. They just provide
/// access to the parachain extension.
struct ValidationExternalities(Extensions);
impl sp_externalities::Externalities for ValidationExternalities {
fn storage(&self, _: &[u8]) -> Option<Vec<u8>> {
panic!("storage: unsupported feature for parachain validation")
}
fn storage_hash(&self, _: &[u8]) -> Option<Vec<u8>> {
panic!("storage_hash: unsupported feature for parachain validation")
}
fn child_storage_hash(&self, _: &ChildInfo, _: &[u8]) -> Option<Vec<u8>> {
panic!("child_storage_hash: unsupported feature for parachain validation")
}
fn child_storage(&self, _: &ChildInfo, _: &[u8]) -> Option<Vec<u8>> {
panic!("child_storage: unsupported feature for parachain validation")
}
fn kill_child_storage(&mut self, _: &ChildInfo, _: Option<u32>) -> (bool, u32) {
panic!("kill_child_storage: unsupported feature for parachain validation")
}
fn clear_prefix(&mut self, _: &[u8]) {
panic!("clear_prefix: unsupported feature for parachain validation")
}
fn clear_child_prefix(&mut self, _: &ChildInfo, _: &[u8]) {
panic!("clear_child_prefix: unsupported feature for parachain validation")
}
fn place_storage(&mut self, _: Vec<u8>, _: Option<Vec<u8>>) {
panic!("place_storage: unsupported feature for parachain validation")
}
fn place_child_storage(&mut self, _: &ChildInfo, _: Vec<u8>, _: Option<Vec<u8>>) {
panic!("place_child_storage: unsupported feature for parachain validation")
}
fn storage_root(&mut self) -> Vec<u8> {
panic!("storage_root: unsupported feature for parachain validation")
}
fn child_storage_root(&mut self, _: &ChildInfo) -> Vec<u8> {
panic!("child_storage_root: unsupported feature for parachain validation")
}
fn storage_changes_root(&mut self, _: &[u8]) -> Result<Option<Vec<u8>>, ()> {
panic!("storage_changes_root: unsupported feature for parachain validation")
}
fn next_child_storage_key(&self, _: &ChildInfo, _: &[u8]) -> Option<Vec<u8>> {
panic!("next_child_storage_key: unsupported feature for parachain validation")
}
fn next_storage_key(&self, _: &[u8]) -> Option<Vec<u8>> {
panic!("next_storage_key: unsupported feature for parachain validation")
}
fn storage_append(
&mut self,
_key: Vec<u8>,
_value: Vec<u8>,
) {
panic!("storage_append: unsupported feature for parachain validation")
}
fn storage_start_transaction(&mut self) {
panic!("storage_start_transaction: unsupported feature for parachain validation")
}
fn storage_rollback_transaction(&mut self) -> Result<(), ()> {
panic!("storage_rollback_transaction: unsupported feature for parachain validation")
}
fn storage_commit_transaction(&mut self) -> Result<(), ()> {
panic!("storage_commit_transaction: unsupported feature for parachain validation")
}
fn wipe(&mut self) {
panic!("wipe: unsupported feature for parachain validation")
}
fn commit(&mut self) {
panic!("commit: unsupported feature for parachain validation")
}
fn read_write_count(&self) -> (u32, u32, u32, u32) {
panic!("read_write_count: unsupported feature for parachain validation")
}
fn reset_read_write_count(&mut self) {
panic!("reset_read_write_count: unsupported feature for parachain validation")
}
fn get_whitelist(&self) -> Vec<TrackedStorageKey> {
panic!("get_whitelist: unsupported feature for parachain validation")
}
fn set_whitelist(&mut self, _: Vec<TrackedStorageKey>) {
panic!("set_whitelist: unsupported feature for parachain validation")
}
fn set_offchain_storage(&mut self, _: &[u8], _: std::option::Option<&[u8]>) {
panic!("set_offchain_storage: unsupported feature for parachain validation")
}
}
impl sp_externalities::ExtensionStore for ValidationExternalities {
fn extension_by_type_id(&mut self, type_id: TypeId) -> Option<&mut dyn Any> {
self.0.get_mut(type_id)
}
fn register_extension_with_type_id(
&mut self,
type_id: TypeId,
extension: Box<dyn sp_externalities::Extension>,
) -> Result<(), sp_externalities::Error> {
self.0.register_with_type_id(type_id, extension)
}
fn deregister_extension_by_type_id(
&mut self,
type_id: TypeId,
) -> Result<(), sp_externalities::Error> {
if self.0.deregister(type_id) {
Ok(())
} else {
Err(sp_externalities::Error::ExtensionIsNotRegistered(type_id))
}
}
}
@@ -1,357 +0,0 @@
// Copyright 2019-2020 Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
#![cfg(not(any(target_os = "android", target_os = "unknown")))]
use std::{env, path::PathBuf, process, sync::Arc, sync::atomic};
use crate::primitives::{ValidationParams, ValidationResult};
use super::{validate_candidate_internal, ValidationError, InvalidCandidate, InternalError};
use parking_lot::Mutex;
use log::{debug, trace};
use futures::executor::ThreadPool;
use sp_core::traits::SpawnNamed;
const WORKER_ARG: &'static str = "validation-worker";
/// CLI Argument to start in validation worker mode.
pub const WORKER_ARGS: &[&'static str] = &[WORKER_ARG];
const LOG_TARGET: &'static str = "parachain::validation-worker";
mod workspace;
/// Execution timeout in seconds;
#[cfg(debug_assertions)]
pub const EXECUTION_TIMEOUT_SEC: u64 = 30;
#[cfg(not(debug_assertions))]
pub const EXECUTION_TIMEOUT_SEC: u64 = 5;
#[derive(Clone)]
struct TaskExecutor(ThreadPool);
impl TaskExecutor {
fn new() -> Result<Self, String> {
ThreadPool::new().map_err(|e| e.to_string()).map(Self)
}
}
impl SpawnNamed for TaskExecutor {
fn spawn_blocking(&self, _: &'static str, future: futures::future::BoxFuture<'static, ()>) {
self.0.spawn_ok(future);
}
fn spawn(&self, _: &'static str, future: futures::future::BoxFuture<'static, ()>) {
self.0.spawn_ok(future);
}
}
/// A pool of hosts.
#[derive(Clone, Debug)]
pub struct ValidationPool {
hosts: Arc<Vec<Mutex<ValidationHost>>>,
}
const DEFAULT_NUM_HOSTS: usize = 8;
impl ValidationPool {
/// Creates a validation pool with the default configuration.
pub fn new() -> ValidationPool {
ValidationPool {
hosts: Arc::new((0..DEFAULT_NUM_HOSTS).map(|_| Default::default()).collect()),
}
}
/// Validate a candidate under the given validation code using the next free validation host.
///
/// This will fail if the validation code is not a proper parachain validation module.
///
/// This function will use `std::env::current_exe()` with the arguments that consist of [`WORKER_ARGS`]
/// with appended `cache_base_path` (if any).
pub fn validate_candidate(
&self,
validation_code: &[u8],
params: ValidationParams,
cache_base_path: Option<&str>,
) -> Result<ValidationResult, ValidationError> {
use std::{iter, borrow::Cow};
let worker_cli_args = match cache_base_path {
Some(cache_base_path) => {
let worker_cli_args: Vec<&str> = WORKER_ARGS
.into_iter()
.cloned()
.chain(iter::once(cache_base_path))
.collect();
Cow::from(worker_cli_args)
}
None => Cow::from(WORKER_ARGS),
};
self.validate_candidate_custom(
validation_code,
params,
&env::current_exe().map_err(|err| ValidationError::Internal(err.into()))?,
&worker_cli_args,
)
}
/// Validate a candidate under the given validation code using the next free validation host.
///
/// This will fail if the validation code is not a proper parachain validation module.
///
/// This function will use the command and the arguments provided in the function's arguments to run the worker.
pub fn validate_candidate_custom(
&self,
validation_code: &[u8],
params: ValidationParams,
command: &PathBuf,
args: &[&str],
) -> Result<ValidationResult, ValidationError> {
for host in self.hosts.iter() {
if let Some(mut host) = host.try_lock() {
return host.validate_candidate(validation_code, params, command, args);
}
}
// all workers are busy, just wait for the first one
self.hosts[0]
.lock()
.validate_candidate(validation_code, params, command, args)
}
}
/// Validation worker process entry point. Runs a loop waiting for candidates to validate
/// and sends back results via shared memory.
pub fn run_worker(mem_id: &str, cache_base_path: Option<PathBuf>) -> Result<(), String> {
let mut worker_handle = match workspace::open(mem_id) {
Err(e) => {
debug!(
target: LOG_TARGET,
"{} Error opening shared memory: {:?}",
process::id(),
e
);
return Err(e);
}
Ok(h) => h,
};
let exit = Arc::new(atomic::AtomicBool::new(false));
let task_executor = TaskExecutor::new()?;
// spawn parent monitor thread
let watch_exit = exit.clone();
std::thread::spawn(move || {
use std::io::Read;
let mut in_data = Vec::new();
// pipe terminates when parent process exits
std::io::stdin().read_to_end(&mut in_data).ok();
debug!(
target: LOG_TARGET,
"{} Parent process is dead. Exiting",
process::id()
);
exit.store(true, atomic::Ordering::Relaxed);
});
worker_handle.signal_ready()?;
let executor = super::ExecutorCache::new(cache_base_path);
loop {
if watch_exit.load(atomic::Ordering::Relaxed) {
break;
}
debug!(
target: LOG_TARGET,
"{} Waiting for candidate",
process::id()
);
let work_item = match worker_handle.wait_for_work(3) {
Err(workspace::WaitForWorkErr::Wait(e)) => {
trace!(
target: LOG_TARGET,
"{} Timeout waiting for candidate: {:?}",
process::id(),
e
);
continue;
}
Err(workspace::WaitForWorkErr::FailedToDecode(e)) => {
return Err(e);
}
Ok(work_item) => work_item,
};
debug!(target: LOG_TARGET, "{} Processing candidate", process::id());
let result = validate_candidate_internal(
&executor,
work_item.code,
work_item.params,
task_executor.clone(),
);
debug!(
target: LOG_TARGET,
"{} Candidate validated: {:?}",
process::id(),
result
);
let result_header = match result {
Ok(r) => workspace::ValidationResultHeader::Ok(r),
Err(ValidationError::Internal(e)) => workspace::ValidationResultHeader::Error(
workspace::WorkerValidationError::InternalError(e.to_string()),
),
Err(ValidationError::InvalidCandidate(e)) => workspace::ValidationResultHeader::Error(
workspace::WorkerValidationError::ValidationError(e.to_string()),
),
};
worker_handle
.report_result(result_header)
.map_err(|e| format!("error reporting result: {:?}", e))?;
}
Ok(())
}
unsafe impl Send for ValidationHost {}
#[derive(Default, Debug)]
struct ValidationHost {
worker: Option<process::Child>,
host_handle: Option<workspace::HostHandle>,
id: u32,
}
impl Drop for ValidationHost {
fn drop(&mut self) {
if let Some(ref mut worker) = &mut self.worker {
worker.kill().ok();
}
}
}
impl ValidationHost {
fn start_worker(&mut self, cmd: &PathBuf, args: &[&str]) -> Result<(), InternalError> {
if let Some(ref mut worker) = self.worker {
// Check if still alive
if let Ok(None) = worker.try_wait() {
// Still running
return Ok(());
}
}
let host_handle =
workspace::create().map_err(|msg| InternalError::FailedToCreateSharedMemory(msg))?;
debug!(
target: LOG_TARGET,
"Starting worker at {:?} with arguments: {:?} and {:?}",
cmd,
args,
host_handle.id(),
);
let worker = process::Command::new(cmd)
.args(args)
.arg(host_handle.id())
.stdin(process::Stdio::piped())
.spawn()?;
self.id = worker.id();
self.worker = Some(worker);
host_handle
.wait_until_ready(EXECUTION_TIMEOUT_SEC)
.map_err(|e| InternalError::WorkerStartTimeout(format!("{:?}", e)))?;
self.host_handle = Some(host_handle);
Ok(())
}
/// Validate a candidate under the given validation code.
///
/// This will fail if the validation code is not a proper parachain validation module.
pub fn validate_candidate(
&mut self,
validation_code: &[u8],
params: ValidationParams,
binary: &PathBuf,
args: &[&str],
) -> Result<ValidationResult, ValidationError> {
// First, check if need to spawn the child process
self.start_worker(binary, args)?;
let host_handle = self
.host_handle
.as_mut()
.expect("host_handle is always `Some` after `start_worker` completes successfully");
debug!(target: LOG_TARGET, "{} Signaling candidate", self.id);
match host_handle.request_validation(validation_code, params) {
Ok(()) => {}
Err(workspace::RequestValidationErr::CodeTooLarge { actual, max }) => {
return Err(ValidationError::InvalidCandidate(
InvalidCandidate::CodeTooLarge(actual, max),
));
}
Err(workspace::RequestValidationErr::ParamsTooLarge { actual, max }) => {
return Err(ValidationError::InvalidCandidate(
InvalidCandidate::ParamsTooLarge(actual, max),
));
}
Err(workspace::RequestValidationErr::Signal(msg)) => {
return Err(ValidationError::Internal(InternalError::FailedToSignal(msg)));
}
Err(workspace::RequestValidationErr::WriteData(msg)) => {
return Err(ValidationError::Internal(InternalError::FailedToWriteData(msg)));
}
}
debug!(target: LOG_TARGET, "{} Waiting for results", self.id);
let result_header = match host_handle.wait_for_result(EXECUTION_TIMEOUT_SEC) {
Ok(inner_result) => inner_result,
Err(assumed_timeout) => {
debug!(target: LOG_TARGET, "Worker timeout: {:?}", assumed_timeout);
if let Some(mut worker) = self.worker.take() {
worker.kill().ok();
}
return Err(ValidationError::InvalidCandidate(InvalidCandidate::Timeout));
}
};
match result_header {
workspace::ValidationResultHeader::Ok(result) => Ok(result),
workspace::ValidationResultHeader::Error(
workspace::WorkerValidationError::InternalError(e),
) => {
debug!(
target: LOG_TARGET,
"{} Internal validation error: {}", self.id, e
);
Err(ValidationError::Internal(InternalError::WasmWorker(e)))
}
workspace::ValidationResultHeader::Error(
workspace::WorkerValidationError::ValidationError(e),
) => {
debug!(
target: LOG_TARGET,
"{} External validation error: {}", self.id, e
);
Err(ValidationError::InvalidCandidate(
InvalidCandidate::ExternalWasmExecutor(e),
))
}
}
}
}
@@ -1,614 +0,0 @@
// Copyright 2021 Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
//! This module implements a "workspace" - basically a wrapper around a shared memory that
//! is used as an IPC channel for communication between the validation host and it's validation
//! worker.
use crate::primitives::{ValidationParams, ValidationResult};
use super::LOG_TARGET;
use parity_scale_codec::{Decode, Encode};
use raw_sync::{
events::{Event, EventImpl, EventInit, EventState},
Timeout,
};
use shared_memory::{Shmem, ShmemConf};
use std::{
error::Error,
fmt,
io::{Cursor, Write},
slice,
sync::atomic::AtomicBool,
time::Duration,
};
// maximum memory in bytes
const MAX_PARAMS_MEM: usize = 16 * 1024 * 1024; // 16 MiB
const MAX_CODE_MEM: usize = 16 * 1024 * 1024; // 16 MiB
/// The size of the shared workspace region. The maximum amount
const SHARED_WORKSPACE_SIZE: usize = MAX_PARAMS_MEM + MAX_CODE_MEM + (1024 * 1024);
/// Params header in shared memory. All offsets should be aligned to WASM page size.
#[derive(Encode, Decode, Debug)]
struct ValidationHeader {
code_size: u64,
params_size: u64,
}
/// An error that could happen during validation of a candidate.
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub enum WorkerValidationError {
InternalError(String),
ValidationError(String),
}
/// An enum that is used to marshal a validation result in order to pass it through the shared memory.
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub enum ValidationResultHeader {
Ok(ValidationResult),
Error(WorkerValidationError),
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
enum Mode {
Initialize,
Attach,
}
fn stringify_err(err: Box<dyn Error>) -> String {
format!("{:?}", err)
}
struct Inner {
shmem: Shmem,
candidate_ready_ev: Box<dyn EventImpl>,
result_ready_ev: Box<dyn EventImpl>,
worker_ready_ev: Box<dyn EventImpl>,
/// Flag that indicates that the worker side is attached to this workspace.
///
/// While there are apparent problems attaching multiple workers to the same workspace, we don't
/// need that anyway. So to make our reasoning a little bit simpler just add a flag and check
/// it before attaching.
attached: *mut AtomicBool,
/// The number of bytes reserved by the auxilary stuff like events from the beginning of the
/// shared memory area.
///
/// We expect this to be way smaller than the whole shmem size.
consumed: usize,
}
impl Inner {
fn layout(shmem: Shmem, mode: Mode) -> Self {
unsafe {
let base_ptr = shmem.as_ptr();
let mut consumed = 0;
let candidate_ready_ev = add_event(base_ptr, &mut consumed, mode);
let result_ready_ev = add_event(base_ptr, &mut consumed, mode);
let worker_ready_ev = add_event(base_ptr, &mut consumed, mode);
// The size of AtomicBool is guaranteed to be the same as the bool, however, docs
// on the bool primitve doesn't actually state that the in-memory size is equal to 1 byte.
//
// AtomicBool requires hardware support of 1 byte width of atomic operations though, so
// that should be fine.
//
// We still assert here to be safe than sorry.
static_assertions::assert_eq_size!(AtomicBool, u8);
// SAFETY: `AtomicBool` is represented by an u8 thus will be happy to take any alignment.
let attached = base_ptr.add(consumed) as *mut AtomicBool;
consumed += 1;
let consumed = align_up_to(consumed, 64);
Self {
shmem,
attached,
consumed,
candidate_ready_ev,
result_ready_ev,
worker_ready_ev,
}
}
}
fn as_slice(&self) -> &[u8] {
unsafe {
let base_ptr = self.shmem.as_ptr().add(self.consumed);
let remaining = self.shmem.len() - self.consumed;
slice::from_raw_parts(base_ptr, remaining)
}
}
fn as_slice_mut(&mut self) -> &mut [u8] {
unsafe {
let base_ptr = self.shmem.as_ptr().add(self.consumed);
let remaining = self.shmem.len() - self.consumed;
slice::from_raw_parts_mut(base_ptr, remaining)
}
}
/// Mark that this workspace has an attached worker already. Returning `true` means that this
/// was the first worker attached.
fn declare_exclusive_attached(&self) -> bool {
unsafe {
// If this succeeded then the value was `false`, thus, we managed to attach exclusively.
(&*self.attached)
.compare_exchange_weak(
false,
true,
std::sync::atomic::Ordering::SeqCst,
std::sync::atomic::Ordering::SeqCst,
)
.is_ok()
}
}
}
fn align_up_to(v: usize, alignment: usize) -> usize {
((v + alignment - 1) / alignment) * alignment
}
/// Initializes a new or attaches to an exising event.
///
/// # Safety
///
/// This function should be called with the combination of `base_ptr` and `consumed` so that `base_ptr + consumed`
/// points on the memory area that is allocated and accessible.
///
/// This function should be called only once for the same combination of the `base_ptr + consumed` and the mode.
/// Furthermore, this function should be called once for initialization.
///
/// Specifically, `consumed` should not be modified by the caller, it should be passed as is to this function.
unsafe fn add_event(base_ptr: *mut u8, consumed: &mut usize, mode: Mode) -> Box<dyn EventImpl> {
// SAFETY: there is no safety proof since the documentation doesn't specify the particular constraints
// besides requiring the pointer to be valid. AFAICT, the pointer is valid.
let ptr = base_ptr.add(*consumed);
const EXPECTATION: &str =
"given that the preconditions were fulfilled, the creation of the event should succeed";
let (ev, used_bytes) = match mode {
Mode::Initialize => Event::new(ptr, true).expect(EXPECTATION),
Mode::Attach => Event::from_existing(ptr).expect(EXPECTATION),
};
*consumed += used_bytes;
ev
}
/// A message received by the worker that specifies a candidate validation work.
pub struct WorkItem<'handle> {
pub params: &'handle [u8],
pub code: &'handle [u8],
}
/// An error that could be returned from [`WorkerHandle::wait_for_work`].
#[derive(Debug)]
pub enum WaitForWorkErr {
/// An error occured during waiting for work. Typically a timeout.
Wait(String),
/// An error ocurred when trying to decode the validation request from the host.
FailedToDecode(String),
}
/// An error that could be returned from [`WorkerHandle::report_result`].
#[derive(Debug)]
pub enum ReportResultErr {
/// An error occured during signalling to the host that the result is ready.
Signal(String),
}
/// A worker side handle to a workspace.
pub struct WorkerHandle {
inner: Inner,
}
impl WorkerHandle {
/// Signals to the validation host that this worker is ready to accept new work requests.
pub fn signal_ready(&self) -> Result<(), String> {
self.inner
.worker_ready_ev
.set(EventState::Signaled)
.map_err(stringify_err)?;
Ok(())
}
/// Waits until a new piece of work. Returns `Err` if the work doesn't come within the given
/// timeout.
pub fn wait_for_work(&mut self, timeout_secs: u64) -> Result<WorkItem, WaitForWorkErr> {
self.inner
.candidate_ready_ev
.wait(Timeout::Val(Duration::from_secs(timeout_secs)))
.map_err(stringify_err)
.map_err(WaitForWorkErr::Wait)?;
let mut cur = self.inner.as_slice();
let header = ValidationHeader::decode(&mut cur)
.map_err(|e| format!("{:?}", e))
.map_err(WaitForWorkErr::FailedToDecode)?;
let (params, cur) = cur.split_at(header.params_size as usize);
let (code, _) = cur.split_at(header.code_size as usize);
Ok(WorkItem { params, code })
}
/// Report back the result of validation.
pub fn report_result(&mut self, result: ValidationResultHeader) -> Result<(), ReportResultErr> {
let mut cur = self.inner.as_slice_mut();
result.encode_to(&mut cur);
self.inner
.result_ready_ev
.set(EventState::Signaled)
.map_err(stringify_err)
.map_err(ReportResultErr::Signal)?;
Ok(())
}
}
/// An error that could be returned from [`HostHandle::wait_until_ready`].
#[derive(Debug)]
pub enum WaitUntilReadyErr {
/// An error occured during waiting for the signal from the worker.
Wait(String),
}
/// An error that could be returned from [`HostHandle::request_validation`].
#[derive(Debug)]
pub enum RequestValidationErr {
/// The code passed exceeds the maximum allowed limit.
CodeTooLarge { actual: usize, max: usize },
/// The call parameters exceed the maximum allowed limit.
ParamsTooLarge { actual: usize, max: usize },
/// An error occured during writing either the code or the call params (the inner string specifies which)
WriteData(&'static str),
/// An error occured during signalling that the request is ready.
Signal(String),
}
/// An error that could be returned from [`HostHandle::wait_for_result`]
#[derive(Debug)]
pub enum WaitForResultErr {
/// A error happened during waiting for the signal. Typically a timeout.
Wait(String),
/// Failed to decode the result header sent by the worker.
HeaderDecodeErr(String),
}
/// A worker side handle to a workspace.
pub struct HostHandle {
inner: Inner,
}
impl fmt::Debug for HostHandle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "HostHandle")
}
}
impl HostHandle {
/// Returns the OS specific ID for this workspace.
pub fn id(&self) -> &str {
self.inner.shmem.get_os_id()
}
/// Wait until the worker is online and ready for accepting validation requests.
pub fn wait_until_ready(&self, timeout_secs: u64) -> Result<(), WaitUntilReadyErr> {
self.inner
.worker_ready_ev
.wait(Timeout::Val(Duration::from_secs(timeout_secs)))
.map_err(stringify_err)
.map_err(WaitUntilReadyErr::Wait)?;
Ok(())
}
/// Request validation with the given code and parameters.
pub fn request_validation(
&mut self,
code: &[u8],
params: ValidationParams,
) -> Result<(), RequestValidationErr> {
if code.len() > MAX_CODE_MEM {
return Err(RequestValidationErr::CodeTooLarge {
actual: code.len(),
max: MAX_CODE_MEM,
});
}
let params = params.encode();
if params.len() > MAX_PARAMS_MEM {
return Err(RequestValidationErr::ParamsTooLarge {
actual: params.len(),
max: MAX_PARAMS_MEM,
});
}
let mut cur = Cursor::new(self.inner.as_slice_mut());
ValidationHeader {
code_size: code.len() as u64,
params_size: params.len() as u64,
}
.encode_to(&mut cur);
cur.write_all(&params)
.map_err(|_| RequestValidationErr::WriteData("params"))?;
cur.write_all(code)
.map_err(|_| RequestValidationErr::WriteData("code"))?;
self.inner
.candidate_ready_ev
.set(EventState::Signaled)
.map_err(stringify_err)
.map_err(RequestValidationErr::Signal)?;
Ok(())
}
/// Wait for the validation result from the worker with the given timeout.
///
/// Returns `Ok` if the response was received within the deadline or error otherwise. An error
/// could also occur because of failing decoding the result from the worker. Returning
/// `Ok` doesn't mean that the candidate was successfully validated though, for that the client
/// needs to inspect the returned validation result header.
pub fn wait_for_result(
&self,
execution_timeout: u64,
) -> Result<ValidationResultHeader, WaitForResultErr> {
self.inner
.result_ready_ev
.wait(Timeout::Val(Duration::from_secs(execution_timeout)))
.map_err(|e| WaitForResultErr::Wait(format!("{:?}", e)))?;
let mut cur = self.inner.as_slice();
let header = ValidationResultHeader::decode(&mut cur)
.map_err(|e| WaitForResultErr::HeaderDecodeErr(format!("{:?}", e)))?;
Ok(header)
}
}
/// Create a new workspace and return a handle to it.
pub fn create() -> Result<HostHandle, String> {
let shmem = ShmemConf::new()
.size(SHARED_WORKSPACE_SIZE)
.create()
.map_err(|e| format!("Error creating shared memory: {:?}", e))?;
Ok(HostHandle {
inner: Inner::layout(shmem, Mode::Initialize),
})
}
/// Open a workspace with the given `id`.
///
/// You can attach only once to a single workspace.
pub fn open(id: &str) -> Result<WorkerHandle, String> {
let shmem = ShmemConf::new()
.os_id(id)
.open()
.map_err(|e| format!("Error opening shared memory: {:?}", e))?;
#[cfg(unix)]
unlink_shmem(&id);
let inner = Inner::layout(shmem, Mode::Attach);
if !inner.declare_exclusive_attached() {
return Err(format!("The workspace has been already attached to"));
}
return Ok(WorkerHandle { inner });
#[cfg(unix)]
fn unlink_shmem(shmem_id: &str) {
// Unlink the shmem. Unlinking it from the filesystem will make it unaccessible for further
// opening, however, the kernel will still let the object live until the last reference dies
// out.
//
// There is still a chance that the shm stays on the fs, but that's a highly unlikely case
// that we don't address at this time.
// shared-memory doesn't return file path to the shmem if get_flink_path is called, so we
// resort to `shm_unlink`.
//
// Additionally, even thouygh `fs::remove_file` is said to use `unlink` we still avoid relying on it,
// because the stdlib doesn't actually provide any gurantees on what syscalls will be called.
// (Not sure, what alternative it has though).
unsafe {
// must be in a local var in order to be not deallocated.
let shmem_id_cstr =
std::ffi::CString::new(shmem_id).expect("the shmmem id cannot have NUL in it; qed");
if libc::shm_unlink(shmem_id_cstr.as_ptr()) == -1 {
// failed to remove the shmem file nothing we can do ¯\_(ツ)_/¯
log::warn!(
target: LOG_TARGET,
"failed to remove the shmem with id {}",
shmem_id,
);
}
}
}
}
#[cfg(test)]
mod tests {
use polkadot_core_primitives::OutboundHrmpMessage;
use crate::primitives::BlockData;
use super::*;
use std::thread;
#[test]
fn wait_until_ready() {
let host = create().unwrap();
let worker_handle = thread::spawn({
let id = host.id().to_string();
move || {
let worker = open(&id).unwrap();
worker.signal_ready().unwrap();
}
});
host.wait_until_ready(1).unwrap();
worker_handle.join().unwrap();
}
#[test]
fn wait_until_ready_timeout() {
let host = create().unwrap();
let _worker_handle = thread::spawn({
let id = host.id().to_string();
move || {
let _worker = open(&id).unwrap();
}
});
assert!(matches!(
host.wait_until_ready(1),
Err(WaitUntilReadyErr::Wait(_))
));
}
#[test]
fn open_junk_id() {
assert!(open("").is_err());
assert!(open("non_existent").is_err());
assert!(open("").is_err());
}
#[test]
fn attach_twice() {
let host = create().unwrap();
thread::spawn({
let id = host.id().to_string();
move || {
let _worker1 = open(&id).unwrap();
assert!(open(&id).is_err());
}
});
}
#[test]
fn validation_works() {
let mut host = create().unwrap();
let worker_handle = thread::spawn({
let id = host.id().to_string();
move || {
let mut worker = open(&id).unwrap();
worker.signal_ready().unwrap();
let work = worker.wait_for_work(3).unwrap();
assert_eq!(work.code, b"\0asm\01\00\00\00");
worker
.report_result(ValidationResultHeader::Ok(ValidationResult {
head_data: Default::default(),
new_validation_code: None,
upward_messages: vec![],
horizontal_messages: vec![],
processed_downward_messages: 322,
hrmp_watermark: 0,
}))
.unwrap();
}
});
host.wait_until_ready(1).unwrap();
host.request_validation(
b"\0asm\01\00\00\00",
ValidationParams {
parent_head: Default::default(),
block_data: BlockData(b"hello world".to_vec()),
relay_parent_number: 228,
relay_parent_storage_root: Default::default(),
},
)
.unwrap();
match host.wait_for_result(3).unwrap() {
ValidationResultHeader::Ok(r) => {
assert_eq!(r.processed_downward_messages, 322);
}
_ => panic!(),
}
worker_handle.join().unwrap();
}
#[test]
fn works_with_jumbo_sized_params() {
let mut host = create().unwrap();
let jumbo_code = vec![0x42; 16 * 1024 * 104];
let fat_pov = vec![0x33; 16 * 1024 * 104];
let big_params = ValidationParams {
parent_head: Default::default(),
block_data: BlockData(fat_pov),
relay_parent_number: 228,
relay_parent_storage_root: Default::default(),
// If modifying please make sure that this has a big size.
};
let plump_result = ValidationResultHeader::Ok(ValidationResult {
head_data: Default::default(),
new_validation_code: Some(jumbo_code.clone().into()),
processed_downward_messages: 322,
hrmp_watermark: 0,
// We don't know about the limits here. Just make sure that those are reasonably big.
upward_messages: fill(|| vec![0x99; 8 * 1024], 64),
horizontal_messages: fill(
|| OutboundHrmpMessage {
recipient: 1.into(),
data: vec![0x11; 8 * 1024],
},
64,
),
// If modifying please make sure that this has a big size.
});
let _worker_handle = thread::spawn({
let id = host.id().to_string();
let jumbo_code = jumbo_code.clone();
let big_params = big_params.clone();
let plump_result = plump_result.clone();
move || {
let mut worker = open(&id).unwrap();
worker.signal_ready().unwrap();
let work = worker.wait_for_work(3).unwrap();
assert_eq!(work.code, &jumbo_code);
assert_eq!(work.params, &big_params.encode());
worker.report_result(plump_result).unwrap();
}
});
host.wait_until_ready(1).unwrap();
host.request_validation(&jumbo_code, big_params).unwrap();
assert_eq!(host.wait_for_result(3).unwrap(), plump_result);
fn fill<T, F: Fn() -> T>(f: F, times: usize) -> Vec<T> {
std::iter::repeat_with(f).take(times).collect()
}
}
}
@@ -9,6 +9,10 @@ edition = "2018"
name = "adder-collator"
path = "src/main.rs"
[[bin]]
name = "adder_collator_puppet_worker"
path = "bin/puppet_worker.rs"
[dependencies]
parity-scale-codec = { version = "2.0.0", default-features = false, features = ["derive"] }
futures = "0.3.12"
@@ -28,6 +32,11 @@ sp-core = { git = "https://github.com/paritytech/substrate", branch = "master" }
sc-authority-discovery = { git = "https://github.com/paritytech/substrate", branch = "master" }
sc-service = { git = "https://github.com/paritytech/substrate", branch = "master" }
# This one is tricky. Even though it is not used directly by the collator, we still need it for the
# `puppet_worker` binary, which is required for the integration test. However, this shouldn't be
# a big problem since it is used transitively anyway.
polkadot-node-core-pvf = { path = "../../../../node/core/pvf" }
[dev-dependencies]
polkadot-parachain = { path = "../../.." }
polkadot-test-service = { path = "../../../../node/test/service" }
@@ -1,4 +1,4 @@
// Copyright 2019-2020 Parity Technologies (UK) Ltd.
// Copyright 2021 Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
@@ -14,17 +14,4 @@
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
mod adder;
mod wasm_executor;
use parachain::wasm_executor::run_worker;
// This is not an actual test, but rather an entry point for out-of process WASM executor.
// When executing tests the executor spawns currently executing binary, which happens to be test binary.
// It then passes "validation_worker" on CLI effectivly making rust test executor to run this single test.
#[test]
fn validation_worker() {
if let Some(id) = std::env::args().find(|a| a.starts_with("/shmem_")) {
run_worker(&id, None).unwrap()
}
}
polkadot_node_core_pvf::decl_puppet_worker_main!();
@@ -233,7 +233,7 @@ mod tests {
use super::*;
use futures::executor::block_on;
use polkadot_parachain::{primitives::ValidationParams, wasm_executor::IsolationStrategy};
use polkadot_parachain::{primitives::{ValidationParams, ValidationResult}};
use polkadot_primitives::v1::PersistedValidationData;
#[test]
@@ -268,18 +268,19 @@ mod tests {
parent_head: HeadData,
collation: Collation,
) {
let ret = polkadot_parachain::wasm_executor::validate_candidate(
use polkadot_node_core_pvf::testing::validate_candidate;
let ret_buf = validate_candidate(
collator.validation_code(),
ValidationParams {
&ValidationParams {
parent_head: parent_head.encode().into(),
block_data: collation.proof_of_validity.block_data,
relay_parent_number: 1,
relay_parent_storage_root: Default::default(),
},
&IsolationStrategy::InProcess,
sp_core::testing::TaskExecutor::new(),
}.encode(),
)
.unwrap();
let ret = ValidationResult::decode(&mut &ret_buf[..]).unwrap();
let new_head = HeadData::decode(&mut &ret.head_data.0[..]).unwrap();
assert_eq!(
@@ -17,6 +17,9 @@
//! Integration test that ensures that we can build and include parachain
//! blocks of the adder parachain.
const PUPPET_EXE: &str = env!("CARGO_BIN_EXE_adder_collator_puppet_worker");
// If this test is failing, make sure to run all tests with the `real-overseer` feature being enabled.
#[substrate_test_utils::test]
async fn collating_using_adder_collator(task_executor: sc_service::TaskExecutor) {
use sp_keyring::AccountKeyring::*;
@@ -30,7 +33,12 @@ async fn collating_using_adder_collator(task_executor: sc_service::TaskExecutor)
let para_id = ParaId::from(100);
// start alice
let alice = polkadot_test_service::run_validator_node(task_executor.clone(), Alice, || {}, vec![]);
let alice = polkadot_test_service::run_validator_node(
task_executor.clone(),
Alice, || {},
vec![],
Some(PUPPET_EXE.into()),
);
// start bob
let bob = polkadot_test_service::run_validator_node(
@@ -38,6 +46,7 @@ async fn collating_using_adder_collator(task_executor: sc_service::TaskExecutor)
Bob,
|| {},
vec![alice.addr.clone()],
Some(PUPPET_EXE.into()),
);
let collator = test_parachain_adder_collator::Collator::new();
@@ -1,153 +0,0 @@
// Copyright 2017-2020 Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
//! Basic parachain that adds a number as part of its state.
const WORKER_ARGS_TEST: &[&'static str] = &["--nocapture", "validation_worker"];
use parachain::{
primitives::{
RelayChainBlockNumber,
BlockData as GenericBlockData,
HeadData as GenericHeadData,
ValidationParams,
},
wasm_executor::{ValidationPool, IsolationStrategy}
};
use parity_scale_codec::{Decode, Encode};
use adder::{HeadData, BlockData, hash_state};
fn isolation_strategy() -> IsolationStrategy {
IsolationStrategy::ExternalProcessCustomHost {
pool: ValidationPool::new(),
binary: std::env::current_exe().unwrap(),
args: WORKER_ARGS_TEST.iter().map(|x| x.to_string()).collect(),
}
}
#[test]
fn execute_good_on_parent_with_inprocess_validation() {
let isolation_strategy = IsolationStrategy::InProcess;
execute_good_on_parent(isolation_strategy);
}
#[test]
pub fn execute_good_on_parent_with_external_process_validation() {
let isolation_strategy = isolation_strategy();
execute_good_on_parent(isolation_strategy);
}
fn execute_good_on_parent(isolation_strategy: IsolationStrategy) {
let parent_head = HeadData {
number: 0,
parent_hash: [0; 32],
post_state: hash_state(0),
};
let block_data = BlockData {
state: 0,
add: 512,
};
let ret = parachain::wasm_executor::validate_candidate(
adder::wasm_binary_unwrap(),
ValidationParams {
parent_head: GenericHeadData(parent_head.encode()),
block_data: GenericBlockData(block_data.encode()),
relay_parent_number: 1,
relay_parent_storage_root: Default::default(),
},
&isolation_strategy,
sp_core::testing::TaskExecutor::new(),
).unwrap();
let new_head = HeadData::decode(&mut &ret.head_data.0[..]).unwrap();
assert_eq!(new_head.number, 1);
assert_eq!(new_head.parent_hash, parent_head.hash());
assert_eq!(new_head.post_state, hash_state(512));
}
#[test]
fn execute_good_chain_on_parent() {
let mut number = 0;
let mut parent_hash = [0; 32];
let mut last_state = 0;
let isolation_strategy = isolation_strategy();
for add in 0..10 {
let parent_head = HeadData {
number,
parent_hash,
post_state: hash_state(last_state),
};
let block_data = BlockData {
state: last_state,
add,
};
let ret = parachain::wasm_executor::validate_candidate(
adder::wasm_binary_unwrap(),
ValidationParams {
parent_head: GenericHeadData(parent_head.encode()),
block_data: GenericBlockData(block_data.encode()),
relay_parent_number: number as RelayChainBlockNumber + 1,
relay_parent_storage_root: Default::default(),
},
&isolation_strategy,
sp_core::testing::TaskExecutor::new(),
).unwrap();
let new_head = HeadData::decode(&mut &ret.head_data.0[..]).unwrap();
assert_eq!(new_head.number, number + 1);
assert_eq!(new_head.parent_hash, parent_head.hash());
assert_eq!(new_head.post_state, hash_state(last_state + add));
number += 1;
parent_hash = new_head.hash();
last_state += add;
}
}
#[test]
fn execute_bad_on_parent() {
let isolation_strategy = isolation_strategy();
let parent_head = HeadData {
number: 0,
parent_hash: [0; 32],
post_state: hash_state(0),
};
let block_data = BlockData {
state: 256, // start state is wrong.
add: 256,
};
let _ret = parachain::wasm_executor::validate_candidate(
adder::wasm_binary_unwrap(),
ValidationParams {
parent_head: GenericHeadData(parent_head.encode()),
block_data: GenericBlockData(block_data.encode()),
relay_parent_number: 1,
relay_parent_storage_root: Default::default(),
},
&isolation_strategy,
sp_core::testing::TaskExecutor::new(),
).unwrap_err();
}
@@ -1,94 +0,0 @@
// Copyright 2019-2020 Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
//! Basic parachain that adds a number as part of its state.
const WORKER_ARGS_TEST: &[&'static str] = &["--nocapture", "validation_worker"];
use crate::adder;
use parachain::{
primitives::{BlockData, ValidationParams},
wasm_executor::{ValidationError, InvalidCandidate, EXECUTION_TIMEOUT_SEC, IsolationStrategy, ValidationPool},
};
fn isolation_strategy() -> IsolationStrategy {
IsolationStrategy::ExternalProcessCustomHost {
pool: ValidationPool::new(),
binary: std::env::current_exe().unwrap(),
args: WORKER_ARGS_TEST.iter().map(|x| x.to_string()).collect(),
}
}
#[test]
fn terminates_on_timeout() {
let isolation_strategy = isolation_strategy();
let result = parachain::wasm_executor::validate_candidate(
halt::wasm_binary_unwrap(),
ValidationParams {
block_data: BlockData(Vec::new()),
parent_head: Default::default(),
relay_parent_number: 1,
relay_parent_storage_root: Default::default(),
},
&isolation_strategy,
sp_core::testing::TaskExecutor::new(),
);
match result {
Err(ValidationError::InvalidCandidate(InvalidCandidate::Timeout)) => {},
r => panic!("{:?}", r),
}
// check that another parachain can validate normaly
adder::execute_good_on_parent_with_external_process_validation();
}
#[test]
fn parallel_execution() {
let isolation_strategy = isolation_strategy();
let isolation_strategy_clone = isolation_strategy.clone();
let start = std::time::Instant::now();
let thread = std::thread::spawn(move ||
parachain::wasm_executor::validate_candidate(
halt::wasm_binary_unwrap(),
ValidationParams {
block_data: BlockData(Vec::new()),
parent_head: Default::default(),
relay_parent_number: 1,
relay_parent_storage_root: Default::default(),
},
&isolation_strategy,
sp_core::testing::TaskExecutor::new(),
).ok());
let _ = parachain::wasm_executor::validate_candidate(
halt::wasm_binary_unwrap(),
ValidationParams {
block_data: BlockData(Vec::new()),
parent_head: Default::default(),
relay_parent_storage_root: Default::default(),
relay_parent_number: 1,
},
&isolation_strategy_clone,
sp_core::testing::TaskExecutor::new(),
);
thread.join().unwrap();
// total time should be < 2 x EXECUTION_TIMEOUT_SEC
assert!(
std::time::Instant::now().duration_since(start)
< std::time::Duration::from_secs(EXECUTION_TIMEOUT_SEC * 2)
);
}