Skip to main content

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