diff --git a/polkadot/node/core/candidate-validation/src/lib.rs b/polkadot/node/core/candidate-validation/src/lib.rs index 5c4e449b2c90..09b396d40e6e 100644 --- a/polkadot/node/core/candidate-validation/src/lib.rs +++ b/polkadot/node/core/candidate-validation/src/lib.rs @@ -90,6 +90,8 @@ pub struct Config { pub node_version: Option, /// Whether the node is attempting to run as a secure validator. pub secure_validator_mode: bool, + /// The architecture of the current host. + pub architecture: String, /// Path to the preparation worker binary pub prep_worker_path: PathBuf, /// Path to the execution worker binary @@ -139,6 +141,7 @@ async fn run( artifacts_cache_path, node_version, secure_validator_mode, + architecture, prep_worker_path, exec_worker_path, }: Config, @@ -148,6 +151,7 @@ async fn run( artifacts_cache_path, node_version, secure_validator_mode, + architecture.clone(), prep_worker_path, exec_worker_path, ), @@ -157,6 +161,7 @@ async fn run( ctx.spawn_blocking("pvf-validation-host", task.boxed())?; loop { + let architecture = architecture.clone(); match ctx.recv().await? { FromOrchestra::Signal(OverseerSignal::ActiveLeaves(_)) => {}, FromOrchestra::Signal(OverseerSignal::BlockFinalized(..)) => {}, @@ -184,6 +189,7 @@ async fn run( pov, executor_params, exec_kind, + architecture, &metrics, ) .await; @@ -219,6 +225,7 @@ async fn run( pov, executor_params, exec_kind, + architecture, &metrics, ) .await; @@ -246,6 +253,7 @@ async fn run( validation_host, relay_parent, validation_code_hash, + architecture, ) .await; @@ -319,6 +327,7 @@ async fn precheck_pvf( mut validation_backend: impl ValidationBackend, relay_parent: Hash, validation_code_hash: ValidationCodeHash, + architecture: String, ) -> PreCheckOutcome where Sender: SubsystemSender, @@ -366,11 +375,12 @@ where &validation_code.0, VALIDATION_CODE_BOMB_LIMIT, ) { - Ok(code) => PvfPrepData::from_code( + Ok(code) => PvfPrepData::new( code.into_owned(), executor_params, timeout, PrepareJobKind::Prechecking, + architecture, ), Err(e) => { gum::debug!(target: LOG_TARGET, err=?e, "precheck: cannot decompress validation code"); @@ -505,6 +515,7 @@ async fn validate_from_chain_state( pov: Arc, executor_params: ExecutorParams, exec_kind: PvfExecKind, + architecture: String, metrics: &Metrics, ) -> Result where @@ -525,6 +536,7 @@ where pov, executor_params, exec_kind, + architecture, metrics, ) .await; @@ -561,6 +573,7 @@ async fn validate_candidate_exhaustive( pov: Arc, executor_params: ExecutorParams, exec_kind: PvfExecKind, + architecture: String, metrics: &Metrics, ) -> Result { let _timer = metrics.time_validate_candidate_exhaustive(); @@ -625,11 +638,12 @@ async fn validate_candidate_exhaustive( PvfExecKind::Backing => { let prep_timeout = pvf_prep_timeout(&executor_params, PvfPrepKind::Prepare); let exec_timeout = pvf_exec_timeout(&executor_params, exec_kind); - let pvf = PvfPrepData::from_code( + let pvf = PvfPrepData::new( raw_validation_code.to_vec(), executor_params, prep_timeout, PrepareJobKind::Compilation, + architecture, ); validation_backend.validate_candidate(pvf, exec_timeout, params.encode()).await @@ -641,6 +655,7 @@ async fn validate_candidate_exhaustive( pvf_exec_timeout(&executor_params, exec_kind), params, executor_params, + architecture, PVF_APPROVAL_EXECUTION_RETRY_DELAY, ) .await, @@ -735,15 +750,17 @@ trait ValidationBackend { exec_timeout: Duration, params: ValidationParams, executor_params: ExecutorParams, + architecture: String, retry_delay: Duration, ) -> Result { let prep_timeout = pvf_prep_timeout(&executor_params, PvfPrepKind::Prepare); // Construct the PVF a single time, since it is an expensive operation. Cloning it is cheap. - let pvf = PvfPrepData::from_code( + let pvf = PvfPrepData::new( raw_validation_code, executor_params, prep_timeout, PrepareJobKind::Compilation, + architecture, ); // We keep track of the total time that has passed and stop retrying if we are taking too // long. diff --git a/polkadot/node/core/pvf/common/src/lib.rs b/polkadot/node/core/pvf/common/src/lib.rs index abebd06f71a4..5c52c584e38c 100644 --- a/polkadot/node/core/pvf/common/src/lib.rs +++ b/polkadot/node/core/pvf/common/src/lib.rs @@ -86,3 +86,17 @@ pub fn framed_recv_blocking(r: &mut (impl Read + Unpin)) -> io::Result> r.read_exact(&mut buf)?; Ok(buf) } + +/// Calls `uname` to get the host architecture. Example output: "Linux-x86_64". +#[cfg(feature = "test-utils")] +pub fn test_get_host_architecture() -> String { + let uname = std::process::Command::new("uname") + .arg("-ms") + .output() + .expect("uname should not fail in tests") + .stdout; + std::str::from_utf8(&uname) + .expect("uname output is printed as an ASCII string; qed") + .trim() + .replace(" ", "-") +} diff --git a/polkadot/node/core/pvf/common/src/pvf.rs b/polkadot/node/core/pvf/common/src/pvf.rs index 2d8f6430187b..914320d93ec9 100644 --- a/polkadot/node/core/pvf/common/src/pvf.rs +++ b/polkadot/node/core/pvf/common/src/pvf.rs @@ -42,20 +42,23 @@ pub struct PvfPrepData { prep_timeout: Duration, /// The kind of preparation job. prep_kind: PrepareJobKind, + /// The host architecture. + architecture: String, } impl PvfPrepData { /// Returns an instance of the PVF out of the given PVF code and executor params. - pub fn from_code( + pub fn new( code: Vec, executor_params: ExecutorParams, prep_timeout: Duration, prep_kind: PrepareJobKind, + architecture: String, ) -> Self { let code = Arc::new(code); let code_hash = blake2_256(&code).into(); let executor_params = Arc::new(executor_params); - Self { code, code_hash, executor_params, prep_timeout, prep_kind } + Self { code, code_hash, executor_params, prep_timeout, prep_kind, architecture } } /// Returns validation code hash for the PVF @@ -83,15 +86,21 @@ impl PvfPrepData { self.prep_kind } + /// Returns host architecture. + pub fn architecture(&self) -> &str { + &self.architecture + } + /// Creates a structure for tests. #[cfg(feature = "test-utils")] pub fn from_discriminator_and_timeout(num: u32, timeout: Duration) -> Self { let descriminator_buf = num.to_le_bytes().to_vec(); - Self::from_code( + Self::new( descriminator_buf, ExecutorParams::default(), timeout, PrepareJobKind::Compilation, + crate::test_get_host_architecture(), ) } diff --git a/polkadot/node/core/pvf/src/artifacts.rs b/polkadot/node/core/pvf/src/artifacts.rs index 17ce5b443e33..f613f1eb5e5a 100644 --- a/polkadot/node/core/pvf/src/artifacts.rs +++ b/polkadot/node/core/pvf/src/artifacts.rs @@ -57,6 +57,7 @@ use crate::{host::PrecheckResultSender, LOG_TARGET}; use always_assert::always; +use helpers::Architecture; use polkadot_core_primitives::Hash; use polkadot_node_core_pvf_common::{ error::PrepareError, prepare::PrepareStats, pvf::PvfPrepData, RUNTIME_VERSION, @@ -84,26 +85,32 @@ fn artifact_prefix() -> String { pub struct ArtifactId { pub(crate) code_hash: ValidationCodeHash, pub(crate) executor_params_hash: ExecutorParamsHash, + pub(crate) architecture: Architecture, } impl ArtifactId { /// Creates a new artifact ID with the given hash. - pub fn new(code_hash: ValidationCodeHash, executor_params_hash: ExecutorParamsHash) -> Self { - Self { code_hash, executor_params_hash } + pub fn new( + code_hash: ValidationCodeHash, + executor_params_hash: ExecutorParamsHash, + architecture: Architecture, + ) -> Self { + Self { code_hash, executor_params_hash, architecture } } /// Returns an artifact ID that corresponds to the PVF with given executor params. pub fn from_pvf_prep_data(pvf: &PvfPrepData) -> Self { - Self::new(pvf.code_hash(), pvf.executor_params().hash()) + Self::new(pvf.code_hash(), pvf.executor_params().hash(), pvf.architecture().into()) } /// Returns the canonical path to the concluded artifact. pub(crate) fn path(&self, cache_path: &Path, checksum: &str) -> PathBuf { let file_name = format!( - "{}_{:#x}_{:#x}_0x{}", + "{}_{:#x}_{:#x}_{}_0x{}", artifact_prefix(), self.code_hash, self.executor_params_hash, + self.architecture, checksum ); cache_path.join(file_name) @@ -111,16 +118,16 @@ impl ArtifactId { /// Tries to recover the artifact id from the given file name. /// Return `None` if the given file name is invalid. - /// VALID_NAME := _ _ _ + /// VALID_NAME := _ _ _ _ fn from_file_name(file_name: &str) -> Option { let file_name = file_name.strip_prefix(&artifact_prefix())?.strip_prefix('_')?; let parts: Vec<&str> = file_name.split('_').collect(); - if let [code_hash, param_hash, _checksum] = parts[..] { + if let [code_hash, param_hash, architecture, _checksum] = parts[..] { let code_hash = Hash::from_str(code_hash).ok()?.into(); let executor_params_hash = ExecutorParamsHash::from_hash(Hash::from_str(param_hash).ok()?); - return Some(Self { code_hash, executor_params_hash }) + return Some(Self { code_hash, executor_params_hash, architecture: architecture.into() }) } None @@ -185,12 +192,16 @@ pub enum ArtifactState { /// A container of all known artifact ids and their states. pub struct Artifacts { inner: HashMap, + architecture: Architecture, } impl Artifacts { #[cfg(test)] pub(crate) fn empty() -> Self { - Self { inner: HashMap::new() } + Self { + inner: HashMap::new(), + architecture: polkadot_node_core_pvf_common::test_get_host_architecture().into(), + } } #[cfg(test)] @@ -203,8 +214,9 @@ impl Artifacts { /// valid, e.g., matching the current node version. The ones deemed invalid will be pruned. /// /// Create the cache directory on-disk if it doesn't exist. - pub async fn new_and_prune(cache_path: &Path) -> Self { - let mut artifacts = Self { inner: HashMap::new() }; + pub async fn new_and_prune(cache_path: &Path, architecture: &str) -> Self { + let architecture: Architecture = architecture.replace("_", "-").into(); + let mut artifacts = Self { inner: HashMap::new(), architecture }; let _ = artifacts.insert_and_prune(cache_path).await.map_err(|err| { gum::error!( target: LOG_TARGET, @@ -215,7 +227,7 @@ impl Artifacts { } async fn insert_and_prune(&mut self, cache_path: &Path) -> Result<(), String> { - async fn is_corrupted(path: &Path) -> bool { + async fn checksum_matches(path: &Path) -> bool { let checksum = match tokio::fs::read(path).await { Ok(bytes) => blake3::hash(&bytes), Err(err) => { @@ -226,16 +238,16 @@ impl Artifacts { "unable to read artifact {:?} when checking integrity, removing...", path, ); - return true + return false }, }; if let Some(file_name) = path.file_name() { if let Some(file_name) = file_name.to_str() { - return !file_name.ends_with(checksum.to_hex().as_str()) + return file_name.ends_with(checksum.to_hex().as_str()) } } - true + false } // Insert the entry into the artifacts table if it is valid. @@ -260,12 +272,17 @@ impl Artifacts { let id = ArtifactId::from_file_name(file_name); let path = cache_path.join(file_name); - if id.is_none() || is_corrupted(&path).await { + if id.is_none() || !checksum_matches(&path).await { let _ = tokio::fs::remove_file(&path).await; return Err(format!("invalid artifact {path:?}, file deleted")) } - let id = id.expect("checked is_none() above; qed"); + let architecture = &artifacts.architecture; + if &id.architecture != architecture { + let _ = tokio::fs::remove_file(&path).await; + return Err(format!("artifact at {path:?} compiled with different architecture (current architecture is {architecture}), file deleted")) + } + gum::debug!( target: LOG_TARGET, "reusing existing {:?} for node version v{}", @@ -374,13 +391,46 @@ impl Artifacts { } } +mod helpers { + use std::fmt; + + /// A helper type for the host architecture. It manipulates the architecture string so that it + /// is guaranteed not to contain characters such as "_" that could cause problems with parsing + /// artifact filenames. + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct Architecture(String); + + impl Architecture { + #[cfg(test)] + pub fn inner(&self) -> &str { + &self.0 + } + } + + impl From for Architecture + where + S: AsRef, + { + fn from(s: S) -> Self { + Self(s.as_ref().replace("_", "-").replace(" ", "-")) + } + } + + impl fmt::Display for Architecture { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } + } +} + #[cfg(test)] mod tests { - use super::{artifact_prefix as prefix, ArtifactId, Artifacts, NODE_VERSION, RUNTIME_VERSION}; - use polkadot_primitives::ExecutorParamsHash; + use super::*; + use rand::Rng; use sp_core::H256; use std::{ + ffi::OsString, fs, io::Write, path::{Path, PathBuf}, @@ -393,8 +443,38 @@ mod tests { (0..len).map(|_| hex[rng.gen_range(0..hex.len())]).collect() } - fn file_name(code_hash: &str, param_hash: &str, checksum: &str) -> String { - format!("{}_0x{}_0x{}_0x{}", prefix(), code_hash, param_hash, checksum) + fn file_name_without_checksum( + prefix: &str, + code_hash: impl AsRef, + params_hash: impl AsRef, + architecture: &Architecture, + ) -> String { + format!( + "{}_0x{}_0x{}_{}", + prefix, + code_hash.as_ref(), + params_hash.as_ref(), + architecture.inner(), + ) + } + + fn append_checksum(mut file_name: OsString, checksum: &str) -> OsString { + file_name.push("_0x"); + file_name.push(checksum); + file_name + } + + fn file_name( + code_hash: &str, + params_hash: &str, + architecture: &Architecture, + checksum: &str, + ) -> String { + let without_checksum = + file_name_without_checksum(&artifact_prefix(), code_hash, params_hash, architecture); + append_checksum(without_checksum.into(), checksum) + .into_string() + .expect("the filename is valid UTF8") } fn create_artifact( @@ -402,22 +482,12 @@ mod tests { prefix: &str, code_hash: impl AsRef, params_hash: impl AsRef, + architecture: &Architecture, ) -> (PathBuf, String) { - fn artifact_path_without_checksum( - dir: impl AsRef, - prefix: &str, - code_hash: impl AsRef, - params_hash: impl AsRef, - ) -> PathBuf { - let mut path = dir.as_ref().to_path_buf(); - let file_name = - format!("{}_0x{}_0x{}", prefix, code_hash.as_ref(), params_hash.as_ref(),); - path.push(file_name); - path - } - let (code_hash, params_hash) = (code_hash.as_ref(), params_hash.as_ref()); - let path = artifact_path_without_checksum(dir, prefix, code_hash, params_hash); + let file_name = file_name_without_checksum(prefix, code_hash, params_hash, architecture); + let mut path = dir.as_ref().to_path_buf(); + path.push(file_name); let mut file = fs::File::create(&path).unwrap(); let content = format!("{}{}", code_hash, params_hash).into_bytes(); @@ -427,31 +497,32 @@ mod tests { (path, checksum) } - fn create_rand_artifact(dir: impl AsRef, prefix: &str) -> (PathBuf, String) { - create_artifact(dir, prefix, rand_hash(64), rand_hash(64)) + fn create_rand_artifact( + dir: impl AsRef, + prefix: &str, + architecture: &Architecture, + ) -> (PathBuf, String) { + create_artifact(dir, prefix, rand_hash(64), rand_hash(64), architecture) } fn concluded_path(path: impl AsRef, checksum: &str) -> PathBuf { let path = path.as_ref(); - let mut file_name = path.file_name().unwrap().to_os_string(); - file_name.push("_0x"); - file_name.push(checksum); + let without_checksum = path.file_name().unwrap().to_os_string(); + let file_name = append_checksum(without_checksum, checksum); path.with_file_name(file_name) } - #[test] - fn artifact_prefix() { - assert_eq!(prefix(), format!("wasmtime_v{}_polkadot_v{}", RUNTIME_VERSION, NODE_VERSION)); - } - #[test] fn from_file_name() { assert!(ArtifactId::from_file_name("").is_none()); assert!(ArtifactId::from_file_name("junk").is_none()); + let architecture = Architecture::from("Linux-x86_64"); + let file_name = file_name( "0022800000000000000000000000000000000000000000000000000000000000", "0033900000000000000000000000000000000000000000000000000000000000", + &architecture, "00000000000000000000000000000000", ); @@ -465,6 +536,7 @@ mod tests { ExecutorParamsHash::from_hash(sp_core::H256(hex_literal::hex![ "0033900000000000000000000000000000000000000000000000000000000000" ])), + architecture, )), ); } @@ -474,13 +546,18 @@ mod tests { let dir = Path::new("/test"); let code_hash = "1234567890123456789012345678901234567890123456789012345678901234"; let params_hash = "4321098765432109876543210987654321098765432109876543210987654321"; + let architecture = Architecture::from("Darwin-arm64"); let checksum = "34567890123456789012345678901234"; - let file_name = file_name(code_hash, params_hash, checksum); + let file_name = file_name(code_hash, params_hash, &architecture, checksum); let code_hash = H256::from_str(code_hash).unwrap(); let params_hash = H256::from_str(params_hash).unwrap(); - let path = ArtifactId::new(code_hash.into(), ExecutorParamsHash::from_hash(params_hash)) - .path(dir, checksum); + let path = ArtifactId::new( + code_hash.into(), + ExecutorParamsHash::from_hash(params_hash), + architecture, + ) + .path(dir, checksum); assert_eq!(path.to_str().unwrap(), format!("/test/{}", file_name)); } @@ -488,35 +565,43 @@ mod tests { #[tokio::test] async fn remove_stale_cache_on_startup() { let cache_dir = tempfile::Builder::new().prefix("test-cache-").tempdir().unwrap(); + let valid_architecture = Architecture::from("Linux-x86_64"); + let valid_prefix = artifact_prefix(); // invalid prefix - create_rand_artifact(&cache_dir, ""); - create_rand_artifact(&cache_dir, "wasmtime_polkadot_v"); - create_rand_artifact(&cache_dir, "wasmtime_v8.0.0_polkadot_v1.0.0"); - - let prefix = prefix(); + create_rand_artifact(&cache_dir, "", &valid_architecture); + create_rand_artifact(&cache_dir, "wasmtime_polkadot_v", &valid_architecture); + create_rand_artifact(&cache_dir, "wasmtime_v8.0.0_polkadot_v1.0.0", &valid_architecture); // no checksum - create_rand_artifact(&cache_dir, &prefix); + create_rand_artifact(&cache_dir, &valid_prefix, &valid_architecture); // invalid hashes - let (path, checksum) = create_artifact(&cache_dir, &prefix, "000", "000001"); + let (path, checksum) = + create_artifact(&cache_dir, &valid_prefix, "000", "000001", &valid_architecture); + let new_path = concluded_path(&path, &checksum); + fs::rename(&path, &new_path).unwrap(); + + // architecture mismatch + let (path, checksum) = + create_rand_artifact(&cache_dir, &valid_prefix, &("Qubes-mips".into())); let new_path = concluded_path(&path, &checksum); fs::rename(&path, &new_path).unwrap(); // checksum tampered - let (path, checksum) = create_rand_artifact(&cache_dir, &prefix); + let (path, checksum) = create_rand_artifact(&cache_dir, &valid_prefix, &valid_architecture); let new_path = concluded_path(&path, checksum.chars().rev().collect::().as_str()); fs::rename(&path, &new_path).unwrap(); // valid - let (path, checksum) = create_rand_artifact(&cache_dir, &prefix); + let (path, checksum) = create_rand_artifact(&cache_dir, &valid_prefix, &valid_architecture); let new_path = concluded_path(&path, &checksum); fs::rename(&path, &new_path).unwrap(); - assert_eq!(fs::read_dir(&cache_dir).unwrap().count(), 7); + assert_eq!(fs::read_dir(&cache_dir).unwrap().count(), 8); - let artifacts = Artifacts::new_and_prune(cache_dir.path()).await; + let artifacts = + Artifacts::new_and_prune(cache_dir.path(), valid_architecture.inner()).await; assert_eq!(fs::read_dir(&cache_dir).unwrap().count(), 1); assert_eq!(artifacts.len(), 1); diff --git a/polkadot/node/core/pvf/src/host.rs b/polkadot/node/core/pvf/src/host.rs index d17a4d918e00..0035dabf2f45 100644 --- a/polkadot/node/core/pvf/src/host.rs +++ b/polkadot/node/core/pvf/src/host.rs @@ -158,6 +158,8 @@ pub struct Config { pub node_version: Option, /// Whether the node is attempting to run as a secure validator. pub secure_validator_mode: bool, + /// The architecture of the current host. + pub architecture: String, /// The path to the program that can be used to spawn the prepare workers. pub prepare_worker_program_path: PathBuf, @@ -183,6 +185,7 @@ impl Config { cache_path: PathBuf, node_version: Option, secure_validator_mode: bool, + architecture: String, prepare_worker_program_path: PathBuf, execute_worker_program_path: PathBuf, ) -> Self { @@ -190,6 +193,7 @@ impl Config { cache_path, node_version, secure_validator_mode, + architecture, prepare_worker_program_path, prepare_worker_spawn_timeout: Duration::from_secs(3), @@ -218,7 +222,7 @@ pub async fn start( gum::debug!(target: LOG_TARGET, ?config, "starting PVF validation host"); // Make sure the cache is initialized before doing anything else. - let artifacts = Artifacts::new_and_prune(&config.cache_path).await; + let artifacts = Artifacts::new_and_prune(&config.cache_path, &config.architecture).await; // Run checks for supported security features once per host startup. If some checks fail, warn // if Secure Validator Mode is disabled and return an error otherwise. diff --git a/polkadot/node/core/pvf/tests/it/main.rs b/polkadot/node/core/pvf/tests/it/main.rs index 09f975b706d2..46f3267db18f 100644 --- a/polkadot/node/core/pvf/tests/it/main.rs +++ b/polkadot/node/core/pvf/tests/it/main.rs @@ -24,6 +24,7 @@ use polkadot_node_core_pvf::{ start, testing::build_workers_and_get_paths, Config, InvalidCandidate, Metrics, PrepareError, PrepareJobKind, PvfPrepData, ValidationError, ValidationHost, JOB_TIMEOUT_WALL_CLOCK_FACTOR, }; +use polkadot_node_core_pvf_common::test_get_host_architecture; use polkadot_parachain_primitives::primitives::{BlockData, ValidationParams, ValidationResult}; use polkadot_primitives::{ExecutorParam, ExecutorParams}; @@ -39,9 +40,10 @@ 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. + // Keep a reference to the tempdir because otherwise it gets deleted on drop. cache_dir: tempfile::TempDir, host: Mutex, + architecture: String, } impl TestHost { @@ -56,17 +58,21 @@ impl TestHost { let (prepare_worker_path, execute_worker_path) = build_workers_and_get_paths(); let cache_dir = tempfile::tempdir().unwrap(); + let architecture = test_get_host_architecture(); let mut config = Config::new( cache_dir.path().to_owned(), None, false, + architecture.clone(), prepare_worker_path, execute_worker_path, ); f(&mut config); + let (host, task) = start(config, Metrics::default()).await.unwrap(); let _ = tokio::task::spawn(task); - Self { cache_dir, host: Mutex::new(host) } + + Self { cache_dir, host: Mutex::new(host), architecture } } async fn precheck_pvf( @@ -83,11 +89,12 @@ impl TestHost { .lock() .await .precheck_pvf( - PvfPrepData::from_code( + PvfPrepData::new( code.into(), executor_params, TEST_PREPARATION_TIMEOUT, PrepareJobKind::Prechecking, + self.architecture.clone(), ), result_tx, ) @@ -111,11 +118,12 @@ impl TestHost { .lock() .await .execute_pvf( - PvfPrepData::from_code( + PvfPrepData::new( code.into(), executor_params, TEST_PREPARATION_TIMEOUT, PrepareJobKind::Compilation, + self.architecture.clone(), ), TEST_EXECUTION_TIMEOUT, params.encode(), diff --git a/polkadot/node/service/src/lib.rs b/polkadot/node/service/src/lib.rs index ff70dbde7349..e4b293752331 100644 --- a/polkadot/node/service/src/lib.rs +++ b/polkadot/node/service/src/lib.rs @@ -234,6 +234,12 @@ pub enum Error { #[error("Expected at least one of polkadot, kusama, westend or rococo runtime feature")] NoRuntime, + #[error("Could not get the system name. This is required for validator nodes as we need to know how to prepare/execute PVFs")] + UnknownSysname, + + #[error("Could not get the system architecture. This is required for validator nodes as we need to know how to prepare/execute PVFs")] + UnknownArchitecture, + #[cfg(feature = "full-node")] #[error("Worker binaries not executable, prepare binary: {prep_worker_path:?}, execute binary: {exec_worker_path:?}")] InvalidWorkerBinaries { prep_worker_path: PathBuf, exec_worker_path: PathBuf }, @@ -949,6 +955,13 @@ pub fn new_full( log::info!("🚀 Using prepare-worker binary at: {:?}", prep_worker_path); log::info!("🚀 Using execute-worker binary at: {:?}", exec_worker_path); + let sysinfo = sc_sysinfo::gather_sysinfo(); + let architecture = format!( + "{}-{}", + sysinfo.sysname.ok_or(Error::UnknownSysname)?, + sysinfo.architecture.ok_or(Error::UnknownArchitecture)?, + ); + Some(CandidateValidationConfig { artifacts_cache_path: config .database @@ -957,6 +970,7 @@ pub fn new_full( .join("pvf-artifacts"), node_version, secure_validator_mode, + architecture, prep_worker_path, exec_worker_path, }) diff --git a/substrate/client/sysinfo/src/sysinfo.rs b/substrate/client/sysinfo/src/sysinfo.rs index bef87a83e46f..802fa456c64b 100644 --- a/substrate/client/sysinfo/src/sysinfo.rs +++ b/substrate/client/sysinfo/src/sysinfo.rs @@ -302,11 +302,38 @@ pub fn gather_sysinfo() -> SysInfo { linux_kernel: None, linux_distro: None, is_virtual_machine: None, + sysname: None, + architecture: None, }; #[cfg(target_os = "linux")] crate::sysinfo_linux::gather_linux_sysinfo(&mut sysinfo); + // NOTE: We don't use the `nix` crate to call this since it doesn't + // currently check for errors. + #[cfg(unix)] + unsafe { + // SAFETY: The `utsname` is full of byte arrays, so this is safe. + let mut uname: libc::utsname = std::mem::zeroed(); + if libc::uname(&mut uname) < 0 { + log::warn!("uname failed: {}", std::io::Error::last_os_error()); + } else { + let length = + uname.sysname.iter().position(|&byte| byte == 0).unwrap_or(uname.sysname.len()); + let sysname = std::slice::from_raw_parts(uname.sysname.as_ptr().cast(), length); + if let Ok(sysname) = std::str::from_utf8(sysname) { + sysinfo.sysname = Some(sysname.into()); + } + + let length = + uname.machine.iter().position(|&byte| byte == 0).unwrap_or(uname.machine.len()); + let machine = std::slice::from_raw_parts(uname.machine.as_ptr().cast(), length); + if let Ok(machine) = std::str::from_utf8(machine) { + sysinfo.architecture = Some(machine.into()); + } + } + } + sysinfo } @@ -715,6 +742,22 @@ mod tests { use super::*; use sp_runtime::assert_eq_error_rate_float; + #[cfg(unix)] + #[test] + fn test_gather_sysinfo() { + let sysinfo = gather_sysinfo(); + + #[cfg(target_arch = "x86_64")] + assert_eq!(sysinfo.architecture, Some("x86_64".into())); + #[cfg(target_arch = "aarch64")] + assert_eq!(sysinfo.architecture, Some("arm64".into())); + + #[cfg(target_os = "linux")] + assert_eq!(sysinfo.sysname, Some("Linux".into())); + #[cfg(target_os = "macos")] + assert_eq!(sysinfo.sysname, Some("Darwin".into())); + } + #[cfg(target_os = "linux")] #[test] fn test_gather_sysinfo_linux() { diff --git a/substrate/client/telemetry/src/lib.rs b/substrate/client/telemetry/src/lib.rs index 113d8303a20f..3ccc93f2f891 100644 --- a/substrate/client/telemetry/src/lib.rs +++ b/substrate/client/telemetry/src/lib.rs @@ -125,7 +125,7 @@ pub struct ConnectionMessage { /// so most of the fields here are optional. #[derive(Debug, Serialize)] pub struct SysInfo { - /// The exact CPU model. + /// The exact CPU model. Example: "Intel(R) Xeon(TM) CPU 2.40GHz" pub cpu: Option, /// The total amount of memory, in bytes. pub memory: Option, @@ -137,6 +137,10 @@ pub struct SysInfo { pub linux_distro: Option, /// Whether the node's running under a virtual machine. pub is_virtual_machine: Option, + /// The system name. Examples: "Darwin", "Linux" + pub sysname: Option, + /// The CPU architecture. Examples: "x86_64", "arm64" + pub architecture: Option, } /// Telemetry worker.