pezsc_executor/
wasm_runtime.rs

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