feat: initialize Kurdistan SDK - independent fork of Polkadot SDK
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! PVF host integration tests checking the chain production pipeline.
|
||||
|
||||
use super::TestHost;
|
||||
use codec::{Decode, Encode};
|
||||
use pezkuwi_node_primitives::PoV;
|
||||
use pezkuwi_primitives::PersistedValidationData;
|
||||
use pezkuwi_teyrchain_primitives::primitives::{
|
||||
BlockData as GenericBlockData, HeadData as GenericHeadData,
|
||||
};
|
||||
use sp_core::H256;
|
||||
use test_teyrchain_adder::{hash_state, BlockData, HeadData};
|
||||
|
||||
#[tokio::test]
|
||||
async fn execute_good_block_on_parent() {
|
||||
let parent_head = HeadData { number: 0, parent_hash: [0; 32], post_state: hash_state(0) };
|
||||
let block_data = BlockData { state: 0, add: 512 };
|
||||
let pvd = PersistedValidationData {
|
||||
parent_head: GenericHeadData(parent_head.encode()),
|
||||
relay_parent_number: 1u32,
|
||||
relay_parent_storage_root: H256::default(),
|
||||
max_pov_size: 4096 * 1024,
|
||||
};
|
||||
let pov = PoV { block_data: GenericBlockData(block_data.encode()) };
|
||||
|
||||
let host = TestHost::new().await;
|
||||
|
||||
let ret = host
|
||||
.validate_candidate(
|
||||
test_teyrchain_adder::wasm_binary_unwrap(),
|
||||
pvd,
|
||||
pov,
|
||||
Default::default(),
|
||||
H256::default(),
|
||||
)
|
||||
.await
|
||||
.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));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn execute_good_chain_on_parent() {
|
||||
let mut parent_hash = [0; 32];
|
||||
let mut last_state = 0;
|
||||
|
||||
let host = TestHost::new().await;
|
||||
|
||||
for (number, add) in (0..10).enumerate() {
|
||||
let parent_head =
|
||||
HeadData { number: number as u64, parent_hash, post_state: hash_state(last_state) };
|
||||
let block_data = BlockData { state: last_state, add };
|
||||
let pvd = PersistedValidationData {
|
||||
parent_head: GenericHeadData(parent_head.encode()),
|
||||
relay_parent_number: 1u32,
|
||||
relay_parent_storage_root: H256::default(),
|
||||
max_pov_size: 4096 * 1024,
|
||||
};
|
||||
let pov = PoV { block_data: GenericBlockData(block_data.encode()) };
|
||||
|
||||
let ret = host
|
||||
.validate_candidate(
|
||||
test_teyrchain_adder::wasm_binary_unwrap(),
|
||||
pvd,
|
||||
pov,
|
||||
Default::default(),
|
||||
H256::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let new_head = HeadData::decode(&mut &ret.head_data.0[..]).unwrap();
|
||||
|
||||
assert_eq!(new_head.number, number as u64 + 1);
|
||||
assert_eq!(new_head.parent_hash, parent_head.hash());
|
||||
assert_eq!(new_head.post_state, hash_state(last_state + add));
|
||||
|
||||
parent_hash = new_head.hash();
|
||||
last_state += add;
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn execute_bad_block_on_parent() {
|
||||
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 pvd = PersistedValidationData {
|
||||
parent_head: GenericHeadData(parent_head.encode()),
|
||||
relay_parent_number: 1u32,
|
||||
relay_parent_storage_root: H256::default(),
|
||||
max_pov_size: 4096 * 1024,
|
||||
};
|
||||
let pov = PoV { block_data: GenericBlockData(block_data.encode()) };
|
||||
|
||||
let host = TestHost::new().await;
|
||||
|
||||
let _err = host
|
||||
.validate_candidate(
|
||||
test_teyrchain_adder::wasm_binary_unwrap(),
|
||||
pvd,
|
||||
pov,
|
||||
Default::default(),
|
||||
H256::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stress_spawn() {
|
||||
let host = std::sync::Arc::new(TestHost::new().await);
|
||||
|
||||
async fn execute(host: std::sync::Arc<TestHost>) {
|
||||
let parent_head = HeadData { number: 0, parent_hash: [0; 32], post_state: hash_state(0) };
|
||||
let block_data = BlockData { state: 0, add: 512 };
|
||||
let pvd = PersistedValidationData {
|
||||
parent_head: GenericHeadData(parent_head.encode()),
|
||||
relay_parent_number: 1u32,
|
||||
relay_parent_storage_root: H256::default(),
|
||||
max_pov_size: 4096 * 1024,
|
||||
};
|
||||
let pov = PoV { block_data: GenericBlockData(block_data.encode()) };
|
||||
let ret = host
|
||||
.validate_candidate(
|
||||
test_teyrchain_adder::wasm_binary_unwrap(),
|
||||
pvd,
|
||||
pov,
|
||||
Default::default(),
|
||||
H256::default(),
|
||||
)
|
||||
.await
|
||||
.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));
|
||||
}
|
||||
|
||||
futures::future::join_all((0..100).map(|_| execute(host.clone()))).await;
|
||||
}
|
||||
|
||||
// With one worker, run multiple execution jobs serially. They should not conflict.
|
||||
#[tokio::test]
|
||||
async fn execute_can_run_serially() {
|
||||
let host = std::sync::Arc::new(
|
||||
TestHost::new_with_config(|cfg| {
|
||||
cfg.execute_workers_max_num = 1;
|
||||
})
|
||||
.await,
|
||||
);
|
||||
|
||||
async fn execute(host: std::sync::Arc<TestHost>) {
|
||||
let parent_head = HeadData { number: 0, parent_hash: [0; 32], post_state: hash_state(0) };
|
||||
let block_data = BlockData { state: 0, add: 512 };
|
||||
let pvd = PersistedValidationData {
|
||||
parent_head: GenericHeadData(parent_head.encode()),
|
||||
relay_parent_number: 1u32,
|
||||
relay_parent_storage_root: H256::default(),
|
||||
max_pov_size: 4096 * 1024,
|
||||
};
|
||||
let pov = PoV { block_data: GenericBlockData(block_data.encode()) };
|
||||
let ret = host
|
||||
.validate_candidate(
|
||||
test_teyrchain_adder::wasm_binary_unwrap(),
|
||||
pvd,
|
||||
pov,
|
||||
Default::default(),
|
||||
H256::default(),
|
||||
)
|
||||
.await
|
||||
.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));
|
||||
}
|
||||
|
||||
futures::future::join_all((0..5).map(|_| execute(host.clone()))).await;
|
||||
}
|
||||
@@ -0,0 +1,861 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! General PVF host integration tests checking the functionality of the PVF host itself.
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
#[cfg(all(feature = "ci-only-tests", target_os = "linux"))]
|
||||
use pezkuwi_node_core_pvf::SecurityStatus;
|
||||
use pezkuwi_node_core_pvf::{
|
||||
start, testing::build_workers_and_get_paths, Config, InvalidCandidate, Metrics,
|
||||
PossiblyInvalidError, PrepareError, PrepareJobKind, PvfPrepData, ValidationError,
|
||||
ValidationHost, JOB_TIMEOUT_WALL_CLOCK_FACTOR,
|
||||
};
|
||||
use pezkuwi_node_core_pvf_common::{compute_checksum, ArtifactChecksum};
|
||||
use pezkuwi_node_primitives::{PoV, POV_BOMB_LIMIT};
|
||||
use pezkuwi_node_subsystem::messages::PvfExecKind;
|
||||
use pezkuwi_primitives::{
|
||||
ExecutorParam, ExecutorParams, Hash, PersistedValidationData, PvfExecKind as RuntimePvfExecKind,
|
||||
};
|
||||
use pezkuwi_teyrchain_primitives::primitives::{BlockData, ValidationResult};
|
||||
use sp_core::H256;
|
||||
|
||||
const VALIDATION_CODE_BOMB_LIMIT: u32 = 30 * 1024 * 1024;
|
||||
|
||||
use std::{io::Write, sync::Arc, time::Duration};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
mod adder;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod process;
|
||||
mod worker_common;
|
||||
|
||||
const TEST_EXECUTION_TIMEOUT: Duration = Duration::from_secs(6);
|
||||
const TEST_PREPARATION_TIMEOUT: Duration = Duration::from_secs(6);
|
||||
|
||||
struct TestHost {
|
||||
// Keep a reference to the tempdir as it gets deleted on drop.
|
||||
cache_dir: tempfile::TempDir,
|
||||
host: Mutex<ValidationHost>,
|
||||
}
|
||||
|
||||
impl TestHost {
|
||||
async fn new() -> Self {
|
||||
Self::new_with_config(|_| ()).await
|
||||
}
|
||||
|
||||
async fn new_with_config<F>(f: F) -> Self
|
||||
where
|
||||
F: FnOnce(&mut Config),
|
||||
{
|
||||
let (prepare_worker_path, execute_worker_path) = build_workers_and_get_paths();
|
||||
|
||||
let cache_dir = tempfile::tempdir().unwrap();
|
||||
let mut config = Config::new(
|
||||
cache_dir.path().to_owned(),
|
||||
None,
|
||||
false,
|
||||
prepare_worker_path,
|
||||
execute_worker_path,
|
||||
2,
|
||||
1,
|
||||
2,
|
||||
);
|
||||
f(&mut config);
|
||||
let (host, task) = start(config, Metrics::default()).await.unwrap();
|
||||
let _ = tokio::task::spawn(task);
|
||||
Self { cache_dir, host: Mutex::new(host) }
|
||||
}
|
||||
|
||||
async fn precheck_pvf(
|
||||
&self,
|
||||
code: &[u8],
|
||||
executor_params: ExecutorParams,
|
||||
) -> Result<(), PrepareError> {
|
||||
let (result_tx, result_rx) = futures::channel::oneshot::channel();
|
||||
|
||||
self.host
|
||||
.lock()
|
||||
.await
|
||||
.precheck_pvf(
|
||||
PvfPrepData::from_code(
|
||||
code.into(),
|
||||
executor_params,
|
||||
TEST_PREPARATION_TIMEOUT,
|
||||
PrepareJobKind::Prechecking,
|
||||
VALIDATION_CODE_BOMB_LIMIT,
|
||||
),
|
||||
result_tx,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
result_rx.await.unwrap()
|
||||
}
|
||||
|
||||
async fn validate_candidate(
|
||||
&self,
|
||||
code: &[u8],
|
||||
pvd: PersistedValidationData,
|
||||
pov: PoV,
|
||||
executor_params: ExecutorParams,
|
||||
relay_parent: Hash,
|
||||
) -> Result<ValidationResult, ValidationError> {
|
||||
let (result_tx, result_rx) = futures::channel::oneshot::channel();
|
||||
|
||||
self.host
|
||||
.lock()
|
||||
.await
|
||||
.execute_pvf(
|
||||
PvfPrepData::from_code(
|
||||
code.into(),
|
||||
executor_params,
|
||||
TEST_PREPARATION_TIMEOUT,
|
||||
PrepareJobKind::Compilation,
|
||||
VALIDATION_CODE_BOMB_LIMIT,
|
||||
),
|
||||
TEST_EXECUTION_TIMEOUT,
|
||||
Arc::new(pvd),
|
||||
Arc::new(pov),
|
||||
pezkuwi_node_core_pvf::Priority::Normal,
|
||||
PvfExecKind::Backing(relay_parent),
|
||||
result_tx,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
result_rx.await.unwrap()
|
||||
}
|
||||
|
||||
async fn replace_artifact_checksum(
|
||||
&self,
|
||||
checksum: ArtifactChecksum,
|
||||
new_checksum: ArtifactChecksum,
|
||||
) {
|
||||
self.host
|
||||
.lock()
|
||||
.await
|
||||
.replace_artifact_checksum(checksum, new_checksum)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "ci-only-tests", target_os = "linux"))]
|
||||
async fn security_status(&self) -> SecurityStatus {
|
||||
self.host.lock().await.security_status.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn prepare_job_terminates_on_timeout() {
|
||||
let host = TestHost::new().await;
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
let result = host
|
||||
.precheck_pvf(pezkuwichain_runtime::WASM_BINARY.unwrap(), Default::default())
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Err(PrepareError::TimedOut) => {},
|
||||
r => panic!("{:?}", r),
|
||||
}
|
||||
|
||||
let duration = std::time::Instant::now().duration_since(start);
|
||||
assert!(duration >= TEST_PREPARATION_TIMEOUT);
|
||||
assert!(duration < TEST_PREPARATION_TIMEOUT * JOB_TIMEOUT_WALL_CLOCK_FACTOR);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn execute_job_terminates_on_timeout() {
|
||||
let host = TestHost::new().await;
|
||||
let pvd = PersistedValidationData {
|
||||
parent_head: Default::default(),
|
||||
relay_parent_number: 1u32,
|
||||
relay_parent_storage_root: H256::default(),
|
||||
max_pov_size: 4096 * 1024,
|
||||
};
|
||||
let pov = PoV { block_data: BlockData(Vec::new()) };
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
let result = host
|
||||
.validate_candidate(
|
||||
test_teyrchain_halt::wasm_binary_unwrap(),
|
||||
pvd,
|
||||
pov,
|
||||
Default::default(),
|
||||
H256::default(),
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Err(ValidationError::Invalid(InvalidCandidate::HardTimeout)) => {},
|
||||
r => panic!("{:?}", r),
|
||||
}
|
||||
|
||||
let duration = std::time::Instant::now().duration_since(start);
|
||||
assert!(duration >= TEST_EXECUTION_TIMEOUT);
|
||||
assert!(duration < TEST_EXECUTION_TIMEOUT * JOB_TIMEOUT_WALL_CLOCK_FACTOR);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ci-only-tests")]
|
||||
#[tokio::test]
|
||||
async fn ensure_parallel_execution() {
|
||||
// Run some jobs that do not complete, thus timing out.
|
||||
let host = TestHost::new().await;
|
||||
let pvd = PersistedValidationData {
|
||||
parent_head: Default::default(),
|
||||
relay_parent_number: 1u32,
|
||||
relay_parent_storage_root: H256::default(),
|
||||
max_pov_size: 4096 * 1024,
|
||||
};
|
||||
let pov = PoV { block_data: BlockData(Vec::new()) };
|
||||
let execute_pvf_future_1 = host.validate_candidate(
|
||||
test_teyrchain_halt::wasm_binary_unwrap(),
|
||||
pvd.clone(),
|
||||
pov.clone(),
|
||||
Default::default(),
|
||||
H256::default(),
|
||||
);
|
||||
let execute_pvf_future_2 = host.validate_candidate(
|
||||
test_teyrchain_halt::wasm_binary_unwrap(),
|
||||
pvd,
|
||||
pov,
|
||||
Default::default(),
|
||||
H256::default(),
|
||||
);
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
let (res1, res2) = futures::join!(execute_pvf_future_1, execute_pvf_future_2);
|
||||
assert_matches!(
|
||||
(res1, res2),
|
||||
(
|
||||
Err(ValidationError::Invalid(InvalidCandidate::HardTimeout)),
|
||||
Err(ValidationError::Invalid(InvalidCandidate::HardTimeout))
|
||||
)
|
||||
);
|
||||
|
||||
// Total time should be < 2 x TEST_EXECUTION_TIMEOUT (two workers run in parallel).
|
||||
let duration = std::time::Instant::now().duration_since(start);
|
||||
let max_duration = 2 * TEST_EXECUTION_TIMEOUT;
|
||||
assert!(
|
||||
duration < max_duration,
|
||||
"Expected duration {}ms to be less than {}ms",
|
||||
duration.as_millis(),
|
||||
max_duration.as_millis()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn execute_queue_doesnt_stall_if_workers_died() {
|
||||
let host = TestHost::new_with_config(|cfg| {
|
||||
cfg.execute_workers_max_num = 5;
|
||||
})
|
||||
.await;
|
||||
let pvd = PersistedValidationData {
|
||||
parent_head: Default::default(),
|
||||
relay_parent_number: 1u32,
|
||||
relay_parent_storage_root: H256::default(),
|
||||
max_pov_size: 4096 * 1024,
|
||||
};
|
||||
let pov = PoV { block_data: BlockData(Vec::new()) };
|
||||
|
||||
// Here we spawn 8 validation jobs for the `halt` PVF and share those between 5 workers. The
|
||||
// first five jobs should timeout and the workers killed. For the next 3 jobs a new batch of
|
||||
// workers should be spun up.
|
||||
let start = std::time::Instant::now();
|
||||
futures::future::join_all((0u8..=8).map(|_| {
|
||||
host.validate_candidate(
|
||||
test_teyrchain_halt::wasm_binary_unwrap(),
|
||||
pvd.clone(),
|
||||
pov.clone(),
|
||||
Default::default(),
|
||||
H256::default(),
|
||||
)
|
||||
}))
|
||||
.await;
|
||||
|
||||
// Total time should be >= 2 x TEST_EXECUTION_TIMEOUT (two separate sets of workers that should
|
||||
// both timeout).
|
||||
let duration = std::time::Instant::now().duration_since(start);
|
||||
let max_duration = 2 * TEST_EXECUTION_TIMEOUT;
|
||||
assert!(
|
||||
duration >= max_duration,
|
||||
"Expected duration {}ms to be greater than or equal to {}ms",
|
||||
duration.as_millis(),
|
||||
max_duration.as_millis()
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ci-only-tests")]
|
||||
#[tokio::test]
|
||||
async fn execute_queue_doesnt_stall_with_varying_executor_params() {
|
||||
let host = TestHost::new_with_config(|cfg| {
|
||||
cfg.execute_workers_max_num = 2;
|
||||
})
|
||||
.await;
|
||||
let pvd = PersistedValidationData {
|
||||
parent_head: Default::default(),
|
||||
relay_parent_number: 1u32,
|
||||
relay_parent_storage_root: H256::default(),
|
||||
max_pov_size: 4096 * 1024,
|
||||
};
|
||||
let pov = PoV { block_data: BlockData(Vec::new()) };
|
||||
|
||||
let executor_params_1 = ExecutorParams::default();
|
||||
let executor_params_2 = ExecutorParams::from(&[ExecutorParam::StackLogicalMax(1024)][..]);
|
||||
|
||||
// Here we spawn 6 validation jobs for the `halt` PVF and share those between 2 workers. Every
|
||||
// 3rd job will have different set of executor parameters. All the workers should be killed
|
||||
// and in this case the queue should respawn new workers with needed executor environment
|
||||
// without waiting. The jobs will be executed in 3 batches, each running two jobs in parallel,
|
||||
// and execution time would be roughly 3 * TEST_EXECUTION_TIMEOUT
|
||||
let start = std::time::Instant::now();
|
||||
futures::future::join_all((0u8..6).map(|i| {
|
||||
host.validate_candidate(
|
||||
test_teyrchain_halt::wasm_binary_unwrap(),
|
||||
pvd.clone(),
|
||||
pov.clone(),
|
||||
match i % 3 {
|
||||
0 => executor_params_1.clone(),
|
||||
_ => executor_params_2.clone(),
|
||||
},
|
||||
H256::default(),
|
||||
)
|
||||
}))
|
||||
.await;
|
||||
|
||||
let duration = std::time::Instant::now().duration_since(start);
|
||||
let min_duration = 3 * TEST_EXECUTION_TIMEOUT;
|
||||
let max_duration = 4 * TEST_EXECUTION_TIMEOUT;
|
||||
assert!(
|
||||
duration >= min_duration,
|
||||
"Expected duration {}ms to be greater than or equal to {}ms",
|
||||
duration.as_millis(),
|
||||
min_duration.as_millis()
|
||||
);
|
||||
assert!(
|
||||
duration <= max_duration,
|
||||
"Expected duration {}ms to be less than or equal to {}ms",
|
||||
duration.as_millis(),
|
||||
max_duration.as_millis()
|
||||
);
|
||||
}
|
||||
|
||||
// Test that deleting a prepared artifact does not lead to a dispute when we try to execute it.
|
||||
#[tokio::test]
|
||||
async fn deleting_prepared_artifact_does_not_dispute() {
|
||||
let host = TestHost::new().await;
|
||||
let cache_dir = host.cache_dir.path();
|
||||
let pvd = PersistedValidationData {
|
||||
parent_head: Default::default(),
|
||||
relay_parent_number: 1u32,
|
||||
relay_parent_storage_root: H256::default(),
|
||||
max_pov_size: 4096 * 1024,
|
||||
};
|
||||
let pov = PoV { block_data: BlockData(Vec::new()) };
|
||||
|
||||
let _stats = host
|
||||
.precheck_pvf(test_teyrchain_halt::wasm_binary_unwrap(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Manually delete the prepared artifact from disk. The in-memory artifacts table won't change.
|
||||
{
|
||||
// Get the artifact path (asserting it exists).
|
||||
let mut cache_dir: Vec<_> = std::fs::read_dir(cache_dir).unwrap().collect();
|
||||
// Should contain the artifact and the worker dir.
|
||||
assert_eq!(cache_dir.len(), 2);
|
||||
let mut artifact_path = cache_dir.pop().unwrap().unwrap();
|
||||
if artifact_path.path().is_dir() {
|
||||
artifact_path = cache_dir.pop().unwrap().unwrap();
|
||||
}
|
||||
|
||||
// Delete the artifact.
|
||||
std::fs::remove_file(artifact_path.path()).unwrap();
|
||||
}
|
||||
|
||||
// Try to validate, artifact should get recreated.
|
||||
let result = host
|
||||
.validate_candidate(
|
||||
test_teyrchain_halt::wasm_binary_unwrap(),
|
||||
pvd,
|
||||
pov,
|
||||
Default::default(),
|
||||
H256::default(),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_matches!(result, Err(ValidationError::Invalid(InvalidCandidate::HardTimeout)));
|
||||
}
|
||||
|
||||
// Test that corruption of a prepared artifact due to disk issues does not lead to a dispute when we
|
||||
// try to execute it.
|
||||
#[tokio::test]
|
||||
async fn corrupted_on_disk_prepared_artifact_does_not_dispute() {
|
||||
let host = TestHost::new().await;
|
||||
let cache_dir = host.cache_dir.path();
|
||||
let pvd = PersistedValidationData {
|
||||
parent_head: Default::default(),
|
||||
relay_parent_number: 1u32,
|
||||
relay_parent_storage_root: H256::default(),
|
||||
max_pov_size: 4096 * 1024,
|
||||
};
|
||||
let pov = PoV { block_data: BlockData(Vec::new()) };
|
||||
|
||||
let _stats = host
|
||||
.precheck_pvf(test_teyrchain_halt::wasm_binary_unwrap(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Manually corrupting the prepared artifact from disk. The in-memory artifacts table won't
|
||||
// change.
|
||||
let artifact_path = {
|
||||
// Get the artifact path (asserting it exists).
|
||||
let mut cache_dir: Vec<_> = std::fs::read_dir(cache_dir).unwrap().collect();
|
||||
// Should contain the artifact and the worker dir.
|
||||
assert_eq!(cache_dir.len(), 2);
|
||||
let mut artifact_path = cache_dir.pop().unwrap().unwrap();
|
||||
if artifact_path.path().is_dir() {
|
||||
artifact_path = cache_dir.pop().unwrap().unwrap();
|
||||
}
|
||||
|
||||
// Corrupt the artifact.
|
||||
let mut f = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(artifact_path.path())
|
||||
.unwrap();
|
||||
f.write_all(b"corrupted wasm").unwrap();
|
||||
f.flush().unwrap();
|
||||
artifact_path
|
||||
};
|
||||
|
||||
assert!(artifact_path.path().exists());
|
||||
|
||||
// Try to validate, artifact should get removed because of the corruption.
|
||||
let result = host
|
||||
.validate_candidate(
|
||||
test_teyrchain_halt::wasm_binary_unwrap(),
|
||||
pvd,
|
||||
pov,
|
||||
Default::default(),
|
||||
H256::default(),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_matches!(
|
||||
result,
|
||||
Err(ValidationError::PossiblyInvalid(PossiblyInvalidError::CorruptedArtifact))
|
||||
);
|
||||
|
||||
// because of CorruptedArtifact we may retry
|
||||
host.precheck_pvf(test_teyrchain_halt::wasm_binary_unwrap(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// The actual artifact removal is done concurrently
|
||||
// with sending of the result of the execution
|
||||
// it is not a problem for further re-preparation as
|
||||
// artifact filenames are random
|
||||
for _ in 1..5 {
|
||||
if !artifact_path.path().exists() {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
|
||||
assert!(
|
||||
!artifact_path.path().exists(),
|
||||
"the corrupted artifact ({}) should be deleted by the host",
|
||||
artifact_path.path().display()
|
||||
);
|
||||
}
|
||||
|
||||
// Test that corruption of a prepared artifact does not lead to a dispute when we try to execute it.
|
||||
#[tokio::test]
|
||||
async fn corrupted_prepared_artifact_does_not_dispute() {
|
||||
let host = TestHost::new().await;
|
||||
let cache_dir = host.cache_dir.path();
|
||||
let pvd = PersistedValidationData {
|
||||
parent_head: Default::default(),
|
||||
relay_parent_number: 1u32,
|
||||
relay_parent_storage_root: H256::default(),
|
||||
max_pov_size: 4096 * 1024,
|
||||
};
|
||||
let pov = PoV { block_data: BlockData(Vec::new()) };
|
||||
|
||||
let _stats = host
|
||||
.precheck_pvf(test_teyrchain_halt::wasm_binary_unwrap(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Manually corrupting the prepared artifact from disk. The in-memory artifacts table won't
|
||||
// change.
|
||||
let (artifact_path, checksum, new_checksum) = {
|
||||
// Get the artifact path (asserting it exists).
|
||||
let mut cache_dir: Vec<_> = std::fs::read_dir(cache_dir).unwrap().collect();
|
||||
// Should contain the artifact and the worker dir.
|
||||
assert_eq!(cache_dir.len(), 2);
|
||||
let mut artifact_path = cache_dir.pop().unwrap().unwrap();
|
||||
if artifact_path.path().is_dir() {
|
||||
artifact_path = cache_dir.pop().unwrap().unwrap();
|
||||
}
|
||||
|
||||
let checksum =
|
||||
compute_checksum(&std::fs::read(artifact_path.path()).expect("artifact exists"));
|
||||
let new_artifact = b"corrupted wasm";
|
||||
let new_checksum = compute_checksum(new_artifact);
|
||||
|
||||
// Corrupt the artifact.
|
||||
let mut f = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(artifact_path.path())
|
||||
.unwrap();
|
||||
f.write_all(new_artifact).unwrap();
|
||||
f.flush().unwrap();
|
||||
(artifact_path, checksum, new_checksum)
|
||||
};
|
||||
|
||||
assert!(artifact_path.path().exists());
|
||||
|
||||
host.replace_artifact_checksum(checksum, new_checksum).await;
|
||||
|
||||
// Try to validate, artifact should get removed because of the corruption.
|
||||
let result = host
|
||||
.validate_candidate(
|
||||
test_teyrchain_halt::wasm_binary_unwrap(),
|
||||
pvd,
|
||||
pov,
|
||||
Default::default(),
|
||||
H256::default(),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_matches!(
|
||||
result,
|
||||
Err(ValidationError::PossiblyInvalid(PossiblyInvalidError::RuntimeConstruction(_)))
|
||||
);
|
||||
|
||||
// because of RuntimeConstruction we may retry
|
||||
host.precheck_pvf(test_teyrchain_halt::wasm_binary_unwrap(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// The actual artifact removal is done concurrently
|
||||
// with sending of the result of the execution
|
||||
// it is not a problem for further re-preparation as
|
||||
// artifact filenames are random
|
||||
for _ in 1..5 {
|
||||
if !artifact_path.path().exists() {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
|
||||
assert!(
|
||||
!artifact_path.path().exists(),
|
||||
"the corrupted artifact ({}) should be deleted by the host",
|
||||
artifact_path.path().display()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cache_cleared_on_startup() {
|
||||
// Don't drop this host, it owns the `TempDir` which gets cleared on drop.
|
||||
let host = TestHost::new().await;
|
||||
|
||||
let _stats = host
|
||||
.precheck_pvf(test_teyrchain_halt::wasm_binary_unwrap(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// The cache dir should contain one artifact and one worker dir.
|
||||
let cache_dir = host.cache_dir.path().to_owned();
|
||||
assert_eq!(std::fs::read_dir(&cache_dir).unwrap().count(), 2);
|
||||
|
||||
// Start a new host, previous artifact should be cleared.
|
||||
let _host = TestHost::new_with_config(|cfg| {
|
||||
cfg.cache_path = cache_dir.clone();
|
||||
})
|
||||
.await;
|
||||
assert_eq!(std::fs::read_dir(&cache_dir).unwrap().count(), 0);
|
||||
}
|
||||
|
||||
// This test checks if the adder teyrchain runtime can be prepared with 10Mb preparation memory
|
||||
// limit enforced. At the moment of writing, the limit if far enough to prepare the PVF. If it
|
||||
// starts failing, either Wasmtime version has changed, or the PVF code itself has changed, and
|
||||
// more memory is required now. Multi-threaded preparation, if ever enabled, may also affect
|
||||
// memory consumption.
|
||||
#[tokio::test]
|
||||
async fn prechecking_within_memory_limits() {
|
||||
let host = TestHost::new().await;
|
||||
let result = host
|
||||
.precheck_pvf(
|
||||
::test_teyrchain_adder::wasm_binary_unwrap(),
|
||||
ExecutorParams::from(&[ExecutorParam::PrecheckingMaxMemory(10 * 1024 * 1024)][..]),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_matches!(result, Ok(_));
|
||||
}
|
||||
|
||||
// This test checks if the adder teyrchain runtime can be prepared with 512Kb preparation memory
|
||||
// limit enforced. At the moment of writing, the limit if not enough to prepare the PVF, and the
|
||||
// preparation is supposed to generate an error. If the test starts failing, either Wasmtime
|
||||
// version has changed, or the PVF code itself has changed, and less memory is required now.
|
||||
#[tokio::test]
|
||||
async fn prechecking_out_of_memory() {
|
||||
use pezkuwi_node_core_pvf::PrepareError;
|
||||
|
||||
let host = TestHost::new().await;
|
||||
let result = host
|
||||
.precheck_pvf(
|
||||
::test_teyrchain_adder::wasm_binary_unwrap(),
|
||||
ExecutorParams::from(&[ExecutorParam::PrecheckingMaxMemory(512 * 1024)][..]),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_matches!(result, Err(PrepareError::OutOfMemory));
|
||||
}
|
||||
|
||||
// With one worker, run multiple preparation jobs serially. They should not conflict.
|
||||
#[tokio::test]
|
||||
async fn prepare_can_run_serially() {
|
||||
let host = TestHost::new_with_config(|cfg| {
|
||||
cfg.prepare_workers_hard_max_num = 1;
|
||||
})
|
||||
.await;
|
||||
|
||||
let _stats = host
|
||||
.precheck_pvf(::test_teyrchain_adder::wasm_binary_unwrap(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Prepare a different wasm blob to prevent skipping work.
|
||||
let _stats = host
|
||||
.precheck_pvf(test_teyrchain_halt::wasm_binary_unwrap(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// CI machines should be able to enable all the security features.
|
||||
#[cfg(all(feature = "ci-only-tests", target_os = "linux"))]
|
||||
#[tokio::test]
|
||||
async fn all_security_features_work() {
|
||||
let can_enable_landlock = {
|
||||
let res = unsafe { libc::syscall(libc::SYS_landlock_create_ruleset, 0usize, 0usize, 1u32) };
|
||||
if res == -1 {
|
||||
let err = std::io::Error::last_os_error().raw_os_error().unwrap();
|
||||
if err == libc::ENOSYS {
|
||||
false
|
||||
} else {
|
||||
panic!("Unexpected errno from landlock check: {err}");
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
let host = TestHost::new().await;
|
||||
|
||||
assert_eq!(
|
||||
host.security_status().await,
|
||||
SecurityStatus {
|
||||
// Disabled in tests to not enforce the presence of security features. This CI-only test
|
||||
// is the only one that tests them.
|
||||
secure_validator_mode: false,
|
||||
can_enable_landlock,
|
||||
can_enable_seccomp: true,
|
||||
can_unshare_user_namespace_and_change_root: true,
|
||||
can_do_secure_clone: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Regression test to make sure the unshare-pivot-root capability does not depend on the PVF
|
||||
// artifacts cache existing.
|
||||
#[cfg(all(feature = "ci-only-tests", target_os = "linux"))]
|
||||
#[tokio::test]
|
||||
async fn nonexistent_cache_dir() {
|
||||
let host = TestHost::new_with_config(|cfg| {
|
||||
cfg.cache_path = cfg.cache_path.join("nonexistent_cache_dir");
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(host.security_status().await.can_unshare_user_namespace_and_change_root);
|
||||
|
||||
let _stats = host
|
||||
.precheck_pvf(::test_teyrchain_adder::wasm_binary_unwrap(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Checks the the artifact is not re-prepared when the executor environment parameters change
|
||||
// in a way not affecting the preparation
|
||||
#[tokio::test]
|
||||
async fn artifact_does_not_reprepare_on_non_meaningful_exec_parameter_change() {
|
||||
let host = TestHost::new_with_config(|cfg| {
|
||||
cfg.prepare_workers_hard_max_num = 1;
|
||||
})
|
||||
.await;
|
||||
let cache_dir = host.cache_dir.path();
|
||||
|
||||
let set1 = ExecutorParams::default();
|
||||
let set2 = ExecutorParams::from(
|
||||
&[ExecutorParam::PvfExecTimeout(RuntimePvfExecKind::Backing, 2500)][..],
|
||||
);
|
||||
|
||||
let _stats = host
|
||||
.precheck_pvf(test_teyrchain_halt::wasm_binary_unwrap(), set1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let md1 = {
|
||||
let mut cache_dir: Vec<_> = std::fs::read_dir(cache_dir).unwrap().collect();
|
||||
assert_eq!(cache_dir.len(), 2);
|
||||
let mut artifact_path = cache_dir.pop().unwrap().unwrap();
|
||||
if artifact_path.path().is_dir() {
|
||||
artifact_path = cache_dir.pop().unwrap().unwrap();
|
||||
}
|
||||
std::fs::metadata(artifact_path.path()).unwrap()
|
||||
};
|
||||
|
||||
// FS times are not monotonical so we wait 2 secs here to be sure that the creation time of the
|
||||
// second attifact will be different
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
|
||||
let _stats = host
|
||||
.precheck_pvf(test_teyrchain_halt::wasm_binary_unwrap(), set2)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let md2 = {
|
||||
let mut cache_dir: Vec<_> = std::fs::read_dir(cache_dir).unwrap().collect();
|
||||
assert_eq!(cache_dir.len(), 2);
|
||||
let mut artifact_path = cache_dir.pop().unwrap().unwrap();
|
||||
if artifact_path.path().is_dir() {
|
||||
artifact_path = cache_dir.pop().unwrap().unwrap();
|
||||
}
|
||||
std::fs::metadata(artifact_path.path()).unwrap()
|
||||
};
|
||||
|
||||
assert_eq!(md1.created().unwrap(), md2.created().unwrap());
|
||||
}
|
||||
|
||||
// Checks if the artifact is re-prepared if the re-preparation is needed by the nature of
|
||||
// the execution environment parameters change
|
||||
#[tokio::test]
|
||||
async fn artifact_does_reprepare_on_meaningful_exec_parameter_change() {
|
||||
let host = TestHost::new_with_config(|cfg| {
|
||||
cfg.prepare_workers_hard_max_num = 1;
|
||||
})
|
||||
.await;
|
||||
let cache_dir = host.cache_dir.path();
|
||||
|
||||
let set1 = ExecutorParams::default();
|
||||
let set2 = ExecutorParams::from(&[ExecutorParam::MaxMemoryPages(128)][..]);
|
||||
|
||||
let _stats = host
|
||||
.precheck_pvf(test_teyrchain_halt::wasm_binary_unwrap(), set1)
|
||||
.await
|
||||
.unwrap();
|
||||
let cache_dir_contents: Vec<_> = std::fs::read_dir(cache_dir).unwrap().collect();
|
||||
|
||||
assert_eq!(cache_dir_contents.len(), 2);
|
||||
|
||||
let _stats = host
|
||||
.precheck_pvf(test_teyrchain_halt::wasm_binary_unwrap(), set2)
|
||||
.await
|
||||
.unwrap();
|
||||
let cache_dir_contents: Vec<_> = std::fs::read_dir(cache_dir).unwrap().collect();
|
||||
|
||||
assert_eq!(cache_dir_contents.len(), 3); // new artifact has been added
|
||||
}
|
||||
|
||||
// Checks that we cannot prepare oversized compressed code
|
||||
#[tokio::test]
|
||||
async fn invalid_compressed_code_fails_prechecking() {
|
||||
let host = TestHost::new().await;
|
||||
let raw_code = vec![2u8; VALIDATION_CODE_BOMB_LIMIT as usize + 1];
|
||||
let validation_code = sp_maybe_compressed_blob::compress_strongly(
|
||||
&raw_code,
|
||||
VALIDATION_CODE_BOMB_LIMIT as usize + 1,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let res = host.precheck_pvf(&validation_code, Default::default()).await;
|
||||
|
||||
assert_matches!(res, Err(PrepareError::CouldNotDecompressCodeBlob(_)));
|
||||
}
|
||||
|
||||
// Checks that we cannot validate with oversized compressed code
|
||||
#[tokio::test]
|
||||
async fn invalid_compressed_code_fails_validation() {
|
||||
let host = TestHost::new().await;
|
||||
let pvd = PersistedValidationData {
|
||||
parent_head: Default::default(),
|
||||
relay_parent_number: 1u32,
|
||||
relay_parent_storage_root: H256::default(),
|
||||
max_pov_size: 4096 * 1024,
|
||||
};
|
||||
let pov = PoV { block_data: BlockData(Vec::new()) };
|
||||
|
||||
let raw_code = vec![2u8; VALIDATION_CODE_BOMB_LIMIT as usize + 1];
|
||||
let validation_code = sp_maybe_compressed_blob::compress_strongly(
|
||||
&raw_code,
|
||||
VALIDATION_CODE_BOMB_LIMIT as usize + 1,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let result = host
|
||||
.validate_candidate(&validation_code, pvd, pov, Default::default(), H256::default())
|
||||
.await;
|
||||
|
||||
assert_matches!(
|
||||
result,
|
||||
Err(ValidationError::Preparation(PrepareError::CouldNotDecompressCodeBlob(_)))
|
||||
);
|
||||
}
|
||||
|
||||
// Checks that we cannot validate with an oversized PoV
|
||||
#[tokio::test]
|
||||
async fn invalid_compressed_pov_fails_validation() {
|
||||
let host = TestHost::new().await;
|
||||
let pvd = PersistedValidationData {
|
||||
parent_head: Default::default(),
|
||||
relay_parent_number: 1u32,
|
||||
relay_parent_storage_root: H256::default(),
|
||||
max_pov_size: 4096 * 1024,
|
||||
};
|
||||
let raw_block_data = vec![1u8; POV_BOMB_LIMIT + 1];
|
||||
let block_data =
|
||||
sp_maybe_compressed_blob::compress_weakly(&raw_block_data, POV_BOMB_LIMIT + 1).unwrap();
|
||||
let pov = PoV { block_data: BlockData(block_data) };
|
||||
|
||||
let result = host
|
||||
.validate_candidate(
|
||||
test_teyrchain_halt::wasm_binary_unwrap(),
|
||||
pvd,
|
||||
pov,
|
||||
Default::default(),
|
||||
H256::default(),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_matches!(
|
||||
result,
|
||||
Err(ValidationError::Invalid(InvalidCandidate::PoVDecompressionFailure))
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Test unexpected behaviors of the spawned processes. We test both worker processes (directly
|
||||
//! spawned by the host) and job processes (spawned by the workers to securely perform PVF jobs).
|
||||
|
||||
use super::TestHost;
|
||||
use assert_matches::assert_matches;
|
||||
use codec::Encode;
|
||||
use pezkuwi_node_core_pvf::{
|
||||
InvalidCandidate, PossiblyInvalidError, PrepareError, ValidationError,
|
||||
};
|
||||
use pezkuwi_node_primitives::PoV;
|
||||
use pezkuwi_primitives::PersistedValidationData;
|
||||
use pezkuwi_teyrchain_primitives::primitives::{
|
||||
BlockData as GenericBlockData, HeadData as GenericHeadData,
|
||||
};
|
||||
use procfs::process;
|
||||
use rusty_fork::rusty_fork_test;
|
||||
use sp_core::H256;
|
||||
use std::{future::Future, sync::Arc, time::Duration};
|
||||
use test_teyrchain_adder::{hash_state, BlockData, HeadData};
|
||||
|
||||
const PREPARE_PROCESS_NAME: &'static str = "pezkuwi-prepare-worker";
|
||||
const EXECUTE_PROCESS_NAME: &'static str = "pezkuwi-execute-worker";
|
||||
|
||||
const SIGNAL_KILL: i32 = 9;
|
||||
const SIGNAL_STOP: i32 = 19;
|
||||
|
||||
fn send_signal_by_sid_and_name(
|
||||
sid: i32,
|
||||
exe_name: &'static str,
|
||||
is_direct_child: bool,
|
||||
signal: i32,
|
||||
) {
|
||||
let process = find_process_by_sid_and_name(sid, exe_name, is_direct_child)
|
||||
.expect("Should have found the expected process");
|
||||
assert_eq!(unsafe { libc::kill(process.pid(), signal) }, 0);
|
||||
}
|
||||
fn get_num_threads_by_sid_and_name(sid: i32, exe_name: &'static str, is_direct_child: bool) -> i64 {
|
||||
let process = find_process_by_sid_and_name(sid, exe_name, is_direct_child)
|
||||
.expect("Should have found the expected process");
|
||||
process.stat().unwrap().num_threads
|
||||
}
|
||||
|
||||
fn find_process_by_sid_and_name(
|
||||
sid: i32,
|
||||
exe_name: &'static str,
|
||||
is_direct_child: bool,
|
||||
) -> Option<process::Process> {
|
||||
let all_processes: Vec<process::Process> = process::all_processes()
|
||||
.expect("Can't read /proc")
|
||||
.filter_map(|p| match p {
|
||||
Ok(p) => Some(p), // happy path
|
||||
Err(e) => match e {
|
||||
// process vanished during iteration, ignore it
|
||||
procfs::ProcError::NotFound(_) => None,
|
||||
x => {
|
||||
panic!("some unknown error: {}", x);
|
||||
},
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut found = None;
|
||||
for process in all_processes {
|
||||
let Ok(stat) = process.stat() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if stat.session != sid || !process.exe().unwrap().to_str().unwrap().contains(exe_name) {
|
||||
continue;
|
||||
}
|
||||
// The workers are direct children of the current process, the worker job processes are not
|
||||
// (they are children of the workers).
|
||||
let process_is_direct_child = stat.ppid as u32 == std::process::id();
|
||||
if is_direct_child != process_is_direct_child {
|
||||
continue;
|
||||
}
|
||||
|
||||
if found.is_some() {
|
||||
panic!("Found more than one process")
|
||||
}
|
||||
found = Some(process);
|
||||
}
|
||||
found
|
||||
}
|
||||
|
||||
/// Sets up the test.
|
||||
///
|
||||
/// We run the runtime manually because `#[tokio::test]` doesn't work in `rusty_fork_test!`.
|
||||
fn test_wrapper<F, Fut>(f: F)
|
||||
where
|
||||
F: FnOnce(Arc<TestHost>, i32) -> Fut,
|
||||
Fut: Future<Output = ()>,
|
||||
{
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async {
|
||||
let host = Arc::new(TestHost::new().await);
|
||||
|
||||
// Create a new session and get the session ID.
|
||||
let sid = unsafe { libc::setsid() };
|
||||
assert!(sid > 0);
|
||||
|
||||
// Pass a clone of the host so that it does not get dropped after.
|
||||
f(host.clone(), sid).await;
|
||||
});
|
||||
}
|
||||
|
||||
// Run these tests in their own processes with rusty-fork. They work by each creating a new session,
|
||||
// then finding the child process that matches the session ID and expected process name and doing
|
||||
// something with that child.
|
||||
rusty_fork_test! {
|
||||
// Everything succeeds.
|
||||
#[test]
|
||||
fn successful_prepare_and_validate() {
|
||||
test_wrapper(|host, _sid| async move {
|
||||
let parent_head = HeadData { number: 0, parent_hash: [0; 32], post_state: hash_state(0) };
|
||||
let block_data = BlockData { state: 0, add: 512 };
|
||||
let pvd = PersistedValidationData {
|
||||
parent_head: GenericHeadData(parent_head.encode()),
|
||||
relay_parent_number: 1u32,
|
||||
relay_parent_storage_root: H256::default(),
|
||||
max_pov_size: 4096 * 1024,
|
||||
};
|
||||
let pov = PoV { block_data: GenericBlockData(block_data.encode()) };
|
||||
host
|
||||
.validate_candidate(
|
||||
test_teyrchain_adder::wasm_binary_unwrap(),
|
||||
pvd,
|
||||
pov,
|
||||
Default::default(),
|
||||
H256::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
}
|
||||
|
||||
// What happens when the prepare worker (not the job) times out?
|
||||
#[test]
|
||||
fn prepare_worker_timeout() {
|
||||
test_wrapper(|host, sid| async move {
|
||||
let (result, _) = futures::join!(
|
||||
// Choose a job that would normally take the entire timeout.
|
||||
host.precheck_pvf(pezkuwichain_runtime::WASM_BINARY.unwrap(), Default::default()),
|
||||
// Send a stop signal to pause the worker.
|
||||
async {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
send_signal_by_sid_and_name(sid, PREPARE_PROCESS_NAME, true, SIGNAL_STOP);
|
||||
}
|
||||
);
|
||||
|
||||
assert_matches!(result, Err(PrepareError::TimedOut));
|
||||
})
|
||||
}
|
||||
|
||||
// What happens when the execute worker (not the job) times out?
|
||||
#[test]
|
||||
fn execute_worker_timeout() {
|
||||
test_wrapper(|host, sid| async move {
|
||||
// Prepare the artifact ahead of time.
|
||||
let binary = test_teyrchain_halt::wasm_binary_unwrap();
|
||||
host.precheck_pvf(binary, Default::default()).await.unwrap();
|
||||
let pvd = PersistedValidationData {
|
||||
parent_head: GenericHeadData(HeadData::default().encode()),
|
||||
relay_parent_number: 1u32,
|
||||
relay_parent_storage_root: H256::default(),
|
||||
max_pov_size: 4096 * 1024,
|
||||
};
|
||||
let pov = PoV { block_data: GenericBlockData(Vec::new()) };
|
||||
|
||||
let (result, _) = futures::join!(
|
||||
// Choose an job that would normally take the entire timeout.
|
||||
host.validate_candidate(
|
||||
binary,
|
||||
pvd,
|
||||
pov,
|
||||
Default::default(),
|
||||
H256::default(),
|
||||
),
|
||||
// Send a stop signal to pause the worker.
|
||||
async {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
send_signal_by_sid_and_name(sid, EXECUTE_PROCESS_NAME, true, SIGNAL_STOP);
|
||||
}
|
||||
);
|
||||
|
||||
assert_matches!(
|
||||
result,
|
||||
Err(ValidationError::Invalid(InvalidCandidate::HardTimeout))
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
// What happens when the prepare worker dies in the middle of a job?
|
||||
#[test]
|
||||
fn prepare_worker_killed_during_job() {
|
||||
test_wrapper(|host, sid| async move {
|
||||
let (result, _) = futures::join!(
|
||||
// Choose a job that would normally take the entire timeout.
|
||||
host.precheck_pvf(pezkuwichain_runtime::WASM_BINARY.unwrap(), Default::default()),
|
||||
// Run a future that kills the job while it's running.
|
||||
async {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
send_signal_by_sid_and_name(sid, PREPARE_PROCESS_NAME, true, SIGNAL_KILL);
|
||||
}
|
||||
);
|
||||
|
||||
assert_matches!(result, Err(PrepareError::IoErr(_)));
|
||||
})
|
||||
}
|
||||
|
||||
// What happens when the execute worker dies in the middle of a job?
|
||||
#[test]
|
||||
fn execute_worker_killed_during_job() {
|
||||
test_wrapper(|host, sid| async move {
|
||||
// Prepare the artifact ahead of time.
|
||||
let binary = test_teyrchain_halt::wasm_binary_unwrap();
|
||||
host.precheck_pvf(binary, Default::default()).await.unwrap();
|
||||
let pvd = PersistedValidationData {
|
||||
parent_head: GenericHeadData(HeadData::default().encode()),
|
||||
relay_parent_number: 1u32,
|
||||
relay_parent_storage_root: H256::default(),
|
||||
max_pov_size: 4096 * 1024,
|
||||
};
|
||||
let pov = PoV { block_data: GenericBlockData(Vec::new()) };
|
||||
|
||||
let (result, _) = futures::join!(
|
||||
// Choose an job that would normally take the entire timeout.
|
||||
host.validate_candidate(
|
||||
binary,
|
||||
pvd,
|
||||
pov,
|
||||
Default::default(),
|
||||
H256::default(),
|
||||
),
|
||||
// Run a future that kills the job while it's running.
|
||||
async {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
send_signal_by_sid_and_name(sid, EXECUTE_PROCESS_NAME, true, SIGNAL_KILL);
|
||||
}
|
||||
);
|
||||
|
||||
assert_matches!(
|
||||
result,
|
||||
Err(ValidationError::PossiblyInvalid(PossiblyInvalidError::AmbiguousWorkerDeath))
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
// What happens when the forked prepare job dies in the middle of its job?
|
||||
#[test]
|
||||
fn forked_prepare_job_killed_during_job() {
|
||||
test_wrapper(|host, sid| async move {
|
||||
let (result, _) = futures::join!(
|
||||
// Choose a job that would normally take the entire timeout.
|
||||
host.precheck_pvf(pezkuwichain_runtime::WASM_BINARY.unwrap(), Default::default()),
|
||||
// Run a future that kills the job while it's running.
|
||||
async {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
send_signal_by_sid_and_name(sid, PREPARE_PROCESS_NAME, false, SIGNAL_KILL);
|
||||
}
|
||||
);
|
||||
|
||||
// Note that we get a more specific error if the job died than if the whole worker died.
|
||||
assert_matches!(
|
||||
result,
|
||||
Err(PrepareError::JobDied{ err, job_pid: _ }) if err == "received signal: SIGKILL"
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
// What happens when the forked execute job dies in the middle of its job?
|
||||
#[test]
|
||||
fn forked_execute_job_killed_during_job() {
|
||||
test_wrapper(|host, sid| async move {
|
||||
// Prepare the artifact ahead of time.
|
||||
let binary = test_teyrchain_halt::wasm_binary_unwrap();
|
||||
host.precheck_pvf(binary, Default::default()).await.unwrap();
|
||||
let pvd = PersistedValidationData {
|
||||
parent_head: GenericHeadData(HeadData::default().encode()),
|
||||
relay_parent_number: 1u32,
|
||||
relay_parent_storage_root: H256::default(),
|
||||
max_pov_size: 4096 * 1024,
|
||||
};
|
||||
let pov = PoV { block_data: GenericBlockData(Vec::new()) };
|
||||
|
||||
let (result, _) = futures::join!(
|
||||
// Choose a job that would normally take the entire timeout.
|
||||
host.validate_candidate(
|
||||
binary,
|
||||
pvd,
|
||||
pov,
|
||||
Default::default(),
|
||||
H256::default(),
|
||||
),
|
||||
// Run a future that kills the job while it's running.
|
||||
async {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
send_signal_by_sid_and_name(sid, EXECUTE_PROCESS_NAME, false, SIGNAL_KILL);
|
||||
}
|
||||
);
|
||||
|
||||
// Note that we get a more specific error if the job died than if the whole worker died.
|
||||
assert_matches!(
|
||||
result,
|
||||
Err(ValidationError::PossiblyInvalid(PossiblyInvalidError::AmbiguousJobDeath(err)))
|
||||
if err == "received signal: SIGKILL"
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
// Ensure that the spawned prepare worker is single-threaded.
|
||||
//
|
||||
// See `run_worker` for why we need this invariant.
|
||||
#[test]
|
||||
fn ensure_prepare_processes_have_correct_num_threads() {
|
||||
test_wrapper(|host, sid| async move {
|
||||
let _ = futures::join!(
|
||||
// Choose a job that would normally take the entire timeout.
|
||||
host.precheck_pvf(pezkuwichain_runtime::WASM_BINARY.unwrap(), Default::default()),
|
||||
// Run a future that kills the job while it's running.
|
||||
async {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
assert_eq!(
|
||||
get_num_threads_by_sid_and_name(sid, PREPARE_PROCESS_NAME, true),
|
||||
1
|
||||
);
|
||||
// Child job should have three threads: main thread, execute thread, CPU time
|
||||
// monitor, and memory tracking.
|
||||
assert_eq!(
|
||||
get_num_threads_by_sid_and_name(sid, PREPARE_PROCESS_NAME, false),
|
||||
pezkuwi_node_core_pvf_prepare_worker::PREPARE_WORKER_THREAD_NUMBER as i64,
|
||||
);
|
||||
|
||||
// End the test.
|
||||
send_signal_by_sid_and_name(sid, PREPARE_PROCESS_NAME, true, SIGNAL_KILL);
|
||||
}
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
// Ensure that the spawned execute worker is single-threaded.
|
||||
//
|
||||
// See `run_worker` for why we need this invariant.
|
||||
#[test]
|
||||
fn ensure_execute_processes_have_correct_num_threads() {
|
||||
test_wrapper(|host, sid| async move {
|
||||
// Prepare the artifact ahead of time.
|
||||
let binary = test_teyrchain_halt::wasm_binary_unwrap();
|
||||
host.precheck_pvf(binary, Default::default()).await.unwrap();
|
||||
let pvd = PersistedValidationData {
|
||||
parent_head: GenericHeadData(HeadData::default().encode()),
|
||||
relay_parent_number: 1u32,
|
||||
relay_parent_storage_root: H256::default(),
|
||||
max_pov_size: 4096 * 1024,
|
||||
};
|
||||
let pov = PoV { block_data: GenericBlockData(Vec::new()) };
|
||||
|
||||
let _ = futures::join!(
|
||||
// Choose a job that would normally take the entire timeout.
|
||||
host.validate_candidate(
|
||||
binary,
|
||||
pvd,
|
||||
pov,
|
||||
Default::default(),
|
||||
H256::default(),
|
||||
),
|
||||
// Run a future that tests the thread count while the worker is running.
|
||||
async {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
assert_eq!(
|
||||
get_num_threads_by_sid_and_name(sid, EXECUTE_PROCESS_NAME, true),
|
||||
1
|
||||
);
|
||||
// Child job should have three threads: main thread, execute thread, and CPU
|
||||
// time monitor.
|
||||
assert_eq!(
|
||||
get_num_threads_by_sid_and_name(sid, EXECUTE_PROCESS_NAME, false),
|
||||
pezkuwi_node_core_pvf_execute_worker::EXECUTE_WORKER_THREAD_NUMBER as i64,
|
||||
);
|
||||
|
||||
// End the test.
|
||||
send_signal_by_sid_and_name(sid, EXECUTE_PROCESS_NAME, true, SIGNAL_KILL);
|
||||
}
|
||||
);
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezkuwi.
|
||||
|
||||
// Pezkuwi 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.
|
||||
|
||||
// Pezkuwi 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 Pezkuwi. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use pezkuwi_node_core_pvf::{
|
||||
testing::{build_workers_and_get_paths, spawn_with_program_path, SpawnErr},
|
||||
SecurityStatus,
|
||||
};
|
||||
use std::{env, time::Duration};
|
||||
|
||||
// Test spawning a program that immediately exits with a failure code.
|
||||
#[tokio::test]
|
||||
async fn spawn_immediate_exit() {
|
||||
let (prepare_worker_path, _) = build_workers_and_get_paths();
|
||||
|
||||
// There's no explicit `exit` subcommand in the worker; it will panic on an unknown
|
||||
// subcommand anyway
|
||||
let spawn_timeout = Duration::from_secs(2);
|
||||
let result = spawn_with_program_path(
|
||||
"integration-test",
|
||||
prepare_worker_path,
|
||||
&env::temp_dir(),
|
||||
&["exit"],
|
||||
Duration::from_secs(2),
|
||||
SecurityStatus::default(),
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
matches!(result, Err(SpawnErr::AcceptTimeout { spawn_timeout: s }) if s == spawn_timeout)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn spawn_timeout() {
|
||||
let (_, execute_worker_path) = build_workers_and_get_paths();
|
||||
|
||||
let spawn_timeout = Duration::from_secs(2);
|
||||
let result = spawn_with_program_path(
|
||||
"integration-test",
|
||||
execute_worker_path,
|
||||
&env::temp_dir(),
|
||||
&["test-sleep"],
|
||||
spawn_timeout,
|
||||
SecurityStatus::default(),
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
matches!(result, Err(SpawnErr::AcceptTimeout { spawn_timeout: s }) if s == spawn_timeout)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn should_connect() {
|
||||
let (prepare_worker_path, _) = build_workers_and_get_paths();
|
||||
|
||||
let _ = spawn_with_program_path(
|
||||
"integration-test",
|
||||
prepare_worker_path,
|
||||
&env::temp_dir(),
|
||||
&["prepare-worker"],
|
||||
Duration::from_secs(2),
|
||||
SecurityStatus::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
Reference in New Issue
Block a user