1use 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, LazyLock},
41};
42
43#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
45pub enum WasmExecutionMethod {
46 Compiled {
48 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 code_hash: Vec<u8>,
65 wasm_method: WasmExecutionMethod,
67}
68
69type ModuleResult = Result<Box<dyn WasmModule>, WasmError>;
70type ModuleInit = Box<dyn FnOnce() -> ModuleResult + Send>;
71
72#[derive(Clone)]
79struct InterruptibleModule(Arc<LazyLock<ModuleResult, ModuleInit>>);
80
81impl InterruptibleModule {
82 fn new(init: impl FnOnce() -> ModuleResult + Send + 'static) -> Self {
83 let init: ModuleInit = Box::new(init);
84 Self(Arc::new(LazyLock::new(init)))
85 }
86
87 fn get(&self) -> Result<&dyn WasmModule, Error> {
90 match LazyLock::force(&self.0) {
91 Ok(module) => Ok(&**module),
92 Err(e) => Err(Error::from(e.clone())),
93 }
94 }
95
96 fn compile_in_background(&self) {
103 let module = self.clone();
104 let spawned = std::thread::Builder::new().name("wasm-interruptible-compile".into()).spawn(
105 move || {
106 let _ = LazyLock::force(&module.0);
107 },
108 );
109
110 if let Err(err) = spawned {
111 tracing::warn!(
112 target: "wasm-runtime",
113 error = %err,
114 "Cannot spawn the interruptible runtime compilation thread, compiling on first use",
115 );
116 }
117 }
118}
119
120struct VersionedRuntime {
122 module: Box<dyn WasmModule>,
124 interruptible_module: InterruptibleModule,
126 version: Option<RuntimeVersion>,
128
129 instances: Vec<Mutex<Option<Box<dyn WasmInstance>>>>,
133}
134
135impl VersionedRuntime {
136 fn with_instance<R, F>(
138 &self,
139 ext: &mut dyn Externalities,
140 heap_alloc_strategy: HeapAllocStrategy,
141 f: F,
142 ) -> Result<R, Error>
143 where
144 F: FnOnce(
145 &dyn WasmModule,
146 &mut dyn WasmInstance,
147 Option<&RuntimeVersion>,
148 &mut dyn Externalities,
149 ) -> Result<R, Error>,
150 {
151 let instance = self
153 .instances
154 .iter()
155 .enumerate()
156 .find_map(|(index, i)| i.try_lock().map(|i| (index, i)));
157
158 match instance {
159 Some((index, mut locked)) => {
160 let (mut instance, new_inst) =
161 locked.take().map(|r| Ok((r, false))).unwrap_or_else(|| {
162 self.module.new_instance(heap_alloc_strategy).map(|i| (i, true))
163 })?;
164
165 if !new_inst {
169 instance.set_heap_alloc_strategy(heap_alloc_strategy);
170 }
171
172 let result = f(&*self.module, &mut *instance, self.version.as_ref(), ext);
173 if let Err(e) = &result {
174 if new_inst {
175 tracing::warn!(
176 target: "wasm-runtime",
177 error = %e,
178 "Fresh runtime instance failed",
179 )
180 } else {
181 tracing::warn!(
182 target: "wasm-runtime",
183 error = %e,
184 "Evicting failed runtime instance",
185 );
186 }
187 } else {
188 *locked = Some(instance);
189
190 if new_inst {
191 tracing::debug!(
192 target: "wasm-runtime",
193 "Allocated WASM instance {}/{}",
194 index + 1,
195 self.instances.len(),
196 );
197 }
198 }
199
200 result
201 },
202 None => {
203 tracing::warn!(target: "wasm-runtime", "Ran out of free WASM instances");
204
205 let mut instance = self.module.new_instance(heap_alloc_strategy)?;
207
208 f(&*self.module, &mut *instance, self.version.as_ref(), ext)
209 },
210 }
211 }
212
213 fn with_interruptible_instance<R, F>(
219 &self,
220 ext: &mut dyn Externalities,
221 heap_alloc_strategy: HeapAllocStrategy,
222 f: F,
223 ) -> Result<R, Error>
224 where
225 F: FnOnce(
226 &mut dyn WasmInstance,
227 Option<&RuntimeVersion>,
228 &mut dyn Externalities,
229 ) -> Result<R, Error>,
230 {
231 let mut instance = self.interruptible_module.get()?.new_instance(heap_alloc_strategy)?;
232
233 f(&mut *instance, self.version.as_ref(), ext)
234 }
235}
236
237pub struct RuntimeCache {
249 runtimes: Mutex<LruMap<VersionedRuntimeId, Arc<VersionedRuntime>>>,
253 max_runtime_instances: usize,
255 cache_path: Option<PathBuf>,
256}
257
258impl RuntimeCache {
259 pub fn new(
270 max_runtime_instances: usize,
271 cache_path: Option<PathBuf>,
272 runtime_cache_size: u8,
273 ) -> RuntimeCache {
274 let cap = ByLength::new(runtime_cache_size.max(1) as u32);
275 RuntimeCache { runtimes: Mutex::new(LruMap::new(cap)), max_runtime_instances, cache_path }
276 }
277
278 pub fn with_instance<'c, H, R, F>(
305 &self,
306 runtime_code: &'c RuntimeCode<'c>,
307 ext: &mut dyn Externalities,
308 wasm_method: WasmExecutionMethod,
309 heap_alloc_strategy: HeapAllocStrategy,
310 allow_missing_func_imports: bool,
311 f: F,
312 ) -> Result<Result<R, Error>, Error>
313 where
314 H: HostFunctions,
315 F: FnOnce(
316 &dyn WasmModule,
317 &mut dyn WasmInstance,
318 Option<&RuntimeVersion>,
319 &mut dyn Externalities,
320 ) -> Result<R, Error>,
321 {
322 let versioned_runtime = self.versioned_runtime::<H>(
323 runtime_code,
324 ext,
325 wasm_method,
326 heap_alloc_strategy,
327 allow_missing_func_imports,
328 )?;
329
330 Ok(versioned_runtime.with_instance(ext, heap_alloc_strategy, f))
331 }
332
333 pub fn with_interruptible_instance<'c, H, R, F>(
343 &self,
344 runtime_code: &'c RuntimeCode<'c>,
345 ext: &mut dyn Externalities,
346 wasm_method: WasmExecutionMethod,
347 heap_alloc_strategy: HeapAllocStrategy,
348 allow_missing_func_imports: bool,
349 f: F,
350 ) -> Result<Result<R, Error>, Error>
351 where
352 H: HostFunctions,
353 F: FnOnce(
354 &mut dyn WasmInstance,
355 Option<&RuntimeVersion>,
356 &mut dyn Externalities,
357 ) -> Result<R, Error>,
358 {
359 let versioned_runtime = self.versioned_runtime::<H>(
360 runtime_code,
361 ext,
362 wasm_method,
363 heap_alloc_strategy,
364 allow_missing_func_imports,
365 )?;
366
367 Ok(versioned_runtime.with_interruptible_instance(ext, heap_alloc_strategy, f))
368 }
369
370 fn versioned_runtime<H>(
373 &self,
374 runtime_code: &RuntimeCode,
375 ext: &mut dyn Externalities,
376 wasm_method: WasmExecutionMethod,
377 heap_alloc_strategy: HeapAllocStrategy,
378 allow_missing_func_imports: bool,
379 ) -> Result<Arc<VersionedRuntime>, WasmError>
380 where
381 H: HostFunctions,
382 {
383 let code_hash = &runtime_code.hash;
384
385 let versioned_runtime_id = VersionedRuntimeId { code_hash: code_hash.clone(), wasm_method };
386
387 let mut runtimes = self.runtimes.lock();
390 if let Some(versioned_runtime) = runtimes.get(&versioned_runtime_id) {
391 return Ok(versioned_runtime.clone());
392 }
393
394 let code = runtime_code.fetch_runtime_code().ok_or(WasmError::CodeNotFound)?;
395
396 let time = std::time::Instant::now();
397
398 let result = create_versioned_wasm_runtime::<H>(
399 &code,
400 ext,
401 wasm_method,
402 heap_alloc_strategy,
403 allow_missing_func_imports,
404 self.max_runtime_instances,
405 self.cache_path.as_deref(),
406 );
407
408 match result {
409 Ok(ref result) => {
410 tracing::debug!(
411 target: "wasm-runtime",
412 "Prepared new runtime version {:?} in {} ms.",
413 result.version,
414 time.elapsed().as_millis(),
415 );
416 },
417 Err(ref err) => {
418 tracing::warn!(target: "wasm-runtime", error = ?err, "Cannot create a runtime");
419 },
420 }
421
422 let versioned_runtime = Arc::new(result?);
423
424 runtimes.insert(versioned_runtime_id, versioned_runtime.clone());
426
427 Ok(versioned_runtime)
428 }
429}
430
431fn wasmtime_config(
432 heap_alloc_strategy: HeapAllocStrategy,
433 instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy,
434 allow_missing_func_imports: bool,
435 cache_path: Option<&Path>,
436) -> sc_executor_wasmtime::Config {
437 sc_executor_wasmtime::Config {
438 allow_missing_func_imports,
439 cache_path: cache_path.map(ToOwned::to_owned),
440 semantics: sc_executor_wasmtime::Semantics {
441 heap_alloc_strategy,
442 instantiation_strategy,
443 deterministic_stack_limit: None,
444 canonicalize_nans: false,
445 parallel_compilation: true,
446 wasm_multi_value: false,
447 wasm_bulk_memory: false,
448 wasm_reference_types: false,
449 wasm_simd: false,
450 epoch_interruption: false,
451 },
452 }
453}
454
455pub fn create_wasm_runtime_with_code<H>(
457 wasm_method: WasmExecutionMethod,
458 heap_alloc_strategy: HeapAllocStrategy,
459 blob: RuntimeBlob,
460 allow_missing_func_imports: bool,
461 cache_path: Option<&Path>,
462) -> Result<Box<dyn WasmModule>, WasmError>
463where
464 H: HostFunctions,
465{
466 if let Some(blob) = blob.as_polkavm_blob() {
467 return sc_executor_polkavm::create_runtime::<H>(blob)
468 .map(|pre| -> Box<dyn WasmModule> { Box::new(pre) });
469 }
470
471 match wasm_method {
472 WasmExecutionMethod::Compiled { instantiation_strategy } => {
473 sc_executor_wasmtime::create_runtime::<H>(
474 blob,
475 wasmtime_config(
476 heap_alloc_strategy,
477 instantiation_strategy,
478 allow_missing_func_imports,
479 cache_path,
480 ),
481 )
482 .map(|runtime| -> Box<dyn WasmModule> { Box::new(runtime) })
483 },
484 }
485}
486
487fn create_wasm_runtimes<H>(
490 wasm_method: WasmExecutionMethod,
491 heap_alloc_strategy: HeapAllocStrategy,
492 blob: RuntimeBlob,
493 allow_missing_func_imports: bool,
494 cache_path: Option<&Path>,
495) -> Result<(Box<dyn WasmModule>, InterruptibleModule), WasmError>
496where
497 H: HostFunctions,
498{
499 if let Some(program_blob) = blob.as_polkavm_blob() {
500 static POLKAVM_TIMEOUT_WARN: std::sync::Once = std::sync::Once::new();
501 POLKAVM_TIMEOUT_WARN.call_once(|| {
502 tracing::warn!(
503 target: "wasm-runtime",
504 "PolkaVM does not support execution timeouts; runtime calls with a timeout will \
505 run uncapped",
506 );
507 });
508
509 let pre = sc_executor_polkavm::create_runtime::<H>(program_blob)?;
511 let module: Box<dyn WasmModule> = Box::new(pre.clone());
512 let interruptible_module =
513 InterruptibleModule::new(move || Ok(Box::new(pre) as Box<dyn WasmModule>));
514
515 return Ok((module, interruptible_module));
516 }
517
518 let interruptible_blob = blob.clone();
519 let runtime = create_wasm_runtime_with_code::<H>(
520 wasm_method,
521 heap_alloc_strategy,
522 blob,
523 allow_missing_func_imports,
524 cache_path,
525 )?;
526
527 let WasmExecutionMethod::Compiled { instantiation_strategy } = wasm_method;
528
529 let mut config = wasmtime_config(
532 heap_alloc_strategy,
533 instantiation_strategy,
534 allow_missing_func_imports,
535 cache_path,
536 );
537 config.semantics.epoch_interruption = true;
538
539 let interruptible_module = InterruptibleModule::new(move || {
540 let time = std::time::Instant::now();
541
542 let result = sc_executor_wasmtime::create_runtime::<H>(interruptible_blob, config)
543 .map(|runtime| -> Box<dyn WasmModule> { Box::new(runtime) });
544
545 match result {
546 Ok(_) => {
547 tracing::debug!(
548 target: "wasm-runtime",
549 "Prepared new interruptible runtime in {} ms.",
550 time.elapsed().as_millis(),
551 );
552 },
553 Err(ref err) => {
554 tracing::warn!(
555 target: "wasm-runtime",
556 error = ?err,
557 "Cannot create an interruptible runtime",
558 );
559 },
560 }
561
562 result
563 });
564
565 Ok((runtime, interruptible_module))
566}
567
568fn decode_version(mut version: &[u8]) -> Result<RuntimeVersion, WasmError> {
569 Decode::decode(&mut version).map_err(|_| {
570 WasmError::Instantiation(
571 "failed to decode \"Core_version\" result using old runtime version".into(),
572 )
573 })
574}
575
576fn decode_runtime_apis(apis: &[u8]) -> Result<Vec<([u8; 8], u32)>, WasmError> {
577 use sp_api::RUNTIME_API_INFO_SIZE;
578
579 apis.chunks(RUNTIME_API_INFO_SIZE)
580 .map(|chunk| {
581 <[u8; RUNTIME_API_INFO_SIZE]>::try_from(chunk)
584 .map(sp_api::deserialize_runtime_api_info)
585 .map_err(|_| WasmError::Other("a clipped runtime api info declaration".to_owned()))
586 })
587 .collect::<Result<Vec<_>, WasmError>>()
588}
589
590pub fn read_embedded_version(blob: &RuntimeBlob) -> Result<Option<RuntimeVersion>, WasmError> {
596 if let Some(mut version_section) = blob.custom_section_contents("runtime_version") {
597 let apis = blob
598 .custom_section_contents("runtime_apis")
599 .map(decode_runtime_apis)
600 .transpose()?
601 .map(Into::into);
602
603 let core_version = apis.as_ref().and_then(sp_version::core_version_from_apis);
604 let mut decoded_version = sp_version::RuntimeVersion::decode_with_version_hint(
609 &mut version_section,
610 core_version,
611 )
612 .map_err(|_| WasmError::Instantiation("failed to decode version section".into()))?;
613
614 if let Some(apis) = apis {
615 decoded_version.apis = apis;
616 }
617
618 Ok(Some(decoded_version))
619 } else {
620 Ok(None)
621 }
622}
623
624fn create_versioned_wasm_runtime<H>(
625 code: &[u8],
626 ext: &mut dyn Externalities,
627 wasm_method: WasmExecutionMethod,
628 heap_alloc_strategy: HeapAllocStrategy,
629 allow_missing_func_imports: bool,
630 max_instances: usize,
631 cache_path: Option<&Path>,
632) -> Result<VersionedRuntime, WasmError>
633where
634 H: HostFunctions,
635{
636 let blob = sc_executor_common::runtime_blob::RuntimeBlob::uncompress_if_needed(code)?;
639
640 let mut version = read_embedded_version(&blob)?;
644
645 let (runtime, interruptible_module) = create_wasm_runtimes::<H>(
646 wasm_method,
647 heap_alloc_strategy,
648 blob,
649 allow_missing_func_imports,
650 cache_path,
651 )?;
652
653 if version.is_none() {
656 let version_result = {
658 let mut ext = AssertUnwindSafe(ext);
661
662 let runtime = AssertUnwindSafe(runtime.as_ref());
665 crate::executor::with_externalities_safe(&mut **ext, move || {
666 runtime.new_instance(heap_alloc_strategy)?.call("Core_version".into(), &[])
667 })
668 .map_err(|_| WasmError::Instantiation("panic in call to get runtime version".into()))?
669 };
670
671 if let Ok(version_buf) = version_result {
672 version = Some(decode_version(&version_buf)?)
673 }
674 }
675
676 interruptible_module.compile_in_background();
679
680 let mut instances = Vec::with_capacity(max_instances);
681 instances.resize_with(max_instances, || Mutex::new(None));
682
683 Ok(VersionedRuntime { module: runtime, interruptible_module, version, instances })
684}
685
686#[cfg(test)]
687mod tests {
688 extern crate alloc;
689
690 use super::*;
691 use alloc::borrow::Cow;
692 use codec::Encode;
693 use sp_api::{Core, RuntimeApiInfo};
694 use sp_version::{create_apis_vec, RuntimeVersion};
695 use sp_wasm_interface::HostFunctions;
696 use substrate_test_runtime::Block;
697
698 #[derive(Encode)]
699 pub struct OldRuntimeVersion {
700 pub spec_name: Cow<'static, str>,
701 pub impl_name: Cow<'static, str>,
702 pub authoring_version: u32,
703 pub spec_version: u32,
704 pub impl_version: u32,
705 pub apis: sp_version::ApisVec,
706 }
707
708 #[test]
709 fn host_functions_are_equal() {
710 let host_functions = sp_io::SubstrateHostFunctions::host_functions();
711
712 let equal = &host_functions[..] == &host_functions[..];
713 assert!(equal, "Host functions are not equal");
714 }
715
716 #[test]
717 fn old_runtime_version_decodes() {
718 let old_runtime_version = OldRuntimeVersion {
719 spec_name: "test".into(),
720 impl_name: "test".into(),
721 authoring_version: 1,
722 spec_version: 1,
723 impl_version: 1,
724 apis: create_apis_vec!([(<dyn Core::<Block>>::ID, 1)]),
725 };
726
727 let version = decode_version(&old_runtime_version.encode()).unwrap();
728 assert_eq!(1, version.transaction_version);
729 assert_eq!(0, version.system_version);
730 }
731
732 #[test]
733 fn old_runtime_version_decodes_fails_with_version_3() {
734 let old_runtime_version = OldRuntimeVersion {
735 spec_name: "test".into(),
736 impl_name: "test".into(),
737 authoring_version: 1,
738 spec_version: 1,
739 impl_version: 1,
740 apis: create_apis_vec!([(<dyn Core::<Block>>::ID, 3)]),
741 };
742
743 decode_version(&old_runtime_version.encode()).unwrap_err();
744 }
745
746 #[test]
747 fn new_runtime_version_decodes() {
748 let old_runtime_version = RuntimeVersion {
749 spec_name: "test".into(),
750 impl_name: "test".into(),
751 authoring_version: 1,
752 spec_version: 1,
753 impl_version: 1,
754 apis: create_apis_vec!([(<dyn Core::<Block>>::ID, 3)]),
755 transaction_version: 3,
756 system_version: 4,
757 };
758
759 let version = decode_version(&old_runtime_version.encode()).unwrap();
760 assert_eq!(3, version.transaction_version);
761 assert_eq!(0, version.system_version);
762
763 let old_runtime_version = RuntimeVersion {
764 spec_name: "test".into(),
765 impl_name: "test".into(),
766 authoring_version: 1,
767 spec_version: 1,
768 impl_version: 1,
769 apis: create_apis_vec!([(<dyn Core::<Block>>::ID, 4)]),
770 transaction_version: 3,
771 system_version: 4,
772 };
773
774 let version = decode_version(&old_runtime_version.encode()).unwrap();
775 assert_eq!(3, version.transaction_version);
776 assert_eq!(4, version.system_version);
777 }
778
779 #[test]
780 fn embed_runtime_version_works() {
781 let wasm = sp_maybe_compressed_blob::decompress(
782 substrate_test_runtime::wasm_binary_unwrap(),
783 sp_maybe_compressed_blob::CODE_BLOB_BOMB_LIMIT,
784 )
785 .expect("Decompressing works");
786 let runtime_version = RuntimeVersion {
787 spec_name: "test_replace".into(),
788 impl_name: "test_replace".into(),
789 authoring_version: 100,
790 spec_version: 100,
791 impl_version: 100,
792 apis: create_apis_vec!([(<dyn Core::<Block>>::ID, 4)]),
793 transaction_version: 100,
794 system_version: 1,
795 };
796
797 let embedded = sp_version::embed::embed_runtime_version(&wasm, runtime_version.clone())
798 .expect("Embedding works");
799
800 let blob = RuntimeBlob::new(&embedded).expect("Embedded blob is valid");
801 let read_version = read_embedded_version(&blob)
802 .ok()
803 .flatten()
804 .expect("Reading embedded version works");
805
806 assert_eq!(runtime_version, read_version);
807 }
808}