// Copyright (C) Parity Technologies (UK) Ltd. // This file is part of Polkadot. // Polkadot is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // Polkadot is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with Polkadot. If not, see . //! Utilities and tests for locating the PVF worker binaries. use super::Error; use is_executable::IsExecutable; use std::path::PathBuf; #[cfg(test)] use std::sync::{Mutex, OnceLock}; /// Override the workers polkadot binary directory path, used for testing. #[cfg(test)] fn workers_exe_path_override() -> &'static Mutex> { static OVERRIDE: OnceLock>> = OnceLock::new(); OVERRIDE.get_or_init(|| Mutex::new(None)) } /// Override the workers lib directory path, used for testing. #[cfg(test)] fn workers_lib_path_override() -> &'static Mutex> { static OVERRIDE: OnceLock>> = OnceLock::new(); OVERRIDE.get_or_init(|| Mutex::new(None)) } /// Determines the final set of paths to use for the PVF workers. /// /// 1. Get the binaries from the workers path if it is passed in, or consider all possible /// locations on the filesystem in order and get all sets of paths at which the binaries exist. /// /// 2. If no paths exist, error out. We can't proceed without workers. /// /// 3. Log a warning if more than one set of paths exists. Continue with the first set of paths. /// /// 4. Check if the returned paths are executable. If not it's evidence of a borked installation /// so error out. /// /// 5. Do the version check, if mismatch error out. /// /// 6. At this point the final set of paths should be good to use. pub fn determine_workers_paths( given_workers_path: Option, workers_names: Option<(String, String)>, node_version: Option, ) -> Result<(PathBuf, PathBuf), Error> { let mut workers_paths = list_workers_paths(given_workers_path.clone(), workers_names.clone())?; if workers_paths.is_empty() { let current_exe_path = get_exe_path()?; return Err(Error::MissingWorkerBinaries { given_workers_path, current_exe_path, workers_names, }) } else if workers_paths.len() > 1 { log::warn!("multiple sets of worker binaries found ({:?})", workers_paths,); } let (prep_worker_path, exec_worker_path) = workers_paths.swap_remove(0); if !prep_worker_path.is_executable() || !exec_worker_path.is_executable() { return Err(Error::InvalidWorkerBinaries { prep_worker_path, exec_worker_path }) } // Do the version check. if let Some(node_version) = node_version { let worker_version = polkadot_node_core_pvf::get_worker_version(&prep_worker_path)?; if worker_version != node_version { return Err(Error::WorkerBinaryVersionMismatch { worker_version, node_version, worker_path: prep_worker_path, }) } let worker_version = polkadot_node_core_pvf::get_worker_version(&exec_worker_path)?; if worker_version != node_version { return Err(Error::WorkerBinaryVersionMismatch { worker_version, node_version, worker_path: exec_worker_path, }) } } else { log::warn!("Skipping node/worker version checks. This could result in incorrect behavior in PVF workers."); } Ok((prep_worker_path, exec_worker_path)) } /// Get list of workers paths by considering the passed-in `given_workers_path` option, or possible /// locations on the filesystem. See `new_full`. fn list_workers_paths( given_workers_path: Option, workers_names: Option<(String, String)>, ) -> Result, Error> { if let Some(path) = given_workers_path { log::trace!("Using explicitly provided workers path {:?}", path); if path.is_executable() { return Ok(vec![(path.clone(), path)]) } let (prep_worker, exec_worker) = build_worker_paths(path, workers_names); // Check if both workers exist. Otherwise return an empty vector which results in an error. return if prep_worker.exists() && exec_worker.exists() { Ok(vec![(prep_worker, exec_worker)]) } else { Ok(vec![]) } } // Workers path not provided, check all possible valid locations. let mut workers_paths = vec![]; // Consider the polkadot binary directory. { let exe_path = get_exe_path()?; let (prep_worker, exec_worker) = build_worker_paths(exe_path.clone(), workers_names.clone()); // Add to set if both workers exist. Warn on partial installs. let (prep_worker_exists, exec_worker_exists) = (prep_worker.exists(), exec_worker.exists()); if prep_worker_exists && exec_worker_exists { log::trace!("Worker binaries found at current exe path: {:?}", exe_path); workers_paths.push((prep_worker, exec_worker)); } else if prep_worker_exists { log::warn!("Worker binary found at {:?} but not {:?}", prep_worker, exec_worker); } else if exec_worker_exists { log::warn!("Worker binary found at {:?} but not {:?}", exec_worker, prep_worker); } } // Consider the /usr/lib/polkadot/ directory. { #[allow(unused_mut)] let mut lib_path = PathBuf::from("/usr/lib/polkadot"); #[cfg(test)] if let Some(ref path_override) = *workers_lib_path_override().lock().unwrap() { lib_path = path_override.clone(); } let (prep_worker, exec_worker) = build_worker_paths(lib_path, workers_names); // Add to set if both workers exist. Warn on partial installs. let (prep_worker_exists, exec_worker_exists) = (prep_worker.exists(), exec_worker.exists()); if prep_worker_exists && exec_worker_exists { log::trace!("Worker binaries found at /usr/lib/polkadot"); workers_paths.push((prep_worker, exec_worker)); } else if prep_worker_exists { log::warn!("Worker binary found at {:?} but not {:?}", prep_worker, exec_worker); } else if exec_worker_exists { log::warn!("Worker binary found at {:?} but not {:?}", exec_worker, prep_worker); } } Ok(workers_paths) } fn get_exe_path() -> Result { let mut exe_path = std::env::current_exe()?; let _ = exe_path.pop(); // executable file will always have a parent directory. #[cfg(test)] if let Some(ref path_override) = *workers_exe_path_override().lock().unwrap() { exe_path = path_override.clone(); } Ok(exe_path) } fn build_worker_paths( worker_dir: PathBuf, workers_names: Option<(String, String)>, ) -> (PathBuf, PathBuf) { let (prep_worker_name, exec_worker_name) = workers_names.unwrap_or(( polkadot_node_core_pvf::PREPARE_BINARY_NAME.to_string(), polkadot_node_core_pvf::EXECUTE_BINARY_NAME.to_string(), )); let mut prep_worker = worker_dir.clone(); prep_worker.push(prep_worker_name); let mut exec_worker = worker_dir; exec_worker.push(exec_worker_name); (prep_worker, exec_worker) } // Tests that set up a temporary directory tree according to what scenario we want to test and // run worker detection. #[cfg(test)] mod tests { use super::*; use assert_matches::assert_matches; use serial_test::serial; use std::{env::temp_dir, fs, os::unix::fs::PermissionsExt, path::Path}; const TEST_NODE_VERSION: &'static str = "v0.1.2"; /// Write a dummy executable to the path which satisfies the version check. fn write_worker_exe(path: impl AsRef) -> Result<(), Box> { let program = get_program(TEST_NODE_VERSION); fs::write(&path, program)?; Ok(fs::set_permissions(&path, fs::Permissions::from_mode(0o744))?) } fn write_worker_exe_invalid_version( path: impl AsRef, version: &str, ) -> Result<(), Box> { let program = get_program(version); fs::write(&path, program)?; Ok(fs::set_permissions(&path, fs::Permissions::from_mode(0o744))?) } fn get_program(version: &str) -> String { format!( "#!/bin/bash if [[ $# -ne 1 ]] ; then echo \"unexpected number of arguments: $#\" exit 1 fi if [[ \"$1\" != \"--version\" ]] ; then echo \"unexpected argument: $1\" exit 1 fi echo {} ", version ) } /// Sets up an empty temp dir structure where the workers can be put by tests. Uses the temp dir /// to override the standard locations where the node searches for the workers. fn with_temp_dir_structure( f: impl FnOnce(PathBuf, PathBuf) -> Result<(), Box>, ) -> Result<(), Box> { // Set up /usr/lib/polkadot and /usr/bin, both empty. let tempdir = temp_dir(); let lib_path = tempdir.join("usr/lib/polkadot"); let _ = fs::remove_dir_all(&lib_path); fs::create_dir_all(&lib_path)?; *workers_lib_path_override().lock()? = Some(lib_path); let exe_path = tempdir.join("usr/bin"); let _ = fs::remove_dir_all(&exe_path); fs::create_dir_all(&exe_path)?; *workers_exe_path_override().lock()? = Some(exe_path.clone()); // Set up custom path at /usr/local/bin. let custom_path = tempdir.join("usr/local/bin"); let _ = fs::remove_dir_all(&custom_path); fs::create_dir_all(&custom_path)?; f(tempdir, exe_path) } #[test] #[serial] fn test_given_worker_path() { with_temp_dir_structure(|tempdir, exe_path| { let given_workers_path = tempdir.join("usr/local/bin"); // Try with provided workers path that has missing binaries. assert_matches!( determine_workers_paths(Some(given_workers_path.clone()), None, Some(TEST_NODE_VERSION.into())), Err(Error::MissingWorkerBinaries { given_workers_path: Some(p1), current_exe_path: p2, workers_names: None }) if p1 == given_workers_path && p2 == exe_path ); // Try with provided workers path that has non-executable binaries. let prepare_worker_path = given_workers_path.join("polkadot-prepare-worker"); write_worker_exe(&prepare_worker_path)?; fs::set_permissions(&prepare_worker_path, fs::Permissions::from_mode(0o644))?; let execute_worker_path = given_workers_path.join("polkadot-execute-worker"); write_worker_exe(&execute_worker_path)?; fs::set_permissions(&execute_worker_path, fs::Permissions::from_mode(0o644))?; assert_matches!( determine_workers_paths(Some(given_workers_path.clone()), None, Some(TEST_NODE_VERSION.into())), Err(Error::InvalidWorkerBinaries { prep_worker_path: p1, exec_worker_path: p2 }) if p1 == prepare_worker_path && p2 == execute_worker_path ); // Try with valid workers directory path that has executable binaries. fs::set_permissions(&prepare_worker_path, fs::Permissions::from_mode(0o744))?; fs::set_permissions(&execute_worker_path, fs::Permissions::from_mode(0o744))?; assert_matches!( determine_workers_paths(Some(given_workers_path), None, Some(TEST_NODE_VERSION.into())), Ok((p1, p2)) if p1 == prepare_worker_path && p2 == execute_worker_path ); // Try with valid provided workers path that is a binary file. let given_workers_path = tempdir.join("usr/local/bin/test-worker"); write_worker_exe(&given_workers_path)?; assert_matches!( determine_workers_paths(Some(given_workers_path.clone()), None, Some(TEST_NODE_VERSION.into())), Ok((p1, p2)) if p1 == given_workers_path && p2 == given_workers_path ); Ok(()) }) .unwrap(); } #[test] #[serial] fn missing_workers_paths_throws_error() { with_temp_dir_structure(|tempdir, exe_path| { // Try with both binaries missing. assert_matches!( determine_workers_paths(None, None, Some(TEST_NODE_VERSION.into())), Err(Error::MissingWorkerBinaries { given_workers_path: None, current_exe_path: p, workers_names: None }) if p == exe_path ); // Try with only prep worker (at bin location). let prepare_worker_path = tempdir.join("usr/bin/polkadot-prepare-worker"); write_worker_exe(&prepare_worker_path)?; assert_matches!( determine_workers_paths(None, None, Some(TEST_NODE_VERSION.into())), Err(Error::MissingWorkerBinaries { given_workers_path: None, current_exe_path: p, workers_names: None }) if p == exe_path ); // Try with only exec worker (at bin location). fs::remove_file(&prepare_worker_path)?; let execute_worker_path = tempdir.join("usr/bin/polkadot-execute-worker"); write_worker_exe(&execute_worker_path)?; assert_matches!( determine_workers_paths(None, None, Some(TEST_NODE_VERSION.into())), Err(Error::MissingWorkerBinaries { given_workers_path: None, current_exe_path: p, workers_names: None }) if p == exe_path ); // Try with only prep worker (at lib location). fs::remove_file(&execute_worker_path)?; let prepare_worker_path = tempdir.join("usr/lib/polkadot/polkadot-prepare-worker"); write_worker_exe(&prepare_worker_path)?; assert_matches!( determine_workers_paths(None, None, Some(TEST_NODE_VERSION.into())), Err(Error::MissingWorkerBinaries { given_workers_path: None, current_exe_path: p, workers_names: None }) if p == exe_path ); // Try with only exec worker (at lib location). fs::remove_file(&prepare_worker_path)?; let execute_worker_path = tempdir.join("usr/lib/polkadot/polkadot-execute-worker"); write_worker_exe(execute_worker_path)?; assert_matches!( determine_workers_paths(None, None, Some(TEST_NODE_VERSION.into())), Err(Error::MissingWorkerBinaries { given_workers_path: None, current_exe_path: p, workers_names: None }) if p == exe_path ); Ok(()) }) .unwrap() } #[test] #[serial] fn should_find_workers_at_all_locations() { with_temp_dir_structure(|tempdir, _| { let prepare_worker_bin_path = tempdir.join("usr/bin/polkadot-prepare-worker"); write_worker_exe(&prepare_worker_bin_path)?; let execute_worker_bin_path = tempdir.join("usr/bin/polkadot-execute-worker"); write_worker_exe(&execute_worker_bin_path)?; let prepare_worker_lib_path = tempdir.join("usr/lib/polkadot/polkadot-prepare-worker"); write_worker_exe(&prepare_worker_lib_path)?; let execute_worker_lib_path = tempdir.join("usr/lib/polkadot/polkadot-execute-worker"); write_worker_exe(&execute_worker_lib_path)?; assert_matches!( list_workers_paths(None, None), Ok(v) if v == vec![(prepare_worker_bin_path, execute_worker_bin_path), (prepare_worker_lib_path, execute_worker_lib_path)] ); Ok(()) }) .unwrap(); } #[test] #[serial] fn should_find_workers_with_custom_names_at_all_locations() { with_temp_dir_structure(|tempdir, _| { let (prep_worker_name, exec_worker_name) = ("test-prepare", "test-execute"); let prepare_worker_bin_path = tempdir.join("usr/bin").join(prep_worker_name); write_worker_exe(&prepare_worker_bin_path)?; let execute_worker_bin_path = tempdir.join("usr/bin").join(exec_worker_name); write_worker_exe(&execute_worker_bin_path)?; let prepare_worker_lib_path = tempdir.join("usr/lib/polkadot").join(prep_worker_name); write_worker_exe(&prepare_worker_lib_path)?; let execute_worker_lib_path = tempdir.join("usr/lib/polkadot").join(exec_worker_name); write_worker_exe(&execute_worker_lib_path)?; assert_matches!( list_workers_paths(None, Some((prep_worker_name.into(), exec_worker_name.into()))), Ok(v) if v == vec![(prepare_worker_bin_path, execute_worker_bin_path), (prepare_worker_lib_path, execute_worker_lib_path)] ); Ok(()) }) .unwrap(); } #[test] #[serial] fn workers_version_mismatch_throws_error() { let bad_version = "v9.9.9.9"; with_temp_dir_structure(|tempdir, _| { // Workers at bin location return bad version. let prepare_worker_bin_path = tempdir.join("usr/bin/polkadot-prepare-worker"); let execute_worker_bin_path = tempdir.join("usr/bin/polkadot-execute-worker"); write_worker_exe_invalid_version(&prepare_worker_bin_path, bad_version)?; write_worker_exe(&execute_worker_bin_path)?; assert_matches!( determine_workers_paths(None, None, Some(TEST_NODE_VERSION.into())), Err(Error::WorkerBinaryVersionMismatch { worker_version: v1, node_version: v2, worker_path: p }) if v1 == bad_version && v2 == TEST_NODE_VERSION && p == prepare_worker_bin_path ); // Workers at lib location return bad version. fs::remove_file(prepare_worker_bin_path)?; fs::remove_file(execute_worker_bin_path)?; let prepare_worker_lib_path = tempdir.join("usr/lib/polkadot/polkadot-prepare-worker"); let execute_worker_lib_path = tempdir.join("usr/lib/polkadot/polkadot-execute-worker"); write_worker_exe(&prepare_worker_lib_path)?; write_worker_exe_invalid_version(&execute_worker_lib_path, bad_version)?; assert_matches!( determine_workers_paths(None, None, Some(TEST_NODE_VERSION.into())), Err(Error::WorkerBinaryVersionMismatch { worker_version: v1, node_version: v2, worker_path: p }) if v1 == bad_version && v2 == TEST_NODE_VERSION && p == execute_worker_lib_path ); // Workers at provided workers location return bad version. let given_workers_path = tempdir.join("usr/local/bin"); let prepare_worker_path = given_workers_path.join("polkadot-prepare-worker"); let execute_worker_path = given_workers_path.join("polkadot-execute-worker"); write_worker_exe_invalid_version(&prepare_worker_path, bad_version)?; write_worker_exe_invalid_version(&execute_worker_path, bad_version)?; assert_matches!( determine_workers_paths(Some(given_workers_path), None, Some(TEST_NODE_VERSION.into())), Err(Error::WorkerBinaryVersionMismatch { worker_version: v1, node_version: v2, worker_path: p }) if v1 == bad_version && v2 == TEST_NODE_VERSION && p == prepare_worker_path ); // Given worker binary returns bad version. let given_workers_path = tempdir.join("usr/local/bin/test-worker"); write_worker_exe_invalid_version(&given_workers_path, bad_version)?; assert_matches!( determine_workers_paths(Some(given_workers_path.clone()), None, Some(TEST_NODE_VERSION.into())), Err(Error::WorkerBinaryVersionMismatch { worker_version: v1, node_version: v2, worker_path: p }) if v1 == bad_version && v2 == TEST_NODE_VERSION && p == given_workers_path ); Ok(()) }) .unwrap(); } #[test] #[serial] fn should_find_valid_workers() { // Test bin location. with_temp_dir_structure(|tempdir, _| { let prepare_worker_bin_path = tempdir.join("usr/bin/polkadot-prepare-worker"); write_worker_exe(&prepare_worker_bin_path)?; let execute_worker_bin_path = tempdir.join("usr/bin/polkadot-execute-worker"); write_worker_exe(&execute_worker_bin_path)?; assert_matches!( determine_workers_paths(None, None, Some(TEST_NODE_VERSION.into())), Ok((p1, p2)) if p1 == prepare_worker_bin_path && p2 == execute_worker_bin_path ); Ok(()) }) .unwrap(); // Test lib location. with_temp_dir_structure(|tempdir, _| { let prepare_worker_lib_path = tempdir.join("usr/lib/polkadot/polkadot-prepare-worker"); write_worker_exe(&prepare_worker_lib_path)?; let execute_worker_lib_path = tempdir.join("usr/lib/polkadot/polkadot-execute-worker"); write_worker_exe(&execute_worker_lib_path)?; assert_matches!( determine_workers_paths(None, None, Some(TEST_NODE_VERSION.into())), Ok((p1, p2)) if p1 == prepare_worker_lib_path && p2 == execute_worker_lib_path ); Ok(()) }) .unwrap(); } }