referrerpolicy=no-referrer-when-downgrade

sc_executor/
wasm_runtime.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//! Traits and accessor functions for calling into the Substrate Wasm runtime.
20//!
21//! The primary means of accessing the runtimes is through a cache which saves the reusable
22//! components of the runtime that are expensive to initialize.
23
24use crate::error::{Error, WasmError};
25
26use codec::Decode;
27use parking_lot::Mutex;
28use sc_executor_common::{
29	runtime_blob::RuntimeBlob,
30	wasm_runtime::{HeapAllocStrategy, WasmInstance, WasmModule},
31};
32use schnellru::{ByLength, LruMap};
33use sp_core::traits::{Externalities, FetchRuntimeCode, RuntimeCode};
34use sp_version::RuntimeVersion;
35use sp_wasm_interface::HostFunctions;
36
37use std::{
38	panic::AssertUnwindSafe,
39	path::{Path, PathBuf},
40	sync::Arc,
41};
42
43/// Specification of different methods of executing the runtime Wasm code.
44#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
45pub enum WasmExecutionMethod {
46	/// Uses the Wasmtime compiled runtime.
47	Compiled {
48		/// The instantiation strategy to use.
49		instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy,
50	},
51}
52
53impl Default for WasmExecutionMethod {
54	fn default() -> Self {
55		Self::Compiled {
56			instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::PoolingCopyOnWrite,
57		}
58	}
59}
60
61#[derive(Debug, PartialEq, Eq, Hash, Clone)]
62struct VersionedRuntimeId {
63	/// Runtime code hash.
64	code_hash: Vec<u8>,
65	/// Wasm runtime type.
66	wasm_method: WasmExecutionMethod,
67	/// The heap allocation strategy this runtime was created with.
68	heap_alloc_strategy: HeapAllocStrategy,
69}
70
71/// A Wasm runtime object along with its cached runtime version.
72struct VersionedRuntime {
73	/// Shared runtime that can spawn instances.
74	module: Box<dyn WasmModule>,
75	/// Runtime version according to `Core_version` if any.
76	version: Option<RuntimeVersion>,
77
78	// TODO: Remove this once the legacy instance reuse instantiation strategy
79	//       for `wasmtime` is gone, as this only makes sense with that particular strategy.
80	/// Cached instance pool.
81	instances: Vec<Mutex<Option<Box<dyn WasmInstance>>>>,
82}
83
84impl VersionedRuntime {
85	/// Run the given closure `f` with an instance of this runtime.
86	fn with_instance<R, F>(&self, ext: &mut dyn Externalities, f: F) -> Result<R, Error>
87	where
88		F: FnOnce(
89			&dyn WasmModule,
90			&mut dyn WasmInstance,
91			Option<&RuntimeVersion>,
92			&mut dyn Externalities,
93		) -> Result<R, Error>,
94	{
95		// Find a free instance
96		let instance = self
97			.instances
98			.iter()
99			.enumerate()
100			.find_map(|(index, i)| i.try_lock().map(|i| (index, i)));
101
102		match instance {
103			Some((index, mut locked)) => {
104				let (mut instance, new_inst) = locked
105					.take()
106					.map(|r| Ok((r, false)))
107					.unwrap_or_else(|| self.module.new_instance().map(|i| (i, true)))?;
108
109				let result = f(&*self.module, &mut *instance, self.version.as_ref(), ext);
110				if let Err(e) = &result {
111					if new_inst {
112						tracing::warn!(
113							target: "wasm-runtime",
114							error = %e,
115							"Fresh runtime instance failed",
116						)
117					} else {
118						tracing::warn!(
119							target: "wasm-runtime",
120							error = %e,
121							"Evicting failed runtime instance",
122						);
123					}
124				} else {
125					*locked = Some(instance);
126
127					if new_inst {
128						tracing::debug!(
129							target: "wasm-runtime",
130							"Allocated WASM instance {}/{}",
131							index + 1,
132							self.instances.len(),
133						);
134					}
135				}
136
137				result
138			},
139			None => {
140				tracing::warn!(target: "wasm-runtime", "Ran out of free WASM instances");
141
142				// Allocate a new instance
143				let mut instance = self.module.new_instance()?;
144
145				f(&*self.module, &mut *instance, self.version.as_ref(), ext)
146			},
147		}
148	}
149}
150
151/// Cache for the runtimes.
152///
153/// When an instance is requested for the first time it is added to this cache. Metadata is kept
154/// with the instance so that it can be efficiently reinitialized.
155///
156/// When using the Wasmi interpreter execution method, the metadata includes the initial memory and
157/// values of mutable globals. Follow-up requests to fetch a runtime return this one instance with
158/// the memory reset to the initial memory. So, one runtime instance is reused for every fetch
159/// request.
160///
161/// The size of cache is configurable via the cli option `--runtime-cache-size`.
162pub struct RuntimeCache {
163	/// A cache of runtimes along with metadata.
164	///
165	/// Runtimes sorted by recent usage. The most recently used is at the front.
166	runtimes: Mutex<LruMap<VersionedRuntimeId, Arc<VersionedRuntime>>>,
167	/// The size of the instances cache for each runtime.
168	max_runtime_instances: usize,
169	cache_path: Option<PathBuf>,
170}
171
172impl RuntimeCache {
173	/// Creates a new instance of a runtimes cache.
174	///
175	/// `max_runtime_instances` specifies the number of instances per runtime preserved in an
176	/// in-memory cache.
177	///
178	/// `cache_path` allows to specify an optional directory where the executor can store files
179	/// for caching.
180	///
181	/// `runtime_cache_size` specifies the number of different runtimes versions preserved in an
182	/// in-memory cache, must always be at least 1.
183	pub fn new(
184		max_runtime_instances: usize,
185		cache_path: Option<PathBuf>,
186		runtime_cache_size: u8,
187	) -> RuntimeCache {
188		let cap = ByLength::new(runtime_cache_size.max(1) as u32);
189		RuntimeCache { runtimes: Mutex::new(LruMap::new(cap)), max_runtime_instances, cache_path }
190	}
191
192	/// Prepares a WASM module instance and executes given function for it.
193	///
194	/// This uses internal cache to find available instance or create a new one.
195	/// # Parameters
196	///
197	/// `runtime_code` - The runtime wasm code used setup the runtime.
198	///
199	/// `ext` - The externalities to access the state.
200	///
201	/// `wasm_method` - Type of WASM backend to use.
202	///
203	/// `heap_alloc_strategy` - The heap allocation strategy to use.
204	///
205	/// `allow_missing_func_imports` - Ignore missing function imports.
206	///
207	/// `f` - Function to execute.
208	///
209	/// `H` - A compile-time list of host functions to expose to the runtime.
210	///
211	/// # Returns result of `f` wrapped in an additional result.
212	/// In case of failure one of two errors can be returned:
213	///
214	/// `Err::RuntimeConstruction` is returned for runtime construction issues.
215	///
216	/// `Error::InvalidMemoryReference` is returned if no memory export with the
217	/// identifier `memory` can be found in the runtime.
218	pub fn with_instance<'c, H, R, F>(
219		&self,
220		runtime_code: &'c RuntimeCode<'c>,
221		ext: &mut dyn Externalities,
222		wasm_method: WasmExecutionMethod,
223		heap_alloc_strategy: HeapAllocStrategy,
224		allow_missing_func_imports: bool,
225		f: F,
226	) -> Result<Result<R, Error>, Error>
227	where
228		H: HostFunctions,
229		F: FnOnce(
230			&dyn WasmModule,
231			&mut dyn WasmInstance,
232			Option<&RuntimeVersion>,
233			&mut dyn Externalities,
234		) -> Result<R, Error>,
235	{
236		let code_hash = &runtime_code.hash;
237
238		let versioned_runtime_id =
239			VersionedRuntimeId { code_hash: code_hash.clone(), heap_alloc_strategy, wasm_method };
240
241		let mut runtimes = self.runtimes.lock(); // this must be released prior to calling f
242		let versioned_runtime = if let Some(versioned_runtime) = runtimes.get(&versioned_runtime_id)
243		{
244			versioned_runtime.clone()
245		} else {
246			let code = runtime_code.fetch_runtime_code().ok_or(WasmError::CodeNotFound)?;
247
248			let time = std::time::Instant::now();
249
250			let result = create_versioned_wasm_runtime::<H>(
251				&code,
252				ext,
253				wasm_method,
254				heap_alloc_strategy,
255				allow_missing_func_imports,
256				self.max_runtime_instances,
257				self.cache_path.as_deref(),
258			);
259
260			match result {
261				Ok(ref result) => {
262					tracing::debug!(
263						target: "wasm-runtime",
264						"Prepared new runtime version {:?} in {} ms.",
265						result.version,
266						time.elapsed().as_millis(),
267					);
268				},
269				Err(ref err) => {
270					tracing::warn!(target: "wasm-runtime", error = ?err, "Cannot create a runtime");
271				},
272			}
273
274			let versioned_runtime = Arc::new(result?);
275
276			// Save new versioned wasm runtime in cache
277			runtimes.insert(versioned_runtime_id, versioned_runtime.clone());
278
279			versioned_runtime
280		};
281
282		// Lock must be released prior to calling f
283		drop(runtimes);
284
285		Ok(versioned_runtime.with_instance(ext, f))
286	}
287}
288
289/// Create a wasm runtime with the given `code`.
290pub fn create_wasm_runtime_with_code<H>(
291	wasm_method: WasmExecutionMethod,
292	heap_alloc_strategy: HeapAllocStrategy,
293	blob: RuntimeBlob,
294	allow_missing_func_imports: bool,
295	cache_path: Option<&Path>,
296) -> Result<Box<dyn WasmModule>, WasmError>
297where
298	H: HostFunctions,
299{
300	if let Some(blob) = blob.as_polkavm_blob() {
301		return sc_executor_polkavm::create_runtime::<H>(blob);
302	}
303
304	match wasm_method {
305		WasmExecutionMethod::Compiled { instantiation_strategy } =>
306			sc_executor_wasmtime::create_runtime::<H>(
307				blob,
308				sc_executor_wasmtime::Config {
309					allow_missing_func_imports,
310					cache_path: cache_path.map(ToOwned::to_owned),
311					semantics: sc_executor_wasmtime::Semantics {
312						heap_alloc_strategy,
313						instantiation_strategy,
314						deterministic_stack_limit: None,
315						canonicalize_nans: false,
316						parallel_compilation: true,
317						wasm_multi_value: false,
318						wasm_bulk_memory: false,
319						wasm_reference_types: false,
320						wasm_simd: false,
321					},
322				},
323			)
324			.map(|runtime| -> Box<dyn WasmModule> { Box::new(runtime) }),
325	}
326}
327
328fn decode_version(mut version: &[u8]) -> Result<RuntimeVersion, WasmError> {
329	Decode::decode(&mut version).map_err(|_| {
330		WasmError::Instantiation(
331			"failed to decode \"Core_version\" result using old runtime version".into(),
332		)
333	})
334}
335
336fn decode_runtime_apis(apis: &[u8]) -> Result<Vec<([u8; 8], u32)>, WasmError> {
337	use sp_api::RUNTIME_API_INFO_SIZE;
338
339	apis.chunks(RUNTIME_API_INFO_SIZE)
340		.map(|chunk| {
341			// `chunk` can be less than `RUNTIME_API_INFO_SIZE` if the total length of `apis`
342			// doesn't completely divide by `RUNTIME_API_INFO_SIZE`.
343			<[u8; RUNTIME_API_INFO_SIZE]>::try_from(chunk)
344				.map(sp_api::deserialize_runtime_api_info)
345				.map_err(|_| WasmError::Other("a clipped runtime api info declaration".to_owned()))
346		})
347		.collect::<Result<Vec<_>, WasmError>>()
348}
349
350/// Take the runtime blob and scan it for the custom wasm sections containing the version
351/// information and construct the `RuntimeVersion` from them.
352///
353/// If there are no such sections, it returns `None`. If there is an error during decoding those
354/// sections, `Err` will be returned.
355pub fn read_embedded_version(blob: &RuntimeBlob) -> Result<Option<RuntimeVersion>, WasmError> {
356	if let Some(mut version_section) = blob.custom_section_contents("runtime_version") {
357		let apis = blob
358			.custom_section_contents("runtime_apis")
359			.map(decode_runtime_apis)
360			.transpose()?
361			.map(Into::into);
362
363		let core_version = apis.as_ref().and_then(sp_version::core_version_from_apis);
364		// We do not use `RuntimeVersion::decode` here because that `decode_version` relies on
365		// presence of a special API in the `apis` field to treat the input as a non-legacy version.
366		// However the structure found in the `runtime_version` always contain an empty `apis`
367		// field. Therefore the version read will be mistakenly treated as an legacy one.
368		let mut decoded_version = sp_version::RuntimeVersion::decode_with_version_hint(
369			&mut version_section,
370			core_version,
371		)
372		.map_err(|_| WasmError::Instantiation("failed to decode version section".into()))?;
373
374		if let Some(apis) = apis {
375			decoded_version.apis = apis;
376		}
377
378		Ok(Some(decoded_version))
379	} else {
380		Ok(None)
381	}
382}
383
384fn create_versioned_wasm_runtime<H>(
385	code: &[u8],
386	ext: &mut dyn Externalities,
387	wasm_method: WasmExecutionMethod,
388	heap_alloc_strategy: HeapAllocStrategy,
389	allow_missing_func_imports: bool,
390	max_instances: usize,
391	cache_path: Option<&Path>,
392) -> Result<VersionedRuntime, WasmError>
393where
394	H: HostFunctions,
395{
396	// The incoming code may be actually compressed. We decompress it here and then work with
397	// the uncompressed code from now on.
398	let blob = sc_executor_common::runtime_blob::RuntimeBlob::uncompress_if_needed(code)?;
399
400	// Use the runtime blob to scan if there is any metadata embedded into the wasm binary
401	// pertaining to runtime version. We do it before consuming the runtime blob for creating the
402	// runtime.
403	let mut version = read_embedded_version(&blob)?;
404
405	let runtime = create_wasm_runtime_with_code::<H>(
406		wasm_method,
407		heap_alloc_strategy,
408		blob,
409		allow_missing_func_imports,
410		cache_path,
411	)?;
412
413	// If the runtime blob doesn't embed the runtime version then use the legacy version query
414	// mechanism: call the runtime.
415	if version.is_none() {
416		// Call to determine runtime version.
417		let version_result = {
418			// `ext` is already implicitly handled as unwind safe, as we store it in a global
419			// variable.
420			let mut ext = AssertUnwindSafe(ext);
421
422			// The following unwind safety assertion is OK because if the method call panics, the
423			// runtime will be dropped.
424			let runtime = AssertUnwindSafe(runtime.as_ref());
425			crate::executor::with_externalities_safe(&mut **ext, move || {
426				runtime.new_instance()?.call("Core_version".into(), &[])
427			})
428			.map_err(|_| WasmError::Instantiation("panic in call to get runtime version".into()))?
429		};
430
431		if let Ok(version_buf) = version_result {
432			version = Some(decode_version(&version_buf)?)
433		}
434	}
435
436	let mut instances = Vec::with_capacity(max_instances);
437	instances.resize_with(max_instances, || Mutex::new(None));
438
439	Ok(VersionedRuntime { module: runtime, version, instances })
440}
441
442#[cfg(test)]
443mod tests {
444	extern crate alloc;
445
446	use super::*;
447	use alloc::borrow::Cow;
448	use codec::Encode;
449	use sp_api::{Core, RuntimeApiInfo};
450	use sp_version::{create_apis_vec, RuntimeVersion};
451	use sp_wasm_interface::HostFunctions;
452	use substrate_test_runtime::Block;
453
454	#[derive(Encode)]
455	pub struct OldRuntimeVersion {
456		pub spec_name: Cow<'static, str>,
457		pub impl_name: Cow<'static, str>,
458		pub authoring_version: u32,
459		pub spec_version: u32,
460		pub impl_version: u32,
461		pub apis: sp_version::ApisVec,
462	}
463
464	#[test]
465	fn host_functions_are_equal() {
466		let host_functions = sp_io::SubstrateHostFunctions::host_functions();
467
468		let equal = &host_functions[..] == &host_functions[..];
469		assert!(equal, "Host functions are not equal");
470	}
471
472	#[test]
473	fn old_runtime_version_decodes() {
474		let old_runtime_version = OldRuntimeVersion {
475			spec_name: "test".into(),
476			impl_name: "test".into(),
477			authoring_version: 1,
478			spec_version: 1,
479			impl_version: 1,
480			apis: create_apis_vec!([(<dyn Core::<Block>>::ID, 1)]),
481		};
482
483		let version = decode_version(&old_runtime_version.encode()).unwrap();
484		assert_eq!(1, version.transaction_version);
485		assert_eq!(0, version.system_version);
486	}
487
488	#[test]
489	fn old_runtime_version_decodes_fails_with_version_3() {
490		let old_runtime_version = OldRuntimeVersion {
491			spec_name: "test".into(),
492			impl_name: "test".into(),
493			authoring_version: 1,
494			spec_version: 1,
495			impl_version: 1,
496			apis: create_apis_vec!([(<dyn Core::<Block>>::ID, 3)]),
497		};
498
499		decode_version(&old_runtime_version.encode()).unwrap_err();
500	}
501
502	#[test]
503	fn new_runtime_version_decodes() {
504		let old_runtime_version = RuntimeVersion {
505			spec_name: "test".into(),
506			impl_name: "test".into(),
507			authoring_version: 1,
508			spec_version: 1,
509			impl_version: 1,
510			apis: create_apis_vec!([(<dyn Core::<Block>>::ID, 3)]),
511			transaction_version: 3,
512			system_version: 4,
513		};
514
515		let version = decode_version(&old_runtime_version.encode()).unwrap();
516		assert_eq!(3, version.transaction_version);
517		assert_eq!(0, version.system_version);
518
519		let old_runtime_version = RuntimeVersion {
520			spec_name: "test".into(),
521			impl_name: "test".into(),
522			authoring_version: 1,
523			spec_version: 1,
524			impl_version: 1,
525			apis: create_apis_vec!([(<dyn Core::<Block>>::ID, 4)]),
526			transaction_version: 3,
527			system_version: 4,
528		};
529
530		let version = decode_version(&old_runtime_version.encode()).unwrap();
531		assert_eq!(3, version.transaction_version);
532		assert_eq!(4, version.system_version);
533	}
534
535	#[test]
536	fn embed_runtime_version_works() {
537		let wasm = sp_maybe_compressed_blob::decompress(
538			substrate_test_runtime::wasm_binary_unwrap(),
539			sp_maybe_compressed_blob::CODE_BLOB_BOMB_LIMIT,
540		)
541		.expect("Decompressing works");
542		let runtime_version = RuntimeVersion {
543			spec_name: "test_replace".into(),
544			impl_name: "test_replace".into(),
545			authoring_version: 100,
546			spec_version: 100,
547			impl_version: 100,
548			apis: create_apis_vec!([(<dyn Core::<Block>>::ID, 4)]),
549			transaction_version: 100,
550			system_version: 1,
551		};
552
553		let embedded = sp_version::embed::embed_runtime_version(&wasm, runtime_version.clone())
554			.expect("Embedding works");
555
556		let blob = RuntimeBlob::new(&embedded).expect("Embedded blob is valid");
557		let read_version = read_embedded_version(&blob)
558			.ok()
559			.flatten()
560			.expect("Reading embedded version works");
561
562		assert_eq!(runtime_version, read_version);
563	}
564}