referrerpolicy=no-referrer-when-downgrade

sc_service/client/
wasm_override.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
5
6// This program is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10
11// This program is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14// GNU General Public License for more details.
15
16// You should have received a copy of the GNU General Public License
17// along with this program. If not, see <https://www.gnu.org/licenses/>.
18
19//! # WASM Local Blob-Override
20//!
21//! WASM Local blob override provides tools to replace on-chain WASM with custom WASM.
22//! These customized WASM blobs may include functionality that is not included in the
23//! on-chain WASM, such as tracing or debugging information. This extra information is especially
24//! useful in external scenarios, like exchanges or archive nodes.
25//!
26//! ## Usage
27//!
28//! WASM overrides may be enabled with the `--wasm-runtime-overrides` argument. The argument
29//! expects a path to a directory that holds custom WASM.
30//!
31//! Any file ending in '.wasm' will be scraped and instantiated as a WASM blob. WASM can be built by
32//! compiling the required runtime with the changes needed. For example, compiling a runtime with
33//! tracing enabled would produce a WASM blob that can used.
34//!
35//! A custom WASM blob will override on-chain WASM if the spec version matches. If it is
36//! required to overrides multiple runtimes, multiple WASM blobs matching each of the spec versions
37//! needed must be provided in the given directory.
38
39use sc_executor::RuntimeVersionOf;
40use sp_blockchain::Result;
41use sp_core::traits::{FetchRuntimeCode, RuntimeCode, WrappedRuntimeCode};
42use sp_state_machine::BasicExternalities;
43use sp_version::RuntimeVersion;
44use std::{
45	collections::{hash_map::DefaultHasher, HashMap},
46	fs,
47	hash::Hasher as _,
48	path::{Path, PathBuf},
49	time::{Duration, Instant},
50};
51
52/// The interval in that we will print a warning when a wasm blob `spec_name`
53/// doesn't match with the on-chain `spec_name`.
54const WARN_INTERVAL: Duration = Duration::from_secs(30);
55
56/// Auxiliary structure that holds a wasm blob and its hash.
57#[derive(Debug)]
58struct WasmBlob {
59	/// The actual wasm blob, aka the code.
60	code: Vec<u8>,
61	/// The hash of [`Self::code`].
62	hash: Vec<u8>,
63	/// The path where this blob was found.
64	path: PathBuf,
65	/// The runtime version of this blob.
66	version: RuntimeVersion,
67	/// When was the last time we have warned about the wasm blob having
68	/// a wrong `spec_name`?
69	last_warn: parking_lot::Mutex<Option<Instant>>,
70}
71
72impl WasmBlob {
73	fn new(code: Vec<u8>, hash: Vec<u8>, path: PathBuf, version: RuntimeVersion) -> Self {
74		Self { code, hash, path, version, last_warn: Default::default() }
75	}
76
77	fn runtime_code(&self, heap_pages: Option<u64>) -> RuntimeCode {
78		RuntimeCode { code_fetcher: self, hash: self.hash.clone(), heap_pages }
79	}
80}
81
82/// Make a hash out of a byte string using the default rust hasher
83fn make_hash<K: std::hash::Hash + ?Sized>(val: &K) -> Vec<u8> {
84	let mut state = DefaultHasher::new();
85	val.hash(&mut state);
86	state.finish().to_le_bytes().to_vec()
87}
88
89impl FetchRuntimeCode for WasmBlob {
90	fn fetch_runtime_code(&self) -> Option<std::borrow::Cow<[u8]>> {
91		Some(self.code.as_slice().into())
92	}
93}
94
95#[derive(Debug, thiserror::Error)]
96#[allow(missing_docs)]
97pub enum WasmOverrideError {
98	#[error("Failed to get runtime version: {0}")]
99	VersionInvalid(String),
100
101	#[error("WASM override IO error")]
102	Io(PathBuf, #[source] std::io::Error),
103
104	#[error("Overwriting WASM requires a directory where local \
105	WASM is stored. {} is not a directory", .0.display())]
106	NotADirectory(PathBuf),
107
108	#[error("Duplicate WASM Runtimes found: \n{}\n", .0.join("\n") )]
109	DuplicateRuntime(Vec<String>),
110}
111
112impl From<WasmOverrideError> for sp_blockchain::Error {
113	fn from(err: WasmOverrideError) -> Self {
114		Self::Application(Box::new(err))
115	}
116}
117
118/// Scrapes WASM from a folder and returns WASM from that folder
119/// if the runtime spec version matches.
120#[derive(Debug)]
121pub struct WasmOverride {
122	// Map of runtime spec version -> Wasm Blob
123	overrides: HashMap<u32, WasmBlob>,
124}
125
126impl WasmOverride {
127	pub fn new<P, E>(path: P, executor: &E) -> Result<Self>
128	where
129		P: AsRef<Path>,
130		E: RuntimeVersionOf,
131	{
132		let overrides = Self::scrape_overrides(path.as_ref(), executor)?;
133		Ok(Self { overrides })
134	}
135
136	/// Gets an override by it's runtime spec version.
137	///
138	/// Returns `None` if an override for a spec version does not exist.
139	pub fn get<'a, 'b: 'a>(
140		&'b self,
141		spec: &u32,
142		pages: Option<u64>,
143		spec_name: &str,
144	) -> Option<(RuntimeCode<'a>, RuntimeVersion)> {
145		self.overrides.get(spec).and_then(|w| {
146			if spec_name == &*w.version.spec_name {
147				Some((w.runtime_code(pages), w.version.clone()))
148			} else {
149				let mut last_warn = w.last_warn.lock();
150				let now = Instant::now();
151
152				if last_warn.map_or(true, |l| l + WARN_INTERVAL <= now) {
153					*last_warn = Some(now);
154
155					tracing::warn!(
156						target = "wasm_overrides",
157						on_chain_spec_name = %spec_name,
158						override_spec_name = %w.version,
159						spec_version = %spec,
160						wasm_file = %w.path.display(),
161						"On chain and override `spec_name` do not match! Ignoring override.",
162					);
163				}
164
165				None
166			}
167		})
168	}
169
170	/// Scrapes a folder for WASM runtimes.
171	/// Returns a hashmap of the runtime version and wasm runtime code.
172	fn scrape_overrides<E>(dir: &Path, executor: &E) -> Result<HashMap<u32, WasmBlob>>
173	where
174		E: RuntimeVersionOf,
175	{
176		let handle_err = |e: std::io::Error| -> sp_blockchain::Error {
177			WasmOverrideError::Io(dir.to_owned(), e).into()
178		};
179
180		if !dir.is_dir() {
181			return Err(WasmOverrideError::NotADirectory(dir.to_owned()).into())
182		}
183
184		let mut overrides = HashMap::new();
185		let mut duplicates = Vec::new();
186		for entry in fs::read_dir(dir).map_err(handle_err)? {
187			let entry = entry.map_err(handle_err)?;
188			let path = entry.path();
189			if let Some("wasm") = path.extension().and_then(|e| e.to_str()) {
190				let code = fs::read(&path).map_err(handle_err)?;
191				let code_hash = make_hash(&code);
192				let version = Self::runtime_version(executor, &code, &code_hash, Some(128))?;
193				tracing::info!(
194					target: "wasm_overrides",
195					version = %version,
196					file = %path.display(),
197					"Found wasm override.",
198				);
199
200				let wasm = WasmBlob::new(code, code_hash, path.clone(), version.clone());
201
202				if let Some(other) = overrides.insert(version.spec_version, wasm) {
203					tracing::info!(
204						target: "wasm_overrides",
205						first = %other.path.display(),
206						second = %path.display(),
207						%version,
208						"Found duplicate spec version for runtime.",
209					);
210					duplicates.push(path.display().to_string());
211				}
212			}
213		}
214
215		if !duplicates.is_empty() {
216			return Err(WasmOverrideError::DuplicateRuntime(duplicates).into())
217		}
218
219		Ok(overrides)
220	}
221
222	fn runtime_version<E>(
223		executor: &E,
224		code: &[u8],
225		code_hash: &[u8],
226		heap_pages: Option<u64>,
227	) -> Result<RuntimeVersion>
228	where
229		E: RuntimeVersionOf,
230	{
231		let mut ext = BasicExternalities::default();
232		executor
233			.runtime_version(
234				&mut ext,
235				&RuntimeCode {
236					code_fetcher: &WrappedRuntimeCode(code.into()),
237					heap_pages,
238					hash: code_hash.into(),
239				},
240			)
241			.map_err(|e| WasmOverrideError::VersionInvalid(e.to_string()).into())
242	}
243}
244
245/// Returns a WasmOverride struct filled with dummy data for testing.
246#[cfg(test)]
247pub fn dummy_overrides() -> WasmOverride {
248	let version = RuntimeVersion { spec_name: "test".into(), ..Default::default() };
249	let mut overrides = HashMap::new();
250	overrides.insert(
251		0,
252		WasmBlob::new(vec![0, 0, 0, 0, 0, 0, 0, 0], vec![0], PathBuf::new(), version.clone()),
253	);
254	overrides.insert(
255		1,
256		WasmBlob::new(vec![1, 1, 1, 1, 1, 1, 1, 1], vec![1], PathBuf::new(), version.clone()),
257	);
258	overrides
259		.insert(2, WasmBlob::new(vec![2, 2, 2, 2, 2, 2, 2, 2], vec![2], PathBuf::new(), version));
260
261	WasmOverride { overrides }
262}
263
264#[cfg(test)]
265mod tests {
266	use super::*;
267	use sc_executor::{HeapAllocStrategy, WasmExecutor};
268	use std::fs::{self, File};
269
270	fn executor() -> WasmExecutor {
271		WasmExecutor::builder()
272			.with_onchain_heap_alloc_strategy(HeapAllocStrategy::Static { extra_pages: 128 })
273			.with_offchain_heap_alloc_strategy(HeapAllocStrategy::Static { extra_pages: 128 })
274			.with_max_runtime_instances(1)
275			.with_runtime_cache_size(2)
276			.build()
277	}
278
279	fn wasm_test<F>(fun: F)
280	where
281		F: Fn(&Path, &[u8], &WasmExecutor),
282	{
283		let exec = executor();
284		let bytes = substrate_test_runtime::wasm_binary_unwrap();
285		let dir = tempfile::tempdir().expect("Create a temporary directory");
286		fun(dir.path(), bytes, &exec);
287		dir.close().expect("Temporary Directory should close");
288	}
289
290	#[test]
291	fn should_get_runtime_version() {
292		let executor = executor();
293
294		let version = WasmOverride::runtime_version(
295			&executor,
296			substrate_test_runtime::wasm_binary_unwrap(),
297			&[1],
298			Some(128),
299		)
300		.expect("should get the `RuntimeVersion` of the test-runtime wasm blob");
301		assert_eq!(version.spec_version, 2);
302	}
303
304	#[test]
305	fn should_scrape_wasm() {
306		wasm_test(|dir, wasm_bytes, exec| {
307			fs::write(dir.join("test.wasm"), wasm_bytes).expect("Create test file");
308			let overrides =
309				WasmOverride::scrape_overrides(dir, exec).expect("HashMap of u32 and WasmBlob");
310			let wasm = overrides.get(&2).expect("WASM binary");
311			assert_eq!(wasm.code, substrate_test_runtime::wasm_binary_unwrap().to_vec())
312		});
313	}
314
315	#[test]
316	fn should_check_for_duplicates() {
317		wasm_test(|dir, wasm_bytes, exec| {
318			fs::write(dir.join("test0.wasm"), wasm_bytes).expect("Create test file");
319			fs::write(dir.join("test1.wasm"), wasm_bytes).expect("Create test file");
320			let scraped = WasmOverride::scrape_overrides(dir, exec);
321
322			match scraped {
323				Err(sp_blockchain::Error::Application(e)) => {
324					match e.downcast_ref::<WasmOverrideError>() {
325						Some(WasmOverrideError::DuplicateRuntime(duplicates)) => {
326							assert_eq!(duplicates.len(), 1);
327						},
328						_ => panic!("Test should end with Msg Error Variant"),
329					}
330				},
331				_ => panic!("Test should end in error"),
332			}
333		});
334	}
335
336	#[test]
337	fn should_ignore_non_wasm() {
338		wasm_test(|dir, wasm_bytes, exec| {
339			File::create(dir.join("README.md")).expect("Create test file");
340			File::create(dir.join("LICENSE")).expect("Create a test file");
341			fs::write(dir.join("test0.wasm"), wasm_bytes).expect("Create test file");
342			let scraped =
343				WasmOverride::scrape_overrides(dir, exec).expect("HashMap of u32 and WasmBlob");
344			assert_eq!(scraped.len(), 1);
345		});
346	}
347}