referrerpolicy=no-referrer-when-downgrade

polkadot_service/
workers.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// This file is part of Polkadot.
3
4// Polkadot is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Polkadot is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
16
17//! Utilities and tests for locating the PVF worker binaries.
18
19use super::Error;
20use is_executable::IsExecutable;
21use std::path::PathBuf;
22
23#[cfg(test)]
24thread_local! {
25	static TMP_DIR: std::cell::RefCell<Option<tempfile::TempDir>> = std::cell::RefCell::new(None);
26}
27
28/// Override the workers polkadot binary directory path, used for testing.
29#[cfg(test)]
30fn workers_exe_path_override() -> Option<PathBuf> {
31	TMP_DIR.with_borrow(|t| t.as_ref().map(|t| t.path().join("usr/bin")))
32}
33
34/// Override the workers lib directory path, used for testing.
35#[cfg(test)]
36fn workers_lib_path_override() -> Option<PathBuf> {
37	TMP_DIR.with_borrow(|t| t.as_ref().map(|t| t.path().join("usr/lib/polkadot")))
38}
39
40/// Determines the final set of paths to use for the PVF workers.
41///
42/// 1. Get the binaries from the workers path if it is passed in, or consider all possible
43/// locations on the filesystem in order and get all sets of paths at which the binaries exist.
44///
45/// 2. If no paths exist, error out. We can't proceed without workers.
46///
47/// 3. Log a warning if more than one set of paths exists. Continue with the first set of paths.
48///
49/// 4. Check if the returned paths are executable. If not it's evidence of a borked installation
50/// so error out.
51///
52/// 5. Do the version check, if mismatch error out.
53///
54/// 6. At this point the final set of paths should be good to use.
55pub fn determine_workers_paths(
56	given_workers_path: Option<PathBuf>,
57	workers_names: Option<(String, String)>,
58	node_version: Option<String>,
59) -> Result<(PathBuf, PathBuf), Error> {
60	let mut workers_paths = list_workers_paths(given_workers_path.clone(), workers_names.clone())?;
61	if workers_paths.is_empty() {
62		let current_exe_path = get_exe_path()?;
63		return Err(Error::MissingWorkerBinaries {
64			given_workers_path,
65			current_exe_path,
66			workers_names,
67		})
68	} else if workers_paths.len() > 1 {
69		log::warn!("multiple sets of worker binaries found ({:?})", workers_paths,);
70	}
71
72	let (prep_worker_path, exec_worker_path) = workers_paths.swap_remove(0);
73	if !prep_worker_path.is_executable() || !exec_worker_path.is_executable() {
74		return Err(Error::InvalidWorkerBinaries { prep_worker_path, exec_worker_path })
75	}
76
77	// Do the version check.
78	if let Some(node_version) = node_version {
79		let worker_version = polkadot_node_core_pvf::get_worker_version(&prep_worker_path)?;
80		if worker_version != node_version {
81			return Err(Error::WorkerBinaryVersionMismatch {
82				worker_version,
83				node_version,
84				worker_path: prep_worker_path,
85			})
86		}
87
88		let worker_version = polkadot_node_core_pvf::get_worker_version(&exec_worker_path)?;
89		if worker_version != node_version {
90			return Err(Error::WorkerBinaryVersionMismatch {
91				worker_version,
92				node_version,
93				worker_path: exec_worker_path,
94			})
95		}
96	} else {
97		log::warn!("Skipping node/worker version checks. This could result in incorrect behavior in PVF workers.");
98	}
99
100	Ok((prep_worker_path, exec_worker_path))
101}
102
103/// Get list of workers paths by considering the passed-in `given_workers_path` option, or possible
104/// locations on the filesystem. See `new_full`.
105fn list_workers_paths(
106	given_workers_path: Option<PathBuf>,
107	workers_names: Option<(String, String)>,
108) -> Result<Vec<(PathBuf, PathBuf)>, Error> {
109	if let Some(path) = given_workers_path {
110		log::trace!("Using explicitly provided workers path {:?}", path);
111
112		if path.is_executable() {
113			return Ok(vec![(path.clone(), path)])
114		}
115
116		let (prep_worker, exec_worker) = build_worker_paths(path, workers_names);
117
118		// Check if both workers exist. Otherwise return an empty vector which results in an error.
119		return if prep_worker.exists() && exec_worker.exists() {
120			Ok(vec![(prep_worker, exec_worker)])
121		} else {
122			Ok(vec![])
123		}
124	}
125
126	// Workers path not provided, check all possible valid locations.
127
128	let mut workers_paths = vec![];
129
130	// Consider the polkadot binary directory.
131	{
132		let exe_path = get_exe_path()?;
133
134		let (prep_worker, exec_worker) =
135			build_worker_paths(exe_path.clone(), workers_names.clone());
136
137		// Add to set if both workers exist. Warn on partial installs.
138		let (prep_worker_exists, exec_worker_exists) = (prep_worker.exists(), exec_worker.exists());
139		if prep_worker_exists && exec_worker_exists {
140			log::trace!("Worker binaries found at current exe path: {:?}", exe_path);
141			workers_paths.push((prep_worker, exec_worker));
142		} else if prep_worker_exists {
143			log::warn!("Worker binary found at {:?} but not {:?}", prep_worker, exec_worker);
144		} else if exec_worker_exists {
145			log::warn!("Worker binary found at {:?} but not {:?}", exec_worker, prep_worker);
146		}
147	}
148
149	// Consider the /usr/lib/polkadot/ directory.
150	{
151		let lib_path = PathBuf::from("/usr/lib/polkadot");
152		#[cfg(test)]
153		let lib_path = if let Some(o) = workers_lib_path_override() { o } else { lib_path };
154
155		let (prep_worker, exec_worker) = build_worker_paths(lib_path, workers_names);
156
157		// Add to set if both workers exist. Warn on partial installs.
158		let (prep_worker_exists, exec_worker_exists) = (prep_worker.exists(), exec_worker.exists());
159		if prep_worker_exists && exec_worker_exists {
160			log::trace!("Worker binaries found at /usr/lib/polkadot");
161			workers_paths.push((prep_worker, exec_worker));
162		} else if prep_worker_exists {
163			log::warn!("Worker binary found at {:?} but not {:?}", prep_worker, exec_worker);
164		} else if exec_worker_exists {
165			log::warn!("Worker binary found at {:?} but not {:?}", exec_worker, prep_worker);
166		}
167	}
168
169	Ok(workers_paths)
170}
171
172fn get_exe_path() -> Result<PathBuf, Error> {
173	let mut exe_path = std::env::current_exe()?;
174	let _ = exe_path.pop(); // executable file will always have a parent directory.
175	#[cfg(test)]
176	if let Some(o) = workers_exe_path_override() {
177		exe_path = o;
178	}
179
180	Ok(exe_path)
181}
182
183fn build_worker_paths(
184	worker_dir: PathBuf,
185	workers_names: Option<(String, String)>,
186) -> (PathBuf, PathBuf) {
187	let (prep_worker_name, exec_worker_name) = workers_names.unwrap_or((
188		polkadot_node_core_pvf::PREPARE_BINARY_NAME.to_string(),
189		polkadot_node_core_pvf::EXECUTE_BINARY_NAME.to_string(),
190	));
191
192	let mut prep_worker = worker_dir.clone();
193	prep_worker.push(prep_worker_name);
194	let mut exec_worker = worker_dir;
195	exec_worker.push(exec_worker_name);
196
197	(prep_worker, exec_worker)
198}
199
200// Tests that set up a temporary directory tree according to what scenario we want to test and
201// run worker detection.
202#[cfg(test)]
203mod tests {
204	use super::*;
205
206	use assert_matches::assert_matches;
207	use std::{fs, os::unix::fs::PermissionsExt, path::Path};
208
209	const TEST_NODE_VERSION: &'static str = "v0.1.2";
210
211	/// Write a dummy executable to the path which satisfies the version check.
212	fn write_worker_exe(path: impl AsRef<Path>) -> Result<(), Box<dyn std::error::Error>> {
213		let program = get_program(TEST_NODE_VERSION);
214		fs::write(&path, program)?;
215		Ok(fs::set_permissions(&path, fs::Permissions::from_mode(0o744))?)
216	}
217
218	fn write_worker_exe_invalid_version(
219		path: impl AsRef<Path>,
220		version: &str,
221	) -> Result<(), Box<dyn std::error::Error>> {
222		let program = get_program(version);
223		fs::write(&path, program)?;
224		Ok(fs::set_permissions(&path, fs::Permissions::from_mode(0o744))?)
225	}
226
227	fn get_program(version: &str) -> String {
228		format!(
229			"#!/usr/bin/env bash
230
231if [[ $# -ne 1 ]] ; then
232    echo \"unexpected number of arguments: $#\"
233    exit 1
234fi
235
236if [[ \"$1\" != \"--version\" ]] ; then
237    echo \"unexpected argument: $1\"
238    exit 1
239fi
240
241echo {}
242",
243			version
244		)
245	}
246
247	/// Sets up an empty temp dir structure where the workers can be put by tests. Uses the temp dir
248	/// to override the standard locations where the node searches for the workers.
249	fn with_temp_dir_structure(
250		f: impl FnOnce(PathBuf, PathBuf) -> Result<(), Box<dyn std::error::Error>>,
251	) -> Result<(), Box<dyn std::error::Error>> {
252		// Set up <tmp>/usr/lib/polkadot and <tmp>/usr/bin, both empty.
253
254		let tempdir = tempfile::tempdir().unwrap();
255		let tmp_dir = tempdir.path().to_path_buf();
256		TMP_DIR.with_borrow_mut(|t| *t = Some(tempdir));
257
258		fs::create_dir_all(workers_lib_path_override().unwrap()).unwrap();
259		fs::create_dir_all(workers_exe_path_override().unwrap()).unwrap();
260
261		let custom_path = tmp_dir.join("usr/local/bin");
262		// Set up custom path at <tmp>/usr/local/bin.
263		fs::create_dir_all(&custom_path).unwrap();
264
265		f(tmp_dir, workers_exe_path_override().unwrap())
266	}
267
268	#[test]
269	fn test_given_worker_path() {
270		with_temp_dir_structure(|tempdir, exe_path| {
271			let given_workers_path = tempdir.join("usr/local/bin");
272
273			// Try with provided workers path that has missing binaries.
274			assert_matches!(
275				determine_workers_paths(Some(given_workers_path.clone()), None, Some(TEST_NODE_VERSION.into())),
276				Err(Error::MissingWorkerBinaries { given_workers_path: Some(p1), current_exe_path: p2, workers_names: None }) if p1 == given_workers_path && p2 == exe_path
277			);
278
279			// Try with provided workers path that has non-executable binaries.
280			let prepare_worker_path = given_workers_path.join("polkadot-prepare-worker");
281			write_worker_exe(&prepare_worker_path)?;
282			fs::set_permissions(&prepare_worker_path, fs::Permissions::from_mode(0o644))?;
283			let execute_worker_path = given_workers_path.join("polkadot-execute-worker");
284			write_worker_exe(&execute_worker_path)?;
285			fs::set_permissions(&execute_worker_path, fs::Permissions::from_mode(0o644))?;
286			assert_matches!(
287				determine_workers_paths(Some(given_workers_path.clone()), None, Some(TEST_NODE_VERSION.into())),
288				Err(Error::InvalidWorkerBinaries { prep_worker_path: p1, exec_worker_path: p2 }) if p1 == prepare_worker_path && p2 == execute_worker_path
289			);
290
291			// Try with valid workers directory path that has executable binaries.
292			fs::set_permissions(&prepare_worker_path, fs::Permissions::from_mode(0o744))?;
293			fs::set_permissions(&execute_worker_path, fs::Permissions::from_mode(0o744))?;
294			assert_matches!(
295				determine_workers_paths(Some(given_workers_path), None, Some(TEST_NODE_VERSION.into())),
296				Ok((p1, p2)) if p1 == prepare_worker_path && p2 == execute_worker_path
297			);
298
299			// Try with valid provided workers path that is a binary file.
300			let given_workers_path = tempdir.join("usr/local/bin/test-worker");
301			write_worker_exe(&given_workers_path)?;
302			assert_matches!(
303				determine_workers_paths(Some(given_workers_path.clone()), None, Some(TEST_NODE_VERSION.into())),
304				Ok((p1, p2)) if p1 == given_workers_path && p2 == given_workers_path
305			);
306
307			Ok(())
308		})
309		.unwrap();
310	}
311
312	#[test]
313	fn missing_workers_paths_throws_error() {
314		with_temp_dir_structure(|tempdir, exe_path| {
315			// Try with both binaries missing.
316			assert_matches!(
317				determine_workers_paths(None, None, Some(TEST_NODE_VERSION.into())),
318				Err(Error::MissingWorkerBinaries { given_workers_path: None, current_exe_path: p, workers_names: None }) if p == exe_path
319			);
320
321			// Try with only prep worker (at bin location).
322			let prepare_worker_path = tempdir.join("usr/bin/polkadot-prepare-worker");
323			write_worker_exe(&prepare_worker_path)?;
324			assert_matches!(
325				determine_workers_paths(None, None, Some(TEST_NODE_VERSION.into())),
326				Err(Error::MissingWorkerBinaries { given_workers_path: None, current_exe_path: p, workers_names: None }) if p == exe_path
327			);
328
329			// Try with only exec worker (at bin location).
330			fs::remove_file(&prepare_worker_path)?;
331			let execute_worker_path = tempdir.join("usr/bin/polkadot-execute-worker");
332			write_worker_exe(&execute_worker_path)?;
333			assert_matches!(
334				determine_workers_paths(None, None, Some(TEST_NODE_VERSION.into())),
335				Err(Error::MissingWorkerBinaries { given_workers_path: None, current_exe_path: p, workers_names: None }) if p == exe_path
336			);
337
338			// Try with only prep worker (at lib location).
339			fs::remove_file(&execute_worker_path)?;
340			let prepare_worker_path = tempdir.join("usr/lib/polkadot/polkadot-prepare-worker");
341			write_worker_exe(&prepare_worker_path)?;
342			assert_matches!(
343				determine_workers_paths(None, None, Some(TEST_NODE_VERSION.into())),
344				Err(Error::MissingWorkerBinaries { given_workers_path: None, current_exe_path: p, workers_names: None }) if p == exe_path
345			);
346
347			// Try with only exec worker (at lib location).
348			fs::remove_file(&prepare_worker_path)?;
349			let execute_worker_path = tempdir.join("usr/lib/polkadot/polkadot-execute-worker");
350			write_worker_exe(execute_worker_path)?;
351			assert_matches!(
352				determine_workers_paths(None, None, Some(TEST_NODE_VERSION.into())),
353				Err(Error::MissingWorkerBinaries { given_workers_path: None, current_exe_path: p, workers_names: None }) if p == exe_path
354			);
355
356			Ok(())
357		})
358		.unwrap()
359	}
360
361	#[test]
362	fn should_find_workers_at_all_locations() {
363		with_temp_dir_structure(|tempdir, _| {
364				let prepare_worker_bin_path = tempdir.join("usr/bin/polkadot-prepare-worker");
365				write_worker_exe(&prepare_worker_bin_path)?;
366
367				let execute_worker_bin_path = tempdir.join("usr/bin/polkadot-execute-worker");
368				write_worker_exe(&execute_worker_bin_path)?;
369
370				let prepare_worker_lib_path = tempdir.join("usr/lib/polkadot/polkadot-prepare-worker");
371				write_worker_exe(&prepare_worker_lib_path)?;
372
373				let execute_worker_lib_path = tempdir.join("usr/lib/polkadot/polkadot-execute-worker");
374				write_worker_exe(&execute_worker_lib_path)?;
375
376				assert_matches!(
377					list_workers_paths(None, None),
378					Ok(v) if v == vec![(prepare_worker_bin_path, execute_worker_bin_path), (prepare_worker_lib_path, execute_worker_lib_path)]
379				);
380
381				Ok(())
382			})
383			.unwrap();
384	}
385
386	#[test]
387	fn should_find_workers_with_custom_names_at_all_locations() {
388		with_temp_dir_structure(|tempdir, _| {
389			let (prep_worker_name, exec_worker_name) = ("test-prepare", "test-execute");
390
391			let prepare_worker_bin_path = tempdir.join("usr/bin").join(prep_worker_name);
392			write_worker_exe(&prepare_worker_bin_path)?;
393
394			let execute_worker_bin_path = tempdir.join("usr/bin").join(exec_worker_name);
395			write_worker_exe(&execute_worker_bin_path)?;
396
397			let prepare_worker_lib_path = tempdir.join("usr/lib/polkadot").join(prep_worker_name);
398			write_worker_exe(&prepare_worker_lib_path)?;
399
400			let execute_worker_lib_path = tempdir.join("usr/lib/polkadot").join(exec_worker_name);
401			write_worker_exe(&execute_worker_lib_path)?;
402
403			assert_matches!(
404				list_workers_paths(None, Some((prep_worker_name.into(), exec_worker_name.into()))),
405				Ok(v) if v == vec![(prepare_worker_bin_path, execute_worker_bin_path), (prepare_worker_lib_path, execute_worker_lib_path)]
406			);
407
408			Ok(())
409		})
410			.unwrap();
411	}
412
413	#[test]
414	fn workers_version_mismatch_throws_error() {
415		let bad_version = "v9.9.9.9";
416
417		with_temp_dir_structure(|tempdir, _| {
418			// Workers at bin location return bad version.
419			let prepare_worker_bin_path = tempdir.join("usr/bin/polkadot-prepare-worker");
420			let execute_worker_bin_path = tempdir.join("usr/bin/polkadot-execute-worker");
421			write_worker_exe_invalid_version(&prepare_worker_bin_path, bad_version)?;
422			write_worker_exe(&execute_worker_bin_path)?;
423			assert_matches!(
424				determine_workers_paths(None, None, Some(TEST_NODE_VERSION.into())),
425				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
426			);
427
428			// Workers at lib location return bad version.
429			fs::remove_file(prepare_worker_bin_path)?;
430			fs::remove_file(execute_worker_bin_path)?;
431			let prepare_worker_lib_path = tempdir.join("usr/lib/polkadot/polkadot-prepare-worker");
432			let execute_worker_lib_path = tempdir.join("usr/lib/polkadot/polkadot-execute-worker");
433			write_worker_exe(&prepare_worker_lib_path)?;
434			write_worker_exe_invalid_version(&execute_worker_lib_path, bad_version)?;
435			assert_matches!(
436				determine_workers_paths(None, None, Some(TEST_NODE_VERSION.into())),
437				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
438			);
439
440			// Workers at provided workers location return bad version.
441			let given_workers_path = tempdir.join("usr/local/bin");
442			let prepare_worker_path = given_workers_path.join("polkadot-prepare-worker");
443			let execute_worker_path = given_workers_path.join("polkadot-execute-worker");
444			write_worker_exe_invalid_version(&prepare_worker_path, bad_version)?;
445			write_worker_exe_invalid_version(&execute_worker_path, bad_version)?;
446			assert_matches!(
447				determine_workers_paths(Some(given_workers_path), None, Some(TEST_NODE_VERSION.into())),
448				Err(Error::WorkerBinaryVersionMismatch { worker_version: v1, node_version: v2, worker_path: p }) if v1 == bad_version && v2 == TEST_NODE_VERSION && p == prepare_worker_path
449			);
450
451			// Given worker binary returns bad version.
452			let given_workers_path = tempdir.join("usr/local/bin/test-worker");
453			write_worker_exe_invalid_version(&given_workers_path, bad_version)?;
454			assert_matches!(
455				determine_workers_paths(Some(given_workers_path.clone()), None, Some(TEST_NODE_VERSION.into())),
456				Err(Error::WorkerBinaryVersionMismatch { worker_version: v1, node_version: v2, worker_path: p }) if v1 == bad_version && v2 == TEST_NODE_VERSION && p == given_workers_path
457			);
458
459			Ok(())
460		})
461		.unwrap();
462	}
463
464	#[test]
465	fn should_find_valid_workers() {
466		// Test bin location.
467		with_temp_dir_structure(|tempdir, _| {
468			let prepare_worker_bin_path = tempdir.join("usr/bin/polkadot-prepare-worker");
469			write_worker_exe(&prepare_worker_bin_path)?;
470
471			let execute_worker_bin_path = tempdir.join("usr/bin/polkadot-execute-worker");
472			write_worker_exe(&execute_worker_bin_path)?;
473
474			assert_matches!(
475				determine_workers_paths(None, None, Some(TEST_NODE_VERSION.into())),
476				Ok((p1, p2)) if p1 == prepare_worker_bin_path && p2 == execute_worker_bin_path
477			);
478
479			Ok(())
480		})
481		.unwrap();
482
483		// Test lib location.
484		with_temp_dir_structure(|tempdir, _| {
485			let prepare_worker_lib_path = tempdir.join("usr/lib/polkadot/polkadot-prepare-worker");
486			write_worker_exe(&prepare_worker_lib_path)?;
487
488			let execute_worker_lib_path = tempdir.join("usr/lib/polkadot/polkadot-execute-worker");
489			write_worker_exe(&execute_worker_lib_path)?;
490
491			assert_matches!(
492				determine_workers_paths(None, None, Some(TEST_NODE_VERSION.into())),
493				Ok((p1, p2)) if p1 == prepare_worker_lib_path && p2 == execute_worker_lib_path
494			);
495
496			Ok(())
497		})
498		.unwrap();
499	}
500}