1use 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#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
45pub enum WasmExecutionMethod {
46 Compiled {
48 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 code_hash: Vec<u8>,
66 wasm_method: WasmExecutionMethod,
68 heap_alloc_strategy: HeapAllocStrategy,
70}
71
72struct VersionedRuntime {
74 module: Box<dyn WasmModule>,
76 version: Option<RuntimeVersion>,
78
79 instances: Vec<Mutex<Option<Box<dyn WasmInstance>>>>,
83}
84
85impl VersionedRuntime {
86 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 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 let mut instance = self.module.new_instance()?;
145
146 f(&*self.module, &mut *instance, self.version.as_ref(), ext)
147 },
148 }
149 }
150}
151
152pub struct RuntimeCache {
164 runtimes: Mutex<LruMap<VersionedRuntimeId, Arc<VersionedRuntime>>>,
168 max_runtime_instances: usize,
170 cache_path: Option<PathBuf>,
171}
172
173impl RuntimeCache {
174 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 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(); 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 runtimes.insert(versioned_runtime_id, versioned_runtime.clone());
279
280 versioned_runtime
281 };
282
283 drop(runtimes);
285
286 Ok(versioned_runtime.with_instance(ext, f))
287 }
288}
289
290pub 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 <[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
352pub 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 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 let blob = pezsc_executor_common::runtime_blob::RuntimeBlob::uncompress_if_needed(code)?;
401
402 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 version.is_none() {
418 let version_result = {
420 let mut ext = AssertUnwindSafe(ext);
423
424 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}