feat: initialize Kurdistan SDK - independent fork of Polkadot SDK

This commit is contained in:
2025-12-13 15:44:15 +03:00
commit e4778b4576
6838 changed files with 1847450 additions and 0 deletions
+205
View File
@@ -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;
}
+861
View File
@@ -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))
);
}
+403
View File
@@ -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();
}