diff --git a/runtime/near-vm-errors/src/lib.rs b/runtime/near-vm-errors/src/lib.rs index 63c032608ff..8d524dd0d49 100644 --- a/runtime/near-vm-errors/src/lib.rs +++ b/runtime/near-vm-errors/src/lib.rs @@ -12,27 +12,27 @@ use std::io; /// /// See the doc comment on `VMResult` for an explanation what the difference /// between this and a `FunctionCallError` is. -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub enum VMRunnerError { /// An error that is caused by an operation on an inconsistent state. /// E.g. an integer overflow by using a value from the given context. + #[error("{0}")] InconsistentStateError(InconsistentStateError), /// Error caused by caching. - CacheError(CacheError), + #[error("cache error: {0}")] + CacheError(#[from] CacheError), + /// Error (eg, resource exhausting) when loading a successfully compiled + /// contract into executable memory. + #[error("loading error: {0}")] + LoadingError(String), /// Type erased error from `External` trait implementation. + #[error("external error")] ExternalError(AnyError), /// Non-deterministic error. + #[error("non-deterministic error during contract execution: {0}")] Nondeterministic(String), - WasmUnknownError { - debug_message: String, - }, - /// Error when requiring an unavailable feature of the WASM compiler. - /// - /// The only place this gets emitted today is when calling `precompile` - /// in wasmtime runner. - UnsupportedCompiler { - debug_message: String, - }, + #[error("unknown error during contract execution: {debug_message}")] + WasmUnknownError { debug_message: String }, } /// Permitted errors that cause a function call to fail gracefully. @@ -416,25 +416,6 @@ impl fmt::Display for MethodResolveError { } } -impl fmt::Display for VMRunnerError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - match self { - VMRunnerError::ExternalError(_err) => write!(f, "Serialized ExternalError"), - VMRunnerError::InconsistentStateError(err) => fmt::Display::fmt(err, f), - VMRunnerError::CacheError(err) => write!(f, "Cache error: {:?}", err), - VMRunnerError::WasmUnknownError { debug_message } => { - write!(f, "Unknown error during Wasm contract execution: {}", debug_message) - } - VMRunnerError::Nondeterministic(msg) => { - write!(f, "Nondeterministic error during contract execution: {}", msg) - } - VMRunnerError::UnsupportedCompiler { debug_message } => { - write!(f, "Unsupported compiler: {debug_message}") - } - } - } -} - impl std::fmt::Display for InconsistentStateError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { match self { diff --git a/runtime/near-vm-runner/src/cache.rs b/runtime/near-vm-runner/src/cache.rs index d2ae610e366..1a73aca20aa 100644 --- a/runtime/near-vm-runner/src/cache.rs +++ b/runtime/near-vm-runner/src/cache.rs @@ -10,11 +10,6 @@ use std::collections::HashMap; use std::fmt; use std::sync::{Arc, Mutex}; -#[cfg(target_arch = "x86_64")] -use crate::prepare; -#[cfg(target_arch = "x86_64")] -use near_vm_errors::{FunctionCallError, VMRunnerError}; - #[derive(Debug, Clone, BorshSerialize)] enum ContractCacheKey { _Version1, @@ -60,27 +55,6 @@ pub fn get_contract_cache_key( CryptoHash::hash_borsh(key) } -#[cfg(target_arch = "x86_64")] -fn cache_error( - error: &CompilationError, - key: &CryptoHash, - cache: &dyn CompiledContractCache, -) -> Result<(), CacheError> { - let record = CompiledContract::CompileModuleError(error.clone()); - cache.put(key, record).map_err(CacheError::ReadError) -} - -#[cfg(target_arch = "x86_64")] -pub fn into_vm_result( - res: Result, CacheError>, -) -> Result, VMRunnerError> { - match res { - Ok(Ok(it)) => Ok(Ok(it)), - Ok(Err(err)) => Ok(Err(FunctionCallError::CompilationError(err))), - Err(cache_error) => Err(VMRunnerError::CacheError(cache_error)), - } -} - #[derive(Default)] pub struct MockCompiledContractCache { store: Arc>>, @@ -111,287 +85,32 @@ impl fmt::Debug for MockCompiledContractCache { } } +/// Size of in-memory cache for compiled and loaded contracts. #[cfg(all(not(feature = "no_cache"), target_arch = "x86_64"))] -const CACHE_SIZE: usize = 128; - -#[cfg(all(feature = "wasmer0_vm", not(feature = "no_cache"), target_arch = "x86_64"))] -static WASMER_CACHE: once_cell::sync::Lazy< - near_cache::SyncLruCache>, -> = once_cell::sync::Lazy::new(|| near_cache::SyncLruCache::new(CACHE_SIZE)); - -#[cfg(all(feature = "wasmer2_vm", not(feature = "no_cache"), target_arch = "x86_64"))] -static WASMER2_CACHE: once_cell::sync::Lazy< - near_cache::SyncLruCache< - CryptoHash, - Result, - >, -> = once_cell::sync::Lazy::new(|| near_cache::SyncLruCache::new(CACHE_SIZE)); - -#[cfg(all(feature = "wasmer0_vm", target_arch = "x86_64"))] -pub mod wasmer0_cache { - use super::*; - use near_vm_errors::CompilationError; - use wasmer_runtime::{compiler_for_backend, Backend}; - use wasmer_runtime_core::cache::Artifact; - use wasmer_runtime_core::load_cache_with; - - pub(crate) fn compile_module( - code: &[u8], - config: &VMConfig, - ) -> Result { - let _span = tracing::debug_span!(target: "vm", "compile_module").entered(); - - let prepared_code = - prepare::prepare_contract(code, config).map_err(CompilationError::PrepareError)?; - wasmer_runtime::compile(&prepared_code).map_err(|err| match err { - wasmer_runtime::error::CompileError::ValidationError { .. } => { - CompilationError::WasmerCompileError { msg: err.to_string() } - } - // NOTE: Despite the `InternalError` name, this failure occurs if - // the input `code` is invalid wasm. - wasmer_runtime::error::CompileError::InternalError { .. } => { - CompilationError::WasmerCompileError { msg: err.to_string() } - } - }) - } - - pub(crate) fn compile_and_serialize_wasmer( - wasm_code: &[u8], - config: &VMConfig, - key: &CryptoHash, - cache: &dyn CompiledContractCache, - ) -> Result, CacheError> { - let _span = tracing::debug_span!(target: "vm", "compile_and_serialize_wasmer").entered(); - - let module = match compile_module(wasm_code, config) { - Ok(module) => module, - Err(err) => { - cache_error(&err, key, cache)?; - return Ok(Err(err)); - } - }; - - let code = module - .cache() - .and_then(|it| it.serialize()) - .map_err(|_e| CacheError::SerializationError { hash: key.0 })?; - let record = CompiledContract::Code(code); - cache.put(key, record).map_err(CacheError::WriteError)?; - Ok(Ok(module)) - } - - /// Deserializes contract or error from the binary data. Signature means that we could either - /// return module or cached error, which both considered to be `Ok()`, or encounter an error during - /// the deserialization process. - fn deserialize_wasmer( - record: CompiledContract, - ) -> Result, CacheError> { - let _span = tracing::debug_span!(target: "vm", "deserialize_wasmer").entered(); - - let serialized_artifact = match record { - CompiledContract::CompileModuleError(err) => return Ok(Err(err)), - CompiledContract::Code(code) => code, - }; - let artifact = Artifact::deserialize(serialized_artifact.as_slice()) - .map_err(|_e| CacheError::DeserializationError)?; - unsafe { - let compiler = compiler_for_backend(Backend::Singlepass).unwrap(); - match load_cache_with(artifact, compiler.as_ref()) { - Ok(module) => Ok(Ok(module)), - Err(_) => Err(CacheError::DeserializationError), - } - } - } - - fn compile_module_cached_wasmer_impl( - key: CryptoHash, - wasm_code: &[u8], - config: &VMConfig, - cache: Option<&dyn CompiledContractCache>, - ) -> Result, CacheError> { - match cache { - None => Ok(compile_module(wasm_code, config)), - Some(cache) => match cache.get(&key).map_err(CacheError::ReadError)? { - Some(record) => deserialize_wasmer(record), - None => compile_and_serialize_wasmer(wasm_code, config, &key, cache), - }, - } - } - - pub(crate) fn compile_module_cached_wasmer0( - code: &ContractCode, - config: &VMConfig, - cache: Option<&dyn CompiledContractCache>, - ) -> Result, CacheError> { - let key = get_contract_cache_key(code, VMKind::Wasmer0, config); - - #[cfg(not(feature = "no_cache"))] - return WASMER_CACHE.get_or_try_put(key, |key| { - compile_module_cached_wasmer_impl(*key, code.code(), config, cache) - }); - - #[cfg(feature = "no_cache")] - return compile_module_cached_wasmer_impl(key, code.code(), config, cache); - } -} - -#[cfg(all(feature = "wasmer2_vm", target_arch = "x86_64"))] -pub mod wasmer2_cache { - use crate::wasmer2_runner::{VMArtifact, Wasmer2VM}; - use near_primitives::contract::ContractCode; - use wasmer_engine::Executable; - - use super::*; - - pub(crate) fn compile_module_wasmer2( - vm: &Wasmer2VM, - code: &[u8], - config: &VMConfig, - ) -> Result { - let _span = tracing::debug_span!(target: "vm", "compile_module_wasmer2").entered(); - let prepared_code = - prepare::prepare_contract(code, config).map_err(CompilationError::PrepareError)?; - vm.compile_uncached(&prepared_code) - } - - pub(crate) fn compile_and_serialize_wasmer2( - wasm_code: &[u8], - key: &CryptoHash, - config: &VMConfig, - cache: &dyn CompiledContractCache, - ) -> Result, CacheError> { - let _span = tracing::debug_span!(target: "vm", "compile_and_serialize_wasmer2").entered(); - let vm = Wasmer2VM::new(config.clone()); - let executable = match compile_module_wasmer2(&vm, wasm_code, config) { - Ok(module) => module, - Err(err) => { - cache_error(&err, key, cache)?; - return Ok(Err(err)); - } - }; - let code = - executable.serialize().map_err(|_e| CacheError::SerializationError { hash: key.0 })?; - let record = CompiledContract::Code(code); - cache.put(key, record).map_err(CacheError::WriteError)?; - match vm.engine.load_universal_executable(&executable) { - Ok(artifact) => Ok(Ok(Arc::new(artifact) as _)), - Err(err) => { - let err = CompilationError::WasmerCompileError { msg: err.to_string() }; - cache_error(&err, key, cache)?; - Ok(Err(err)) - } - } - } - - fn deserialize_wasmer2( - record: CompiledContract, - config: &VMConfig, - ) -> Result, CacheError> { - let _span = tracing::debug_span!(target: "vm", "deserialize_wasmer2").entered(); - - let serialized_module = match record { - CompiledContract::CompileModuleError(err) => return Ok(Err(err)), - CompiledContract::Code(code) => code, - }; - unsafe { - // (UN-)SAFETY: the `serialized_module` must have been produced by a prior call to - // `serialize`. - // - // In practice this is not necessarily true. One could have forgotten to change the - // cache key when upgrading the version of the wasmer library or the database could - // have had its data corrupted while at rest. - // - // There should definitely be some validation in wasmer to ensure we load what we think - // we load. - let artifact = Wasmer2VM::new(config.clone()) - .deserialize(&serialized_module) - .map_err(|_| CacheError::DeserializationError)?; - Ok(Ok(artifact)) - } - } - - fn compile_module_cached_wasmer2_impl( - key: CryptoHash, - code: &ContractCode, - config: &VMConfig, - cache: Option<&dyn CompiledContractCache>, - ) -> Result, CacheError> { - let vm = Wasmer2VM::new(config.clone()); - match cache { - None => Ok(compile_module_wasmer2(&vm, code.code(), config).and_then(|executable| { - vm.engine - .load_universal_executable(&executable) - .map(|v| Arc::new(v) as _) - .map_err(|err| panic!("could not load the executable: {}", err.to_string())) - })), - Some(cache) => match cache.get(&key).map_err(CacheError::ReadError)? { - Some(record) => deserialize_wasmer2(record, config), - None => compile_and_serialize_wasmer2(code.code(), &key, config, cache), - }, - } - } - - pub(crate) fn compile_module_cached_wasmer2( - code: &ContractCode, - config: &VMConfig, - cache: Option<&dyn CompiledContractCache>, - ) -> Result, CacheError> { - let key = get_contract_cache_key(code, VMKind::Wasmer2, config); - - #[cfg(not(feature = "no_cache"))] - return WASMER2_CACHE.get_or_try_put(key, |key| { - compile_module_cached_wasmer2_impl(*key, code, config, cache) - }); - - #[cfg(feature = "no_cache")] - return compile_module_cached_wasmer2_impl(key, code, config, cache); - } -} - -pub fn precompile_contract_vm( - vm_kind: VMKind, - wasm_code: &ContractCode, - config: &VMConfig, - cache: Option<&dyn CompiledContractCache>, -) -> Result, CacheError> { - let cache = match cache { - None => return Ok(Ok(ContractPrecompilatonResult::CacheNotAvailable)), - Some(it) => it, - }; - let key = get_contract_cache_key(wasm_code, vm_kind, config); - // Check if we already cached with such a key. - if cache.has(&key).map_err(CacheError::ReadError)? { - return Ok(Ok(ContractPrecompilatonResult::ContractAlreadyInCache)); - } - match vm_kind { - #[cfg(all(feature = "wasmer0_vm", target_arch = "x86_64"))] - VMKind::Wasmer0 => { - Ok(wasmer0_cache::compile_and_serialize_wasmer(wasm_code.code(), config, &key, cache)? - .map(|_| ContractPrecompilatonResult::ContractCompiled)) - } - #[cfg(not(all(feature = "wasmer0_vm", target_arch = "x86_64")))] - VMKind::Wasmer0 => panic!("Wasmer0 is not enabled!"), - #[cfg(all(feature = "wasmer2_vm", target_arch = "x86_64"))] - VMKind::Wasmer2 => { - Ok(wasmer2_cache::compile_and_serialize_wasmer2(wasm_code.code(), &key, config, cache)? - .map(|_| ContractPrecompilatonResult::ContractCompiled)) - } - #[cfg(not(all(feature = "wasmer2_vm", target_arch = "x86_64")))] - VMKind::Wasmer2 => panic!("Wasmer2 is not enabled!"), - VMKind::Wasmtime => Ok(Ok(ContractPrecompilatonResult::CacheNotAvailable)), - } -} +pub(crate) const CACHE_SIZE: usize = 128; /// Precompiles contract for the current default VM, and stores result to the cache. /// Returns `Ok(true)` if compiled code was added to the cache, and `Ok(false)` if element /// is already in the cache, or if cache is `None`. pub fn precompile_contract( - wasm_code: &ContractCode, + code: &ContractCode, config: &VMConfig, current_protocol_version: ProtocolVersion, cache: Option<&dyn CompiledContractCache>, ) -> Result, CacheError> { let _span = tracing::debug_span!(target: "vm", "precompile_contract").entered(); let vm_kind = VMKind::for_protocol_version(current_protocol_version); - precompile_contract_vm(vm_kind, wasm_code, config, cache) + let runtime = vm_kind + .runtime(config.clone()) + .unwrap_or_else(|| panic!("the {vm_kind:?} runtime has not been enabled at compile time")); + let cache = match cache { + Some(it) => it, + None => return Ok(Ok(ContractPrecompilatonResult::CacheNotAvailable)), + }; + let key = get_contract_cache_key(code, vm_kind, config); + // Check if we already cached with such a key. + if cache.has(&key).map_err(CacheError::ReadError)? { + return Ok(Ok(ContractPrecompilatonResult::ContractAlreadyInCache)); + } + runtime.precompile(code, cache) } diff --git a/runtime/near-vm-runner/src/lib.rs b/runtime/near-vm-runner/src/lib.rs index 35c8495bc04..a29bbbe767b 100644 --- a/runtime/near-vm-runner/src/lib.rs +++ b/runtime/near-vm-runner/src/lib.rs @@ -20,9 +20,7 @@ mod wasmtime_runner; pub use near_vm_logic::with_ext_cost_counter; -pub use cache::{ - get_contract_cache_key, precompile_contract, precompile_contract_vm, MockCompiledContractCache, -}; +pub use cache::{get_contract_cache_key, precompile_contract, MockCompiledContractCache}; pub use runner::{run, VM}; /// This is public for internal experimentation use only, and should otherwise be considered an diff --git a/runtime/near-vm-runner/src/runner.rs b/runtime/near-vm-runner/src/runner.rs index b8b1ff6f8aa..016a63d3b7f 100644 --- a/runtime/near-vm-runner/src/runner.rs +++ b/runtime/near-vm-runner/src/runner.rs @@ -1,11 +1,11 @@ +use crate::errors::ContractPrecompilatonResult; use crate::vm_kind::VMKind; use near_primitives::config::VMConfig; use near_primitives::contract::ContractCode; -use near_primitives::hash::CryptoHash; use near_primitives::runtime::fees::RuntimeFeesConfig; use near_primitives::types::CompiledContractCache; use near_primitives::version::ProtocolVersion; -use near_vm_errors::{FunctionCallError, VMRunnerError}; +use near_vm_errors::{CacheError, CompilationError, VMRunnerError}; use near_vm_logic::types::PromiseResult; use near_vm_logic::{External, VMContext, VMOutcome}; @@ -24,7 +24,7 @@ use near_vm_logic::{External, VMContext, VMOutcome}; /// (See also `PartialExecutionStatus`.) /// Similarly, the gas values on `VMOutcome` must be the exact same on all /// validators, even when a guest error occurs, or else their state will diverge. -pub(crate) type VMResult = Result; +pub(crate) type VMResult = Result; /// Validate and run the specified contract. /// @@ -64,9 +64,9 @@ pub fn run( ) .entered(); - let runtime = vm_kind.runtime(wasm_config.clone()).unwrap_or_else(|| { - panic!("the {:?} runtime has not been enabled at compile time", vm_kind) - }); + let runtime = vm_kind + .runtime(wasm_config.clone()) + .unwrap_or_else(|| panic!("the {vm_kind:?} runtime has not been enabled at compile time")); let outcome = runtime.run( code, @@ -118,15 +118,9 @@ pub trait VM { /// precompilation step. fn precompile( &self, - code: &[u8], - code_hash: &CryptoHash, + code: &ContractCode, cache: &dyn CompiledContractCache, - ) -> Result, VMRunnerError>; - - /// Verify the `code` contract can be compiled with this `VM`. - /// - /// This is intended primarily for testing purposes. - fn check_compile(&self, code: &[u8]) -> bool; + ) -> Result, CacheError>; } impl VMKind { diff --git a/runtime/near-vm-runner/src/tests/cache.rs b/runtime/near-vm-runner/src/tests/cache.rs index 78214c4d383..836f6023322 100644 --- a/runtime/near-vm-runner/src/tests/cache.rs +++ b/runtime/near-vm-runner/src/tests/cache.rs @@ -128,12 +128,12 @@ fn test_wasmer2_artifact_output_stability() { ]; let mut got_compiled_hashes = Vec::with_capacity(seeds.len()); for seed in seeds { - let contract = near_test_contracts::arbitrary_contract(seed); + let contract = ContractCode::new(near_test_contracts::arbitrary_contract(seed), None); let config = VMConfig::test(); - let prepared_code = prepare::prepare_contract(&contract, &config).unwrap(); + let prepared_code = prepare::prepare_contract(contract.code(), &config).unwrap(); let mut hasher = StableHasher::new(); - (&contract, &prepared_code).hash(&mut hasher); + (&contract.code(), &prepared_code).hash(&mut hasher); got_prepared_hashes.push(hasher.finish()); let mut features = CpuFeature::set(); @@ -141,7 +141,7 @@ fn test_wasmer2_artifact_output_stability() { let triple = "x86_64-unknown-linux-gnu".parse().unwrap(); let target = Target::new(triple, features); let vm = Wasmer2VM::new_for_target(config, target); - let artifact = vm.compile_uncached(&prepared_code).unwrap(); + let artifact = vm.compile_uncached(&contract).unwrap(); let serialized = artifact.serialize().unwrap(); let mut hasher = StableHasher::new(); serialized.hash(&mut hasher); diff --git a/runtime/near-vm-runner/src/tests/fuzzers.rs b/runtime/near-vm-runner/src/tests/fuzzers.rs index d25dd0e9fed..527d2ce6088 100644 --- a/runtime/near-vm-runner/src/tests/fuzzers.rs +++ b/runtime/near-vm-runner/src/tests/fuzzers.rs @@ -181,11 +181,7 @@ fn wasmer2_is_reproducible() { let mut first_hash = None; for _ in 0..3 { let vm = Wasmer2VM::new(config.clone()); - let prepared_code = match crate::prepare::prepare_contract(code.code(), &config) { - Ok(c) => c, - Err(_) => return, - }; - let exec = match vm.compile_uncached(&prepared_code) { + let exec = match vm.compile_uncached(&code) { Ok(e) => e, Err(_) => return, }; diff --git a/runtime/near-vm-runner/src/wasmer2_runner.rs b/runtime/near-vm-runner/src/wasmer2_runner.rs index 41ff8a39893..026bd5aabfa 100644 --- a/runtime/near-vm-runner/src/wasmer2_runner.rs +++ b/runtime/near-vm-runner/src/wasmer2_runner.rs @@ -1,14 +1,16 @@ -use crate::cache::into_vm_result; +use crate::errors::ContractPrecompilatonResult; use crate::imports::wasmer2::Wasmer2Imports; -use crate::prepare::WASM_FEATURES; -use crate::{cache, imports}; +use crate::internal::VMKind; +use crate::prepare::{self, WASM_FEATURES}; +use crate::runner::VMResult; +use crate::{get_contract_cache_key, imports}; use memoffset::offset_of; use near_primitives::contract::ContractCode; use near_primitives::runtime::fees::RuntimeFeesConfig; -use near_primitives::types::CompiledContractCache; +use near_primitives::types::{CompiledContract, CompiledContractCache}; use near_stable_hasher::StableHasher; use near_vm_errors::{ - CompilationError, FunctionCallError, MethodResolveError, VMRunnerError, WasmTrap, + CacheError, CompilationError, FunctionCallError, MethodResolveError, VMRunnerError, WasmTrap, }; use near_vm_logic::gas_counter::FastGasCounter; use near_vm_logic::types::{PromiseResult, ProtocolVersion}; @@ -17,8 +19,10 @@ use std::hash::{Hash, Hasher}; use std::mem::size_of; use std::sync::Arc; use wasmer_compiler_singlepass::Singlepass; -use wasmer_engine::{DeserializeError, Engine}; -use wasmer_engine_universal::{Universal, UniversalEngine, UniversalExecutableRef}; +use wasmer_engine::{Engine, Executable}; +use wasmer_engine_universal::{ + Universal, UniversalEngine, UniversalExecutable, UniversalExecutableRef, +}; use wasmer_types::{Features, FunctionIndex, InstanceConfig, MemoryType, Pages, WASM_PAGE_SIZE}; use wasmer_vm::{ Artifact, Instantiatable, LinearMemory, LinearTable, Memory, MemoryStyle, TrapCode, VMMemory, @@ -266,26 +270,133 @@ impl Wasmer2VM { pub(crate) fn compile_uncached( &self, - code: &[u8], - ) -> Result { + code: &ContractCode, + ) -> Result { + let _span = tracing::debug_span!(target: "vm", "Wasmer2VM::compile_uncached").entered(); + let prepared_code = prepare::prepare_contract(code.code(), &self.config) + .map_err(CompilationError::PrepareError)?; + self.engine - .validate(code) + .validate(&prepared_code) .map_err(|e| CompilationError::WasmerCompileError { msg: e.to_string() })?; - self.engine - .compile_universal(code, &self) - .map_err(|e| CompilationError::WasmerCompileError { msg: e.to_string() }) + let executable = self + .engine + .compile_universal(&prepared_code, &self) + .map_err(|e| CompilationError::WasmerCompileError { msg: e.to_string() })?; + Ok(executable) } - pub(crate) unsafe fn deserialize( + fn compile_and_cache( &self, - serialized: &[u8], - ) -> Result { - let executable = UniversalExecutableRef::deserialize(serialized)?; - let artifact = self - .engine - .load_universal_executable_ref(&executable) - .map_err(|e| DeserializeError::Compiler(e))?; - Ok(Arc::new(artifact)) + code: &ContractCode, + cache: Option<&dyn CompiledContractCache>, + ) -> Result, CacheError> { + let executable_or_error = self.compile_uncached(code); + let key = get_contract_cache_key(code, VMKind::Wasmer2, &self.config); + + if let Some(cache) = cache { + let record = match &executable_or_error { + Ok(executable) => { + let code = executable + .serialize() + .map_err(|_e| CacheError::SerializationError { hash: key.0 })?; + CompiledContract::Code(code) + } + Err(err) => CompiledContract::CompileModuleError(err.clone()), + }; + cache.put(&key, record).map_err(CacheError::WriteError)?; + } + + Ok(executable_or_error) + } + + fn compile_and_load( + &self, + code: &ContractCode, + cache: Option<&dyn CompiledContractCache>, + ) -> VMResult> { + // A bit of a tricky logic ahead! We need to deal with two levels of + // caching: + // * `cache` stores compiled machine code in the database + // * `MEM_CACHE` below holds in-memory cache of loaded contracts + // + // Caches also cache _compilation_ errors, so that we don't have to + // re-parse invalid code (invalid code, in a sense, is a normal + // outcome). And `cache`, being a database, can fail with an `io::Error`. + let _span = tracing::debug_span!(target: "vm", "Wasmer2VM::compile_and_load").entered(); + + let key = get_contract_cache_key(code, VMKind::Wasmer2, &self.config); + + let compile_or_read_from_cache = || -> VMResult> { + let _span = tracing::debug_span!(target: "vm", "Wasmer2VM::compile_or_read_from_cache") + .entered(); + let cache_record = cache + .map(|cache| cache.get(&key)) + .transpose() + .map_err(CacheError::ReadError)? + .flatten(); + + let stored_artifact: Option = match cache_record { + None => None, + Some(CompiledContract::CompileModuleError(err)) => return Ok(Err(err)), + Some(CompiledContract::Code(serialized_module)) => { + let _span = + tracing::debug_span!(target: "vm", "Wasmer2VM::read_from_cache").entered(); + unsafe { + // (UN-)SAFETY: the `serialized_module` must have been produced by a prior call to + // `serialize`. + // + // In practice this is not necessarily true. One could have forgotten to change the + // cache key when upgrading the version of the wasmer library or the database could + // have had its data corrupted while at rest. + // + // There should definitely be some validation in wasmer to ensure we load what we think + // we load. + let executable = UniversalExecutableRef::deserialize(&serialized_module) + .map_err(|_| CacheError::DeserializationError)?; + let artifact = self + .engine + .load_universal_executable_ref(&executable) + .map(Arc::new) + .map_err(|err| VMRunnerError::LoadingError(err.to_string()))?; + Some(artifact) + } + } + }; + + let artifact: VMArtifact = match stored_artifact { + Some(it) => it, + None => { + let executable_or_error = self.compile_and_cache(code, cache)?; + match executable_or_error { + Ok(executable) => self + .engine + .load_universal_executable(&executable) + .map(Arc::new) + .map_err(|err| VMRunnerError::LoadingError(err.to_string()))?, + Err(it) => return Ok(Err(it)), + } + } + }; + + Ok(Ok(artifact)) + }; + + #[cfg(feature = "no_cache")] + return compile_or_read_from_cache(); + + #[cfg(not(feature = "no_cache"))] + return { + static MEM_CACHE: once_cell::sync::Lazy< + near_cache::SyncLruCache< + near_primitives::hash::CryptoHash, + Result, + >, + > = once_cell::sync::Lazy::new(|| { + near_cache::SyncLruCache::new(crate::cache::CACHE_SIZE) + }); + MEM_CACHE.get_or_try_put(key, |_key| compile_or_read_from_cache()) + }; } fn run_method( @@ -501,12 +612,11 @@ impl crate::runner::VM for Wasmer2VM { return Ok(VMOutcome::abort(logic, e)); } - let artifact = - cache::wasmer2_cache::compile_module_cached_wasmer2(code, &self.config, cache); - let artifact = match into_vm_result(artifact)? { + let artifact = self.compile_and_load(code, cache)?; + let artifact = match artifact { Ok(it) => it, Err(err) => { - return Ok(VMOutcome::abort(logic, err)); + return Ok(VMOutcome::abort(logic, FunctionCallError::CompilationError(err))); } }; @@ -535,22 +645,13 @@ impl crate::runner::VM for Wasmer2VM { fn precompile( &self, - code: &[u8], - code_hash: &near_primitives::hash::CryptoHash, + code: &ContractCode, cache: &dyn CompiledContractCache, - ) -> Result, VMRunnerError> { - let result = crate::cache::wasmer2_cache::compile_and_serialize_wasmer2( - code, - code_hash, - &self.config, - cache, - ); - let outcome = into_vm_result(result)?; - Ok(outcome.err()) - } - - fn check_compile(&self, code: &[u8]) -> bool { - self.compile_uncached(code).is_ok() + ) -> Result, near_vm_errors::CacheError> + { + Ok(self + .compile_and_cache(code, Some(cache))? + .map(|_| ContractPrecompilatonResult::ContractCompiled)) } } diff --git a/runtime/near-vm-runner/src/wasmer_runner.rs b/runtime/near-vm-runner/src/wasmer_runner.rs index 5842f65f157..d6f95282acc 100644 --- a/runtime/near-vm-runner/src/wasmer_runner.rs +++ b/runtime/near-vm-runner/src/wasmer_runner.rs @@ -1,23 +1,21 @@ -use crate::cache::into_vm_result; -use crate::errors::IntoVMError; +use crate::errors::{ContractPrecompilatonResult, IntoVMError}; +use crate::internal::VMKind; use crate::memory::WasmerMemory; -use crate::prepare::WASM_FEATURES; -use crate::{cache, imports}; +use crate::prepare; +use crate::runner::VMResult; +use crate::{get_contract_cache_key, imports}; use near_primitives::config::VMConfig; use near_primitives::contract::ContractCode; use near_primitives::runtime::fees::RuntimeFeesConfig; -use near_primitives::types::CompiledContractCache; +use near_primitives::types::{CompiledContract, CompiledContractCache}; use near_primitives::version::ProtocolVersion; use near_vm_errors::{ - CompilationError, FunctionCallError, MethodResolveError, VMRunnerError, WasmTrap, + CacheError, CompilationError, FunctionCallError, MethodResolveError, VMRunnerError, WasmTrap, }; use near_vm_logic::types::PromiseResult; use near_vm_logic::{External, VMContext, VMLogic, VMLogicError, VMOutcome}; use wasmer_runtime::{ImportObject, Module}; -const WASMER_FEATURES: wasmer_runtime::Features = - wasmer_runtime::Features { threads: WASM_FEATURES.threads, simd: WASM_FEATURES.simd }; - fn check_method(module: &Module, method_name: &str) -> Result<(), FunctionCallError> { let info = module.info(); use wasmer_runtime_core::module::ExportIndex::Func; @@ -242,6 +240,125 @@ impl Wasmer0VM { pub(crate) fn new(config: VMConfig) -> Self { Self { config } } + + pub(crate) fn compile_uncached( + &self, + code: &ContractCode, + ) -> Result { + let _span = tracing::debug_span!(target: "vm", "Wasmer0VM::compile_uncached").entered(); + let prepared_code = prepare::prepare_contract(code.code(), &self.config) + .map_err(CompilationError::PrepareError)?; + wasmer_runtime::compile(&prepared_code).map_err(|err| match err { + wasmer_runtime::error::CompileError::ValidationError { .. } => { + CompilationError::WasmerCompileError { msg: err.to_string() } + } + // NOTE: Despite the `InternalError` name, this failure occurs if + // the input `code` is invalid wasm. + wasmer_runtime::error::CompileError::InternalError { .. } => { + CompilationError::WasmerCompileError { msg: err.to_string() } + } + }) + } + + pub(crate) fn compile_and_cache( + &self, + code: &ContractCode, + cache: Option<&dyn CompiledContractCache>, + ) -> Result, CacheError> { + let module_or_error = self.compile_uncached(code); + let key = get_contract_cache_key(code, VMKind::Wasmer0, &self.config); + + if let Some(cache) = cache { + let record = match &module_or_error { + Ok(module) => { + let code = module + .cache() + .and_then(|it| it.serialize()) + .map_err(|_e| CacheError::SerializationError { hash: key.0 })?; + CompiledContract::Code(code) + } + Err(err) => CompiledContract::CompileModuleError(err.clone()), + }; + cache.put(&key, record).map_err(CacheError::WriteError)?; + } + + Ok(module_or_error) + } + + pub(crate) fn compile_and_load( + &self, + code: &ContractCode, + cache: Option<&dyn CompiledContractCache>, + ) -> VMResult> { + let _span = tracing::debug_span!(target: "vm", "Wasmer0VM::compile_and_load").entered(); + + let key = get_contract_cache_key(code, VMKind::Wasmer0, &self.config); + + let compile_or_read_from_cache = + || -> VMResult> { + let _span = + tracing::debug_span!(target: "vm", "Wasmer0VM::compile_or_read_from_cache") + .entered(); + + let cache_record = cache + .map(|cache| cache.get(&key)) + .transpose() + .map_err(CacheError::ReadError)? + .flatten(); + + let stored_module: Option = match cache_record { + None => None, + Some(CompiledContract::CompileModuleError(err)) => return Ok(Err(err)), + Some(CompiledContract::Code(serialized_module)) => { + let _span = + tracing::debug_span!(target: "vm", "Wasmer0VM::read_from_cache") + .entered(); + let artifact = wasmer_runtime_core::cache::Artifact::deserialize( + serialized_module.as_slice(), + ) + .map_err(|_e| CacheError::DeserializationError)?; + unsafe { + let compiler = wasmer_runtime::compiler_for_backend( + wasmer_runtime::Backend::Singlepass, + ) + .unwrap(); + let module = wasmer_runtime_core::load_cache_with( + artifact, + compiler.as_ref(), + ) + .map_err(|err| VMRunnerError::LoadingError(format!("{err:?}")))?; + Some(module) + } + } + }; + + let module = match stored_module { + Some(it) => it, + None => match self.compile_and_cache(code, cache)? { + Ok(it) => it, + Err(it) => return Ok(Err(it)), + }, + }; + + Ok(Ok(module)) + }; + + #[cfg(feature = "no_cache")] + return compile_or_read_from_cache(); + + #[cfg(not(feature = "no_cache"))] + return { + static MEM_CACHE: once_cell::sync::Lazy< + near_cache::SyncLruCache< + near_primitives::hash::CryptoHash, + Result, + >, + > = once_cell::sync::Lazy::new(|| { + near_cache::SyncLruCache::new(crate::cache::CACHE_SIZE) + }); + MEM_CACHE.get_or_try_put(key, |_key| compile_or_read_from_cache()) + }; + } } impl crate::runner::VM for Wasmer0VM { @@ -295,8 +412,8 @@ impl crate::runner::VM for Wasmer0VM { } // TODO: consider using get_module() here, once we'll go via deployment path. - let module = cache::wasmer0_cache::compile_module_cached_wasmer0(code, &self.config, cache); - let module = match into_vm_result(module)? { + let module = self.compile_and_load(code, cache)?; + let module = match module { Ok(x) => x, // Note on backwards-compatibility: This error used to be an error // without result, later refactored to NOP outcome. Now this returns @@ -305,7 +422,9 @@ impl crate::runner::VM for Wasmer0VM { // version do not have gas costs before reaching this code. (Also // see `test_old_fn_loading_behavior_preserved` for a test that // verifies future changes do not counteract this assumption.) - Err(guest_err) => return Ok(VMOutcome::abort(logic, guest_err)), + Err(err) => { + return Ok(VMOutcome::abort(logic, FunctionCallError::CompilationError(err))) + } }; let result = logic.after_loading_executable(current_protocol_version, code.code().len()); @@ -332,25 +451,12 @@ impl crate::runner::VM for Wasmer0VM { fn precompile( &self, - code: &[u8], - code_hash: &near_primitives::hash::CryptoHash, + code: &ContractCode, cache: &dyn CompiledContractCache, - ) -> Result, VMRunnerError> { - let result = crate::cache::wasmer0_cache::compile_and_serialize_wasmer( - code, - &self.config, - code_hash, - cache, - ); - let outcome = into_vm_result(result)?; - Ok(outcome.err()) - } - - fn check_compile(&self, code: &[u8]) -> bool { - wasmer_runtime::compile_with_config( - code, - wasmer_runtime::CompilerConfig { features: WASMER_FEATURES, ..Default::default() }, - ) - .is_ok() + ) -> Result, near_vm_errors::CacheError> + { + Ok(self + .compile_and_cache(code, Some(cache))? + .map(|_| ContractPrecompilatonResult::ContractCompiled)) } } diff --git a/runtime/near-vm-runner/src/wasmtime_runner.rs b/runtime/near-vm-runner/src/wasmtime_runner.rs index 6d53f8dbc37..102e7858aea 100644 --- a/runtime/near-vm-runner/src/wasmtime_runner.rs +++ b/runtime/near-vm-runner/src/wasmtime_runner.rs @@ -1,14 +1,14 @@ -use crate::errors::IntoVMError; +use crate::errors::{ContractPrecompilatonResult, IntoVMError}; use crate::prepare::WASM_FEATURES; use crate::{imports, prepare}; use near_primitives::config::VMConfig; use near_primitives::contract::ContractCode; -use near_primitives::hash::CryptoHash; use near_primitives::runtime::fees::RuntimeFeesConfig; use near_primitives::types::CompiledContractCache; use near_primitives::version::ProtocolVersion; use near_vm_errors::{ - FunctionCallError, MethodResolveError, PrepareError, VMLogicError, VMRunnerError, WasmTrap, + CompilationError, FunctionCallError, MethodResolveError, PrepareError, VMLogicError, + VMRunnerError, WasmTrap, }; use near_vm_logic::types::PromiseResult; use near_vm_logic::{External, MemoryLike, VMContext, VMLogic, VMOutcome}; @@ -290,18 +290,10 @@ impl crate::runner::VM for WasmtimeVM { fn precompile( &self, - _code: &[u8], - _code_hash: &CryptoHash, + _code: &ContractCode, _cache: &dyn CompiledContractCache, - ) -> Result, VMRunnerError> { - Err(VMRunnerError::UnsupportedCompiler { - debug_message: "Precompilation not supported in Wasmtime yet".to_string(), - }) - } - - fn check_compile(&self, code: &[u8]) -> bool { - let mut config = default_config(); - let engine = get_engine(&mut config); - Module::new(&engine, code).is_ok() + ) -> Result, near_vm_errors::CacheError> + { + Ok(Ok(ContractPrecompilatonResult::CacheNotAvailable)) } } diff --git a/runtime/runtime-params-estimator/src/vm_estimator.rs b/runtime/runtime-params-estimator/src/vm_estimator.rs index 0710f809032..785af7a4f27 100644 --- a/runtime/runtime-params-estimator/src/vm_estimator.rs +++ b/runtime/runtime-params-estimator/src/vm_estimator.rs @@ -9,7 +9,6 @@ use near_primitives::version::PROTOCOL_VERSION; use near_store::StoreCompiledContractCache; use near_vm_logic::VMContext; use near_vm_runner::internal::VMKind; -use near_vm_runner::precompile_contract_vm; const CURRENT_ACCOUNT_ID: &str = "alice"; const SIGNER_ACCOUNT_ID: &str = "bob"; @@ -41,13 +40,14 @@ fn measure_contract( vm_kind: VMKind, gas_metric: GasMetric, contract: &ContractCode, - cache: Option<&dyn CompiledContractCache>, + cache: &dyn CompiledContractCache, ) -> GasCost { let config_store = RuntimeConfigStore::new(None); let runtime_config = config_store.get_config(PROTOCOL_VERSION).as_ref(); let vm_config = runtime_config.wasm_config.clone(); let start = GasCost::measure(gas_metric); - let result = precompile_contract_vm(vm_kind, contract, &vm_config, cache); + let vm = vm_kind.runtime(vm_config).unwrap(); + let result = vm.precompile(contract, cache).unwrap(); let end = start.elapsed(); assert!(result.is_ok(), "Compilation failed"); end @@ -80,12 +80,12 @@ fn precompilation_cost( let cache_store1: StoreCompiledContractCache; let cache_store2 = MockCompiledContractCache; let use_store = true; - let cache: Option<&dyn CompiledContractCache> = if use_store { + let cache: &dyn CompiledContractCache = if use_store { let store = near_store::test_utils::create_test_store(); cache_store1 = StoreCompiledContractCache::new(&store); - Some(&cache_store1) + &cache_store1 } else { - Some(&cache_store2) + &cache_store2 }; let mut xs = vec![]; let mut ys = vec![]; @@ -154,7 +154,7 @@ pub(crate) fn compile_single_contract_cost( let store = near_store::test_utils::create_test_store(); let cache = StoreCompiledContractCache::new(&store); - measure_contract(vm_kind, metric, &contract, Some(&cache)) + measure_contract(vm_kind, metric, &contract, &cache) } pub(crate) fn compute_compile_cost_vm( diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index 1f49307cd7a..10f5b18737b 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -138,15 +138,15 @@ pub(crate) fn execute_function_call( metrics::FUNCTION_CALL_PROCESSED_CACHE_ERRORS.with_label_values(&[(&err).into()]).inc(); StorageError::StorageInconsistentState(err.to_string()).into() } + VMRunnerError::LoadingError(msg) => { + panic!("Contract runtime failed to load a contrct: {msg}") + } VMRunnerError::Nondeterministic(msg) => { panic!("Contract runner returned non-deterministic error '{}', aborting", msg) } VMRunnerError::WasmUnknownError { debug_message } => { panic!("Wasmer returned unknown message: {}", debug_message) } - VMRunnerError::UnsupportedCompiler { debug_message } => { - panic!("Unsupported compiler error: {}", debug_message) - } }) }