Skip to main content

soil_client/executor/
wasm_runtime.rs

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