1use 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#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
33pub enum WasmExecutionMethod {
34 Compiled {
36 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 code_hash: Vec<u8>,
54 wasm_method: WasmExecutionMethod,
56 heap_alloc_strategy: HeapAllocStrategy,
58}
59
60struct VersionedRuntime {
62 module: Box<dyn WasmModule>,
64 version: Option<RuntimeVersion>,
66
67 instances: Vec<Mutex<Option<Box<dyn WasmInstance>>>>,
71}
72
73impl VersionedRuntime {
74 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 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 let mut instance = self.module.new_instance()?;
133
134 f(&*self.module, &mut *instance, self.version.as_ref(), ext)
135 },
136 }
137 }
138}
139
140pub struct RuntimeCache {
152 runtimes: Mutex<LruMap<VersionedRuntimeId, Arc<VersionedRuntime>>>,
156 max_runtime_instances: usize,
158 cache_path: Option<PathBuf>,
159}
160
161impl RuntimeCache {
162 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 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(); 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 runtimes.insert(versioned_runtime_id, versioned_runtime.clone());
267
268 versioned_runtime
269 };
270
271 drop(runtimes);
273
274 Ok(versioned_runtime.with_instance(ext, f))
275 }
276}
277
278pub 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 <[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
340pub 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 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 let blob = crate::executor::common::runtime_blob::RuntimeBlob::uncompress_if_needed(code)?;
389
390 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 version.is_none() {
406 let version_result = {
408 let mut ext = AssertUnwindSafe(ext);
411
412 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}