mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-30 10:31:03 +00:00
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:
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
¶ms.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(¶ms)
|
||||
.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" }
|
||||
|
||||
+2
-15
@@ -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)
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user