diff --git a/soroban-env-host/src/auth.rs b/soroban-env-host/src/auth.rs index 7108bed2d..2ca8203ce 100644 --- a/soroban-env-host/src/auth.rs +++ b/soroban-env-host/src/auth.rs @@ -2319,6 +2319,7 @@ impl Host { CallParams::default_internal_call(), ); if let Err(e) = &res { + use crate::ErrorHandler; self.error( e.error, "check auth invocation for a custom account contract failed", diff --git a/soroban-env-host/src/builtin_contracts/account_contract.rs b/soroban-env-host/src/builtin_contracts/account_contract.rs index 5d2481b18..534f5fbd1 100644 --- a/soroban-env-host/src/builtin_contracts/account_contract.rs +++ b/soroban-env-host/src/builtin_contracts/account_contract.rs @@ -18,7 +18,7 @@ use crate::{ self, AccountId, ContractIdPreimage, Hash, ScErrorCode, ScErrorType, ThresholdIndexes, Uint256, }, - Env, EnvBase, HostError, Symbol, TryFromVal, TryIntoVal, Val, + Env, EnvBase, ErrorHandler, HostError, Symbol, TryFromVal, TryIntoVal, Val, }; use core::cmp::Ordering; diff --git a/soroban-env-host/src/builtin_contracts/stellar_asset_contract/allowance.rs b/soroban-env-host/src/builtin_contracts/stellar_asset_contract/allowance.rs index ca43d720b..bd147bea2 100644 --- a/soroban-env-host/src/builtin_contracts/stellar_asset_contract/allowance.rs +++ b/soroban-env-host/src/builtin_contracts/stellar_asset_contract/allowance.rs @@ -6,7 +6,7 @@ use crate::{ }, err, host::{metered_clone::MeteredClone, Host}, - Env, HostError, StorageType, TryIntoVal, + Env, ErrorHandler, HostError, StorageType, TryIntoVal, }; use super::storage_types::AllowanceValue; diff --git a/soroban-env-host/src/builtin_contracts/stellar_asset_contract/balance.rs b/soroban-env-host/src/builtin_contracts/stellar_asset_contract/balance.rs index 3249ca95d..00e8ad6c9 100644 --- a/soroban-env-host/src/builtin_contracts/stellar_asset_contract/balance.rs +++ b/soroban-env-host/src/builtin_contracts/stellar_asset_contract/balance.rs @@ -18,7 +18,7 @@ use crate::{ LedgerEntry, LedgerEntryData, LedgerKey, ScAddress, TrustLineAsset, TrustLineEntry, TrustLineEntryExt, TrustLineFlags, }, - Env, Host, HostError, StorageType, TryIntoVal, + Env, ErrorHandler, Host, HostError, StorageType, TryIntoVal, }; use super::storage_types::{BalanceValue, BALANCE_EXTEND_AMOUNT, BALANCE_TTL_THRESHOLD}; diff --git a/soroban-env-host/src/builtin_contracts/stellar_asset_contract/contract.rs b/soroban-env-host/src/builtin_contracts/stellar_asset_contract/contract.rs index 89a7bc376..ce78c0a7a 100644 --- a/soroban-env-host/src/builtin_contracts/stellar_asset_contract/contract.rs +++ b/soroban-env-host/src/builtin_contracts/stellar_asset_contract/contract.rs @@ -21,7 +21,7 @@ use crate::{ err, host::{metered_clone::MeteredClone, Host}, xdr::Asset, - BytesObject, Compare, Env, EnvBase, HostError, TryFromVal, TryIntoVal, + BytesObject, Compare, Env, EnvBase, ErrorHandler, HostError, TryFromVal, TryIntoVal, }; use soroban_builtin_sdk_macros::contractimpl; diff --git a/soroban-env-host/src/cost_runner/cost_types/vm_ops.rs b/soroban-env-host/src/cost_runner/cost_types/vm_ops.rs index 1dcf6839b..76dee7c99 100644 --- a/soroban-env-host/src/cost_runner/cost_types/vm_ops.rs +++ b/soroban-env-host/src/cost_runner/cost_types/vm_ops.rs @@ -4,13 +4,13 @@ use crate::{ xdr::{ContractCostType::VmInstantiation, Hash}, Vm, }; -use std::{hint::black_box, rc::Rc}; +use std::{hint::black_box, rc::Rc, sync::Arc}; #[derive(Clone)] pub struct VmInstantiationSample { pub id: Option, pub wasm: Vec, - pub module: Rc, + pub module: Arc, } // Protocol 20 coarse and unified cost model @@ -73,7 +73,7 @@ mod v21 { type SampleType = VmInstantiationSample; - type RecycledType = (Option>, Vec); + type RecycledType = (Option>, Vec); fn run_iter( host: &crate::Host, @@ -83,7 +83,9 @@ mod v21 { let module = black_box( ParsedModule::new( host, - sample.module.module.engine(), + host.get_ledger_protocol_version() + .expect("protocol version"), + sample.module.wasmi_module.engine(), &sample.wasm[..], sample.module.cost_inputs.clone(), ) diff --git a/soroban-env-host/src/crypto/bls12_381.rs b/soroban-env-host/src/crypto/bls12_381.rs index 7580a11e4..ec3707205 100644 --- a/soroban-env-host/src/crypto/bls12_381.rs +++ b/soroban-env-host/src/crypto/bls12_381.rs @@ -2,8 +2,8 @@ use crate::{ budget::AsBudget, host_object::HostVec, xdr::{ContractCostType, ScBytes, ScErrorCode, ScErrorType}, - Bool, BytesObject, ConversionError, Env, Host, HostError, TryFromVal, U256Object, U256Small, - U256Val, Val, VecObject, U256, + Bool, BytesObject, ConversionError, Env, ErrorHandler, Host, HostError, TryFromVal, U256Object, + U256Small, U256Val, Val, VecObject, U256, }; use ark_bls12_381::{ g1::Config as G1Config, g2::Config as G2Config, Bls12_381, Fq, Fq12, Fq2, Fr, G1Affine, diff --git a/soroban-env-host/src/e2e_invoke.rs b/soroban-env-host/src/e2e_invoke.rs index 609882fb9..db47b50e9 100644 --- a/soroban-env-host/src/e2e_invoke.rs +++ b/soroban-env-host/src/e2e_invoke.rs @@ -4,7 +4,6 @@ /// host functions. use std::{cmp::max, rc::Rc}; -use crate::ledger_info::get_key_durability; use crate::storage::EntryWithLiveUntil; #[cfg(any(test, feature = "recording_mode"))] use crate::{ @@ -31,6 +30,7 @@ use crate::{ }, DiagnosticLevel, Error, Host, HostError, LedgerInfo, MeteredOrdMap, }; +use crate::{ledger_info::get_key_durability, ModuleCache}; #[cfg(any(test, feature = "recording_mode"))] use sha2::{Digest, Sha256}; @@ -336,6 +336,44 @@ pub fn invoke_host_function_with_trace_hook, I: ExactSizeIterator base_prng_seed: T, diagnostic_events: &mut Vec, trace_hook: Option, +) -> Result { + invoke_host_function_with_trace_hook_and_module_cache( + budget, + enable_diagnostics, + encoded_host_fn, + encoded_resources, + encoded_source_account, + encoded_auth_entries, + ledger_info, + encoded_ledger_entries, + encoded_ttl_entries, + base_prng_seed, + diagnostic_events, + trace_hook, + None, + ) +} + +/// Same as `invoke_host_function_with_trace_hook` but allows to pass a `ModuleCache` +/// which should be pre-loaded with all contracts in this invocation. +#[allow(clippy::too_many_arguments)] +pub fn invoke_host_function_with_trace_hook_and_module_cache< + T: AsRef<[u8]>, + I: ExactSizeIterator, +>( + budget: &Budget, + enable_diagnostics: bool, + encoded_host_fn: T, + encoded_resources: T, + encoded_source_account: T, + encoded_auth_entries: I, + ledger_info: LedgerInfo, + encoded_ledger_entries: I, + encoded_ttl_entries: I, + base_prng_seed: T, + diagnostic_events: &mut Vec, + trace_hook: Option, + module_cache: Option, ) -> Result { let _span0 = tracy_span!("invoke_host_function"); @@ -376,6 +414,9 @@ pub fn invoke_host_function_with_trace_hook, I: ExactSizeIterator if enable_diagnostics { host.set_diagnostic_level(DiagnosticLevel::Debug)?; } + if let Some(module_cache) = module_cache { + host.set_module_cache(module_cache)?; + } let result = { let _span1 = tracy_span!("Host::invoke_function"); host.invoke_function(host_function) diff --git a/soroban-env-host/src/host.rs b/soroban-env-host/src/host.rs index 85a8edd20..f64506d63 100644 --- a/soroban-env-host/src/host.rs +++ b/soroban-env-host/src/host.rs @@ -43,7 +43,7 @@ pub(crate) mod prng; pub(crate) mod trace; mod validity; -pub use error::HostError; +pub use error::{ErrorHandler, HostError}; use frame::CallParams; pub use prng::{Seed, SEED_BYTES}; pub use trace::{TraceEvent, TraceHook, TraceRecord, TraceState}; @@ -92,7 +92,6 @@ pub(crate) const MIN_LEDGER_PROTOCOL_VERSION: u32 = 22; #[derive(Clone, Default)] struct HostImpl { module_cache: RefCell>, - shared_linker: RefCell>>, source_account: RefCell>, ledger: RefCell>, objects: RefCell>, @@ -217,12 +216,6 @@ impl_checked_borrow_helpers!( try_borrow_module_cache, try_borrow_module_cache_mut ); -impl_checked_borrow_helpers!( - shared_linker, - Option>, - try_borrow_linker, - try_borrow_linker_mut -); impl_checked_borrow_helpers!( source_account, Option, @@ -360,7 +353,6 @@ impl Host { let _client = tracy_client::Client::start(); Self(Rc::new(HostImpl { module_cache: RefCell::new(None), - shared_linker: RefCell::new(None), source_account: RefCell::new(None), ledger: RefCell::new(None), objects: Default::default(), @@ -398,13 +390,44 @@ impl Host { pub fn build_module_cache_if_needed(&self) -> Result<(), HostError> { if self.try_borrow_module_cache()?.is_none() { let cache = ModuleCache::new(self)?; - let linker = cache.make_linker(self)?; *self.try_borrow_module_cache_mut()? = Some(cache); - *self.try_borrow_linker_mut()? = Some(linker); } Ok(()) } + // Install a module cache from _outside_ the Host. Doing this is potentially + // delicate: the cache must contain all contracts that will be run by the + // host, and will not be further populated during execution. This is + // only allowed if the cache is of "reusable" type, i.e. it was created + // using `ModuleCache::new_reusable`. + pub fn set_module_cache(&self, cache: ModuleCache) -> Result<(), HostError> { + if !cache.is_reusable() { + return Err(self.err( + ScErrorType::Context, + ScErrorCode::InternalError, + "module cache not reusable", + &[], + )); + } + *self.try_borrow_module_cache_mut()? = Some(cache); + Ok(()) + } + + // Remove and return the module cache, to allow reuse in another host. Should + // typically only be called during the "finish" sequence of a host's lifecycle, + // i.e. when [Self::can_finish] returns `true` and the host is about to be + // destroyed. + pub fn take_module_cache(&self) -> Result { + self.try_borrow_module_cache_mut()?.take().ok_or_else(|| { + self.err( + ScErrorType::Context, + ScErrorCode::InternalError, + "missing module cache", + &[], + ) + }) + } + #[cfg(any(test, feature = "recording_mode"))] pub fn in_storage_recording_mode(&self) -> Result { if let crate::storage::FootprintMode::Recording(_) = self.try_borrow_storage()?.mode { @@ -417,7 +440,6 @@ impl Host { #[cfg(any(test, feature = "recording_mode"))] pub fn clear_module_cache(&self) -> Result<(), HostError> { *self.try_borrow_module_cache_mut()? = None; - *self.try_borrow_linker_mut()? = None; Ok(()) } diff --git a/soroban-env-host/src/host/conversion.rs b/soroban-env-host/src/host/conversion.rs index 755004b84..f78c4efc7 100644 --- a/soroban-env-host/src/host/conversion.rs +++ b/soroban-env-host/src/host/conversion.rs @@ -19,6 +19,8 @@ use crate::{ SymbolObject, TryFromVal, TryIntoVal, U32Val, Val, VecObject, }; +use super::ErrorHandler; + impl Host { // Notes on metering: free pub(crate) fn usize_to_u32(&self, u: usize) -> Result { diff --git a/soroban-env-host/src/host/data_helper.rs b/soroban-env-host/src/host/data_helper.rs index 62c3c0e62..9f536d045 100644 --- a/soroban-env-host/src/host/data_helper.rs +++ b/soroban-env-host/src/host/data_helper.rs @@ -15,7 +15,7 @@ use crate::{ LedgerKeyTrustLine, PublicKey, ScAddress, ScContractInstance, ScErrorCode, ScErrorType, ScMap, ScVal, Signer, SignerKey, ThresholdIndexes, TrustLineAsset, Uint256, }, - AddressObject, Env, Host, HostError, StorageType, U32Val, Val, + AddressObject, Env, ErrorHandler, Host, HostError, StorageType, U32Val, Val, }; impl Host { diff --git a/soroban-env-host/src/host/declared_size.rs b/soroban-env-host/src/host/declared_size.rs index 46822f6e9..c8146c73a 100644 --- a/soroban-env-host/src/host/declared_size.rs +++ b/soroban-env-host/src/host/declared_size.rs @@ -264,6 +264,11 @@ impl DeclaredSizeForMetering for Rc { const DECLARED_SIZE: u64 = 16; } +// Arc is the same. +impl DeclaredSizeForMetering for std::sync::Arc { + const DECLARED_SIZE: u64 = 16; +} + // RefCell is the underlying data plus an `isize` flag impl DeclaredSizeForMetering for RefCell { const DECLARED_SIZE: u64 = T::DECLARED_SIZE + 8; diff --git a/soroban-env-host/src/host/error.rs b/soroban-env-host/src/host/error.rs index a34040430..a9275c820 100644 --- a/soroban-env-host/src/host/error.rs +++ b/soroban-env-host/src/host/error.rs @@ -238,17 +238,51 @@ impl TryBorrowOrErr for RefCell { } } -impl Host { - /// Convenience function to construct an [Error] and pass to [Host::error]. - pub(crate) fn err( - &self, - type_: ScErrorType, - code: ScErrorCode, - msg: &str, - args: &[Val], - ) -> HostError { - let error = Error::from_type_and_code(type_, code); - self.error(error, msg, args) +/// This is a trait for mapping Results carrying various error types into `HostError`, +/// while potentially recording the existence of the error to diagnostic logs. +pub trait ErrorHandler { + fn map_err(&self, res: Result) -> Result + where + Error: From, + E: Debug; + fn error(&self, error: Error, msg: &str, args: &[Val]) -> HostError; +} + +impl ErrorHandler for Host { + /// Given a result carrying some error type that can be converted to an + /// [Error] and supports [core::fmt::Debug], calls [Host::error] with the + /// error when there's an error, also passing the result of + /// [core::fmt::Debug::fmt] when [Host::is_debug] is `true`. Returns a + /// [Result] over [HostError]. + /// + /// If you have an error type `T` you want to record as a detailed debug + /// event and a less-detailed [Error] code embedded in a [HostError], add an + /// `impl From for Error` over in `soroban_env_common::error`, or in the + /// module defining `T`, and call this where the error is generated. + /// + /// Note: we do _not_ want to `impl From for HostError` for such types, + /// as doing so will avoid routing them through the host in order to record + /// their extended diagnostic information into the event log. This means you + /// will wind up writing `host.map_err(...)?` a bunch in code that you used + /// to be able to get away with just writing `...?`, there's no way around + /// this if we want to record the diagnostic information. + fn map_err(&self, res: Result) -> Result + where + Error: From, + E: Debug, + { + res.map_err(|e| { + use std::borrow::Cow; + let mut msg: Cow<'_, str> = Cow::Borrowed(&""); + // This observes the debug state, but it only causes a different + // (richer) string to be logged as a diagnostic event, which + // is itself not observable outside the debug state. + self.with_debug_mode(|| { + msg = Cow::Owned(format!("{:?}", e)); + Ok(()) + }); + self.error(e.into(), &msg, &[]) + }) } /// At minimum constructs and returns a [HostError] built from the provided @@ -256,7 +290,7 @@ impl Host { /// records a diagnostic event with the provided `msg` and `args` and then /// enriches the returned [Error] with [DebugInfo] in the form of a /// [Backtrace] and snapshot of the [Events] buffer. - pub(crate) fn error(&self, error: Error, msg: &str, args: &[Val]) -> HostError { + fn error(&self, error: Error, msg: &str, args: &[Val]) -> HostError { let mut he = HostError::from(error); self.with_debug_mode(|| { // We _try_ to take a mutable borrow of the events buffer refcell @@ -277,6 +311,20 @@ impl Host { }); he } +} + +impl Host { + /// Convenience function to construct an [Error] and pass to [Host::error]. + pub(crate) fn err( + &self, + type_: ScErrorType, + code: ScErrorCode, + msg: &str, + args: &[Val], + ) -> HostError { + let error = Error::from_type_and_code(type_, code); + self.error(error, msg, args) + } pub(crate) fn maybe_get_debug_info(&self) -> Option> { #[allow(unused_mut)] @@ -332,42 +380,6 @@ impl Host { } } - /// Given a result carrying some error type that can be converted to an - /// [Error] and supports [core::fmt::Debug], calls [Host::error] with the - /// error when there's an error, also passing the result of - /// [core::fmt::Debug::fmt] when [Host::is_debug] is `true`. Returns a - /// [Result] over [HostError]. - /// - /// If you have an error type `T` you want to record as a detailed debug - /// event and a less-detailed [Error] code embedded in a [HostError], add an - /// `impl From for Error` over in `soroban_env_common::error`, or in the - /// module defining `T`, and call this where the error is generated. - /// - /// Note: we do _not_ want to `impl From for HostError` for such types, - /// as doing so will avoid routing them through the host in order to record - /// their extended diagnostic information into the event log. This means you - /// will wind up writing `host.map_err(...)?` a bunch in code that you used - /// to be able to get away with just writing `...?`, there's no way around - /// this if we want to record the diagnostic information. - pub(crate) fn map_err(&self, res: Result) -> Result - where - Error: From, - E: Debug, - { - res.map_err(|e| { - use std::borrow::Cow; - let mut msg: Cow<'_, str> = Cow::Borrowed(&""); - // This observes the debug state, but it only causes a different - // (richer) string to be logged as a diagnostic event, which - // is itself not observable outside the debug state. - self.with_debug_mode(|| { - msg = Cow::Owned(format!("{:?}", e)); - Ok(()) - }); - self.error(e.into(), &msg, &[]) - }) - } - // Extracts the account id from the given ledger key as address object `Val`. // Returns Void for unsupported entries. // Useful as a helper for error reporting. @@ -471,7 +483,7 @@ macro_rules! err { )* Ok(()) }); - $host.error($error.into(), $msg, &buf[0..i]) + <_ as $crate::ErrorHandler>::error($host, $error.into(), $msg, &buf[0..i]) } }; } diff --git a/soroban-env-host/src/host/frame.rs b/soroban-env-host/src/host/frame.rs index 5df04a596..94d0e7607 100644 --- a/soroban-env-host/src/host/frame.rs +++ b/soroban-env-host/src/host/frame.rs @@ -11,8 +11,8 @@ use crate::{ ContractExecutable, ContractIdPreimage, CreateContractArgsV2, Hash, HostFunction, HostFunctionType, ScAddress, ScContractInstance, ScErrorCode, ScErrorType, ScVal, }, - AddressObject, Error, Host, HostError, Object, Symbol, SymbolStr, TryFromVal, TryIntoVal, Val, - Vm, DEFAULT_HOST_DEPTH_LIMIT, + AddressObject, Error, ErrorHandler, Host, HostError, Object, Symbol, SymbolStr, TryFromVal, + TryIntoVal, Val, Vm, DEFAULT_HOST_DEPTH_LIMIT, }; #[cfg(any(test, feature = "testutils"))] diff --git a/soroban-env-host/src/host/mem_helper.rs b/soroban-env-host/src/host/mem_helper.rs index 6763c49d1..788bd704f 100644 --- a/soroban-env-host/src/host/mem_helper.rs +++ b/soroban-env-host/src/host/mem_helper.rs @@ -6,6 +6,7 @@ use crate::{ Compare, Host, HostError, Symbol, SymbolObject, SymbolSmall, SymbolStr, U32Val, Vm, VmCaller, }; +use super::ErrorHandler; use std::{cmp::Ordering, rc::Rc}; /// Helper type for host functions that receive a position and length pair and diff --git a/soroban-env-host/src/host/metered_clone.rs b/soroban-env-host/src/host/metered_clone.rs index b71d12567..de89f6a97 100644 --- a/soroban-env-host/src/host/metered_clone.rs +++ b/soroban-env-host/src/host/metered_clone.rs @@ -341,6 +341,10 @@ impl MeteredClone for Asset {} // cloning Rc is just a ref-count bump impl MeteredClone for Rc {} +// cloning Arc is just an _atomic_ ref-count bump, but still O(1) +// too cheap to meter. We don't use Arcs very much. +impl MeteredClone for std::sync::Arc {} + // cloning a RefCell clones its underlying data structure impl MeteredClone for RefCell { const IS_SHALLOW: bool = T::IS_SHALLOW; diff --git a/soroban-env-host/src/host/metered_map.rs b/soroban-env-host/src/host/metered_map.rs index 22bbcba6d..20dddf650 100644 --- a/soroban-env-host/src/host/metered_map.rs +++ b/soroban-env-host/src/host/metered_map.rs @@ -13,7 +13,17 @@ const MAP_OOB: Error = Error::from_type_and_code(ScErrorType::Object, ScErrorCod pub struct MeteredOrdMap { pub(crate) map: Vec<(K, V)>, - ctx: PhantomData, + // This is PhantomData instead of PhantomData because we just + // want MeteredOrdMap to require Budget when doing operations, not + // pretend it's carrying one. If we used PhantomData then we'd make + // MeteredOrdMap non-Send/Sync, which would prevent its use in the + // ModuleCache. + // + // See + // https://doc.rust-lang.org/nomicon/phantom-data.html#table-of-phantomdata-patterns + // for discussion of the ways you can use PhantomData to precisely model + // various sorts of constraints. + ctx: PhantomData, } /// `Clone` should not be used directly, used `MeteredClone` instead if diff --git a/soroban-env-host/src/host/metered_xdr.rs b/soroban-env-host/src/host/metered_xdr.rs index ec8ddf419..ce27c13da 100644 --- a/soroban-env-host/src/host/metered_xdr.rs +++ b/soroban-env-host/src/host/metered_xdr.rs @@ -6,6 +6,8 @@ use crate::{ }; use std::io::Write; +use super::ErrorHandler; + struct MeteredWrite<'a, W: Write> { budget: &'a Budget, w: &'a mut W, diff --git a/soroban-env-host/src/lib.rs b/soroban-env-host/src/lib.rs index c9e4f96cc..9c0343cfa 100644 --- a/soroban-env-host/src/lib.rs +++ b/soroban-env-host/src/lib.rs @@ -33,11 +33,12 @@ pub(crate) mod host_object; pub mod auth; pub mod vm; -pub use vm::Vm; +pub use vm::{CompilationContext, ModuleCache, Vm}; pub mod storage; pub use budget::{DEFAULT_HOST_DEPTH_LIMIT, DEFAULT_XDR_RW_LIMITS}; pub use host::{ - metered_map::MeteredOrdMap, metered_vector::MeteredVector, Host, HostError, Seed, SEED_BYTES, + metered_map::MeteredOrdMap, metered_vector::MeteredVector, ErrorHandler, Host, HostError, Seed, + SEED_BYTES, }; pub use soroban_env_common::*; diff --git a/soroban-env-host/src/test/budget_metering.rs b/soroban-env-host/src/test/budget_metering.rs index 754076d8d..d9bd4bd15 100644 --- a/soroban-env-host/src/test/budget_metering.rs +++ b/soroban-env-host/src/test/budget_metering.rs @@ -1,9 +1,11 @@ use crate::{ budget::{AsBudget, Budget}, - host::metered_clone::{MeteredClone, MeteredIterator}, - host::metered_xdr::metered_write_xdr, + host::{ + metered_clone::{MeteredClone, MeteredIterator}, + metered_xdr::metered_write_xdr, + }, xdr::{ContractCostType, ScMap, ScMapEntry, ScVal}, - Env, Host, HostError, Symbol, Val, + Env, ErrorHandler, Host, HostError, Symbol, Val, }; use expect_test::{self, expect}; use soroban_env_common::xdr::{ScErrorCode, ScErrorType}; diff --git a/soroban-env-host/src/test/event.rs b/soroban-env-host/src/test/event.rs index d3f8c45aa..e3c756ca3 100644 --- a/soroban-env-host/src/test/event.rs +++ b/soroban-env-host/src/test/event.rs @@ -9,7 +9,8 @@ use crate::{ ContractCostType, ContractEvent, ContractEventBody, ContractEventType, ContractEventV0, ExtensionPoint, Hash, ScAddress, ScErrorCode, ScErrorType, ScMap, ScMapEntry, ScVal, }, - Compare, ContractFunctionSet, Env, Error, Host, HostError, Symbol, SymbolSmall, Val, VecObject, + Compare, ContractFunctionSet, Env, Error, ErrorHandler, Host, HostError, Symbol, SymbolSmall, + Val, VecObject, }; use expect_test::expect; use more_asserts::assert_le; diff --git a/soroban-env-host/src/test/lifecycle.rs b/soroban-env-host/src/test/lifecycle.rs index e3110e269..b06454a7c 100644 --- a/soroban-env-host/src/test/lifecycle.rs +++ b/soroban-env-host/src/test/lifecycle.rs @@ -1093,7 +1093,7 @@ mod cap_54_55_56 { let wasm = get_contract_wasm_ref(&host, contract_id); let module_cache = host.try_borrow_module_cache()?; if let Some(module_cache) = &*module_cache { - assert!(module_cache.get_module(&host, &wasm).is_ok()); + assert!(module_cache.get_module(&*host, &wasm).is_ok()); } else { panic!("expected module cache"); } @@ -1409,7 +1409,7 @@ mod cap_54_55_56 { // Check that the module cache did not get populated with the new wasm. if let Some(module_cache) = &*host.try_borrow_module_cache()? { - assert!(module_cache.get_module(&host, &wasm_hash)?.is_none()); + assert!(module_cache.get_module(&*host, &wasm_hash)?.is_none()); } else { panic!("expected module cache"); } diff --git a/soroban-env-host/src/test/map.rs b/soroban-env-host/src/test/map.rs index ebe6f0635..217c2f08c 100644 --- a/soroban-env-host/src/test/map.rs +++ b/soroban-env-host/src/test/map.rs @@ -4,8 +4,8 @@ use crate::{ AccountId, ContractCostType, LedgerEntry, LedgerKey, LedgerKeyAccount, PublicKey, ScErrorCode, ScErrorType, ScMap, ScMapEntry, ScVal, ScVec, Uint256, VecM, }, - Env, Error, Host, HostError, MapObject, MeteredOrdMap, Symbol, SymbolSmall, TryFromVal, U32Val, - Val, + Env, Error, ErrorHandler, Host, HostError, MapObject, MeteredOrdMap, Symbol, SymbolSmall, + TryFromVal, U32Val, Val, }; use more_asserts::assert_ge; use soroban_test_wasms::LINEAR_MEMORY; diff --git a/soroban-env-host/src/test/vec.rs b/soroban-env-host/src/test/vec.rs index a7a0eb1d0..06cb85f51 100644 --- a/soroban-env-host/src/test/vec.rs +++ b/soroban-env-host/src/test/vec.rs @@ -1,7 +1,8 @@ use crate::{ testutils::wasm, xdr::{ContractCostType, ScErrorCode, ScErrorType, ScVal}, - Compare, Env, Host, HostError, Object, Symbol, Tag, TryFromVal, U32Val, Val, VecObject, + Compare, Env, ErrorHandler, Host, HostError, Object, Symbol, Tag, TryFromVal, U32Val, Val, + VecObject, }; use core::cmp::Ordering; use more_asserts::assert_ge; diff --git a/soroban-env-host/src/testutils.rs b/soroban-env-host/src/testutils.rs index 68b7aab3f..d70ceb62a 100644 --- a/soroban-env-host/src/testutils.rs +++ b/soroban-env-host/src/testutils.rs @@ -1,5 +1,6 @@ use crate::e2e_invoke::ledger_entry_to_ledger_key; use crate::storage::EntryWithLiveUntil; +use crate::ErrorHandler; use crate::{ budget::Budget, builtin_contracts::testutils::create_account, diff --git a/soroban-env-host/src/vm.rs b/soroban-env-host/src/vm.rs index 10b52a188..4949f9f27 100644 --- a/soroban-env-host/src/vm.rs +++ b/soroban-env-host/src/vm.rs @@ -27,17 +27,16 @@ use crate::{ metered_hash::{CountingHasher, MeteredHash}, }, xdr::{ContractCostType, Hash, ScErrorCode, ScErrorType}, - ConversionError, Host, HostError, Symbol, SymbolStr, TryIntoVal, Val, WasmiMarshal, + ConversionError, ErrorHandler, Host, HostError, Symbol, SymbolStr, TryIntoVal, Val, + WasmiMarshal, }; -use std::{cell::RefCell, collections::BTreeSet, rc::Rc}; +use std::{cell::RefCell, collections::BTreeSet, rc::Rc, sync::Arc}; use fuel_refillable::FuelRefillable; use func_info::HOST_FUNCTIONS; pub use module_cache::ModuleCache; -pub use parsed_module::{ParsedModule, VersionedContractCodeCostInputs}; - -use wasmi::{Instance, Linker, Memory, Store, Value}; +pub use parsed_module::{CompilationContext, ParsedModule, VersionedContractCodeCostInputs}; use crate::VmCaller; use wasmi::{Caller, StoreContextMut}; @@ -86,10 +85,10 @@ impl Drop for VmInstantiationTimer { pub struct Vm { pub(crate) contract_id: Hash, #[allow(dead_code)] - pub(crate) module: Rc, - store: RefCell>, - instance: Instance, - pub(crate) memory: Option, + pub(crate) module: Arc, + wasmi_store: RefCell>, + wasmi_instance: wasmi::Instance, + pub(crate) wasmi_memory: Option, } impl std::hash::Hash for Vm { @@ -99,18 +98,33 @@ impl std::hash::Hash for Vm { } impl Host { - pub(crate) fn make_linker( + // Make a wasmi linker restricted to _only_ importing the symbols + // mentioned in `symbols`. + pub(crate) fn make_minimal_wasmi_linker_for_symbols( + context: &Ctx, engine: &wasmi::Engine, symbols: &BTreeSet<(&str, &str)>, - ) -> Result, HostError> { - let mut linker = Linker::new(&engine); + ) -> Result, HostError> { + let mut linker = wasmi::Linker::new(&engine); for hf in HOST_FUNCTIONS { if symbols.contains(&(hf.mod_str, hf.fn_str)) { - (hf.wrap)(&mut linker).map_err(|le| wasmi::Error::Linker(le))?; + context.map_err((hf.wrap)(&mut linker).map_err(|le| wasmi::Error::Linker(le)))?; } } Ok(linker) } + + // Make a wasmi linker that imports all the symbols. + pub(crate) fn make_maximal_wasmi_linker( + context: &Ctx, + engine: &wasmi::Engine, + ) -> Result, HostError> { + let mut linker = wasmi::Linker::new(&engine); + for hf in HOST_FUNCTIONS { + context.map_err((hf.wrap)(&mut linker).map_err(|le| wasmi::Error::Linker(le)))?; + } + Ok(linker) + } } // In one very narrow context -- when recording, and with a module cache -- we @@ -146,81 +160,25 @@ impl Vm { .collect() } - /// Instantiates a VM given the arguments provided in [`Self::new`], - /// or [`Self::new_from_module_cache`] - fn instantiate( + /// Instantiate wasmi components specifically (vs. any other future backend). + fn instantiate_wasmi( host: &Host, - contract_id: Hash, - parsed_module: Rc, - linker: &Linker, - ) -> Result, HostError> { - let _span = tracy_span!("Vm::instantiate"); - - // The host really never should have made it past construction on an old - // protocol version, but it doesn't hurt to double check here before we - // instantiate a VM, which is the place old-protocol replay will - // diverge. - host.check_ledger_protocol_supported()?; - - let engine = parsed_module.module.engine(); - let mut store = Store::new(engine, host.clone()); - + parsed_module: &Arc, + wasmi_linker: &wasmi::Linker, + ) -> Result<(wasmi::Store, wasmi::Instance, Option), HostError> { + let _span = tracy_span!("Vm::instantiate_wasmi"); + + let wasmi_engine = parsed_module.wasmi_module.engine(); + let mut store = { + let _span = tracy_span!("Vm::instantiate_wasmi - store"); + wasmi::Store::new(wasmi_engine, host.clone()) + }; parsed_module.cost_inputs.charge_for_instantiation(host)?; - store.limiter(|host| host); - - { - // We perform instantiation-time protocol version gating of - // all module-imported symbols here. - // Reasons for doing link-time instead of run-time check: - // 1. VM instantiation is performed in both contract upload and - // execution, thus any errorous contract will be rejected at - // upload time. - // 2. If a contract contains a call to an outdated host function, - // i.e. `contract_protocol > hf.max_supported_protocol`, failing - // early is preferred from resource usage perspective. - // 3. If a contract contains a call to an non-existent host - // function, the current (correct) behavior is to return - // `Wasmi::errors::LinkerError::MissingDefinition` error (which gets - // converted to a `(WasmVm, InvalidAction)`). If that host - // function is defined in a later protocol, and we replay that - // contract (in the earlier protocol where it belongs), we need - // to return the same error. - let _span0 = tracy_span!("define host functions"); - let ledger_proto = host.with_ledger_info(|li| Ok(li.protocol_version))?; - parsed_module.with_import_symbols(host, |module_symbols| { - for hf in HOST_FUNCTIONS { - if !module_symbols.contains(&(hf.mod_str, hf.fn_str)) { - continue; - } - if let Some(min_proto) = hf.min_proto { - if parsed_module.proto_version < min_proto || ledger_proto < min_proto { - return Err(host.err( - ScErrorType::WasmVm, - ScErrorCode::InvalidAction, - "contract calls a host function not yet supported by current protocol", - &[], - )); - } - } - if let Some(max_proto) = hf.max_proto { - if parsed_module.proto_version > max_proto || ledger_proto > max_proto { - return Err(host.err( - ScErrorType::WasmVm, - ScErrorCode::InvalidAction, - "contract calls a host function no longer supported in the current protocol", - &[], - )); - } - } - } - Ok(()) - })?; - } - + parsed_module.check_contract_imports_match_host_protocol(host)?; let not_started_instance = { - let _span0 = tracy_span!("instantiate module"); - host.map_err(linker.instantiate(&mut store, &parsed_module.module))? + let _span = tracy_span!("Vm::instantiate_wasmi - instantiate"); + host.map_err(wasmi_linker.instantiate(&mut store, &parsed_module.wasmi_module))? }; let instance = host.map_err( @@ -234,6 +192,27 @@ impl Vm { } else { None }; + Ok((store, instance, memory)) + } + + /// Instantiates a VM given the arguments provided in [`Self::new`], + /// or [`Self::new_from_module_cache`] + fn instantiate( + host: &Host, + contract_id: Hash, + parsed_module: Arc, + wasmi_linker: &wasmi::Linker, + ) -> Result, HostError> { + let _span = tracy_span!("Vm::instantiate"); + + // The host really never should have made it past construction on an old + // protocol version, but it doesn't hurt to double check here before we + // instantiate a VM, which is the place old-protocol replay will + // diverge. + host.check_ledger_protocol_supported()?; + + let (wasmi_store, wasmi_instance, wasmi_memory) = + Self::instantiate_wasmi(host, &parsed_module, wasmi_linker)?; // Here we do _not_ supply the store with any fuel. Fuel is supplied // right before the VM is being run, i.e., before crossing the host->VM @@ -241,24 +220,24 @@ impl Vm { Ok(Rc::new(Self { contract_id, module: parsed_module, - store: RefCell::new(store), - instance, - memory, + wasmi_store: RefCell::new(wasmi_store), + wasmi_instance, + wasmi_memory, })) } pub fn from_parsed_module( host: &Host, contract_id: Hash, - parsed_module: Rc, + parsed_module: Arc, ) -> Result, HostError> { let _span = tracy_span!("Vm::from_parsed_module"); VmInstantiationTimer::new(host.clone()); - if let Some(linker) = &*host.try_borrow_linker()? { - Self::instantiate(host, contract_id, parsed_module, linker) + if let Some(cache) = &*host.try_borrow_module_cache()? { + Self::instantiate(host, contract_id, parsed_module, &cache.wasmi_linker) } else { - let linker = parsed_module.make_linker(host)?; - Self::instantiate(host, contract_id, parsed_module, &linker) + let wasmi_linker = parsed_module.make_wasmi_linker(host)?; + Self::instantiate(host, contract_id, parsed_module, &wasmi_linker) } } @@ -304,8 +283,8 @@ impl Vm { let _span = tracy_span!("Vm::new"); VmInstantiationTimer::new(host.clone()); let parsed_module = Self::parse_module(host, wasm, cost_inputs, cost_mode)?; - let linker = parsed_module.make_linker(host)?; - Self::instantiate(host, contract_id, parsed_module, &linker) + let wasmi_linker = parsed_module.make_wasmi_linker(host)?; + Self::instantiate(host, contract_id, parsed_module, &wasmi_linker) } #[cfg(not(any(test, feature = "recording_mode")))] @@ -314,7 +293,7 @@ impl Vm { wasm: &[u8], cost_inputs: VersionedContractCodeCostInputs, _cost_mode: ModuleParseCostMode, - ) -> Result, HostError> { + ) -> Result, HostError> { ParsedModule::new_with_isolated_engine(host, wasm, cost_inputs) } @@ -359,7 +338,7 @@ impl Vm { wasm: &[u8], cost_inputs: VersionedContractCodeCostInputs, cost_mode: ModuleParseCostMode, - ) -> Result, HostError> { + ) -> Result, HostError> { if cost_mode == ModuleParseCostMode::PossiblyDeferredIfRecording { if host.in_storage_recording_mode()? { return host.budget_ref().with_observable_shadow_mode(|| { @@ -370,8 +349,8 @@ impl Vm { ParsedModule::new_with_isolated_engine(host, wasm, cost_inputs) } - pub(crate) fn get_memory(&self, host: &Host) -> Result { - match self.memory { + pub(crate) fn get_memory(&self, host: &Host) -> Result { + match self.wasmi_memory { Some(mem) => Ok(mem), None => Err(host.err( ScErrorType::WasmVm, @@ -390,7 +369,7 @@ impl Vm { self: &Rc, host: &Host, func_sym: &Symbol, - inputs: &[Value], + inputs: &[wasmi::Value], treat_missing_function_as_noop: bool, ) -> Result { host.charge_budget(ContractCostType::InvokeVmFunction, None)?; @@ -398,8 +377,8 @@ impl Vm { // resolve the function entity to be called let func_ss: SymbolStr = func_sym.try_into_val(host)?; let ext = match self - .instance - .get_export(&*self.store.try_borrow_or_err()?, func_ss.as_ref()) + .wasmi_instance + .get_export(&*self.wasmi_store.try_borrow_or_err()?, func_ss.as_ref()) { None => { if treat_missing_function_as_noop { @@ -437,13 +416,15 @@ impl Vm { } // call the function - let mut wasm_ret: [Value; 1] = [Value::I64(0)]; - self.store.try_borrow_mut_or_err()?.add_fuel_to_vm(host)?; + let mut wasm_ret: [wasmi::Value; 1] = [wasmi::Value::I64(0)]; + self.wasmi_store + .try_borrow_mut_or_err()? + .add_fuel_to_vm(host)?; // Metering: the `func.call` will trigger `wasmi::Call` (or `CallIndirect`) instruction, // which is technically covered by wasmi fuel metering. So we are double charging a bit // here (by a few 100s cpu insns). It is better to be safe. let res = func.call( - &mut *self.store.try_borrow_mut_or_err()?, + &mut *self.wasmi_store.try_borrow_mut_or_err()?, inputs, &mut wasm_ret, ); @@ -452,7 +433,7 @@ impl Vm { // wasmi instruction) remaining when the `OutOfFuel` trap occurs. This is only observable // if the contract traps with `OutOfFuel`, which may appear confusing if they look closely // at the budget amount consumed. So it should be fine. - self.store + self.wasmi_store .try_borrow_mut_or_err()? .return_fuel_to_host(host)?; @@ -510,11 +491,11 @@ impl Vm { treat_missing_function_as_noop: bool, ) -> Result { let _span = tracy_span!("Vm::invoke_function_raw"); - Vec::::charge_bulk_init_cpy(args.len() as u64, host.as_budget())?; - let wasm_args: Vec = args + Vec::::charge_bulk_init_cpy(args.len() as u64, host.as_budget())?; + let wasm_args: Vec = args .iter() .map(|i| host.absolute_to_relative(*i).map(|v| v.marshal_from_self())) - .collect::, HostError>>()?; + .collect::, HostError>>()?; self.metered_func_call( host, func_sym, @@ -536,9 +517,9 @@ impl Vm { where F: FnOnce(&mut VmCaller) -> Result, { - let store: &mut Store = &mut *self.store.try_borrow_mut_or_err()?; + let store: &mut wasmi::Store = &mut *self.wasmi_store.try_borrow_mut_or_err()?; let mut ctx: StoreContextMut = store.into(); - let caller: Caller = Caller::new(&mut ctx, Some(&self.instance)); + let caller: Caller = Caller::new(&mut ctx, Some(&self.wasmi_instance)); let mut vmcaller: VmCaller = VmCaller(Some(caller)); f(&mut vmcaller) } @@ -548,15 +529,15 @@ impl Vm { where F: FnOnce(Caller) -> Result, { - let store: &mut Store = &mut *self.store.try_borrow_mut_or_err()?; + let store: &mut wasmi::Store = &mut *self.wasmi_store.try_borrow_mut_or_err()?; let mut ctx: StoreContextMut = store.into(); - let caller: Caller = Caller::new(&mut ctx, Some(&self.instance)); + let caller: Caller = Caller::new(&mut ctx, Some(&self.wasmi_instance)); f(caller) } pub(crate) fn memory_hash_and_size(&self, budget: &Budget) -> Result<(u64, usize), HostError> { use std::hash::Hasher; - if let Some(mem) = self.memory { + if let Some(mem) = self.wasmi_memory { self.with_vmcaller(|vmcaller| { let mut state = CountingHasher::default(); let data = mem.data(vmcaller.try_ref()?); @@ -578,7 +559,7 @@ impl Vm { let ctx: StoreContext<'_, _> = vmcaller.try_ref()?.into(); let mut size: usize = 0; let mut state = CountingHasher::default(); - for export in self.instance.exports(vmcaller.try_ref()?) { + for export in self.wasmi_instance.exports(vmcaller.try_ref()?) { size = size.saturating_add(1); export.name().metered_hash(&mut state, budget)?; diff --git a/soroban-env-host/src/vm/dispatch.rs b/soroban-env-host/src/vm/dispatch.rs index 514e12cd9..39997adf6 100644 --- a/soroban-env-host/src/vm/dispatch.rs +++ b/soroban-env-host/src/vm/dispatch.rs @@ -4,9 +4,10 @@ use crate::{ CheckedEnvArg, EnvBase, Host, HostError, VmCaller, VmCallerEnv, }; use crate::{ - AddressObject, Bool, BytesObject, DurationObject, Error, I128Object, I256Object, I256Val, - I64Object, MapObject, StorageType, StringObject, Symbol, SymbolObject, TimepointObject, - U128Object, U256Object, U256Val, U32Val, U64Object, U64Val, Val, VecObject, Void, + AddressObject, Bool, BytesObject, DurationObject, Error, ErrorHandler, I128Object, I256Object, + I256Val, I64Object, MapObject, StorageType, StringObject, Symbol, SymbolObject, + TimepointObject, U128Object, U256Object, U256Val, U32Val, U64Object, U64Val, Val, VecObject, + Void, }; use core::fmt::Debug; use soroban_env_common::{call_macro_with_all_host_functions, WasmiMarshal}; diff --git a/soroban-env-host/src/vm/module_cache.rs b/soroban-env-host/src/vm/module_cache.rs index e93df15b9..1736ea018 100644 --- a/soroban-env-host/src/vm/module_cache.rs +++ b/soroban-env-host/src/vm/module_cache.rs @@ -1,15 +1,17 @@ use super::{ func_info::HOST_FUNCTIONS, - parsed_module::{ParsedModule, VersionedContractCodeCostInputs}, + parsed_module::{CompilationContext, ParsedModule, VersionedContractCodeCostInputs}, }; use crate::{ - budget::{get_wasmi_config, AsBudget}, + budget::{get_wasmi_config, AsBudget, Budget}, host::metered_clone::{MeteredClone, MeteredContainer}, xdr::{Hash, ScErrorCode, ScErrorType}, Host, HostError, MeteredOrdMap, }; -use std::{collections::BTreeSet, rc::Rc}; -use wasmi::Engine; +use std::{ + collections::{BTreeMap, BTreeSet}, + sync::{Arc, Mutex, MutexGuard}, +}; /// A [ModuleCache] is a cache of a set of Wasm modules that have been parsed /// but not yet instantiated, along with a shared and reusable [Engine] storing @@ -18,20 +20,125 @@ use wasmi::Engine; /// [Engine] is locked during execution and no new modules can be added to it. #[derive(Clone, Default)] pub struct ModuleCache { - pub(crate) engine: Engine, - modules: MeteredOrdMap, Host>, + pub(crate) wasmi_engine: wasmi::Engine, + pub(crate) wasmi_linker: wasmi::Linker, + modules: ModuleCacheMap, +} + +// We may use the ModuleCache from multiple C++ theads where +// there's no checking of Send+Sync but we can at least ensure +// Rust thinks its API is thread-safe. +static_assertions::assert_impl_all!(ModuleCache: Send, Sync); + +// The module cache was originally designed as an immutable object +// established at host creation time and never updated. In order to support +// longer-lived modules caches, we allow construction of unmetered, "reusable" +// module maps, that imply various changes: +// +// - Modules can be added post-construction. +// - Adding an existing module is a harmless no-op, not an error. +// - The linkers are set to "maximal" mode to cover all possible imports. +// - The cache easily scales to a large number of modules, unlike MeteredOrdMap. +// - There is no metering of cache map operations. +// - The cache can be cloned, but the clone is a shallow copy. +// - The cache is mutable and shared among all copies, using a mutex. + +#[derive(Clone)] +enum ModuleCacheMap { + MeteredSingleUseMap(MeteredOrdMap, Budget>), + UnmeteredReusableMap(Arc>>>), +} + +impl Default for ModuleCacheMap { + fn default() -> Self { + Self::MeteredSingleUseMap(MeteredOrdMap::new()) + } +} + +impl ModuleCacheMap { + fn lock_map( + map: &Arc>>>, + ) -> Result>>, HostError> { + map.lock() + .map_err(|_| HostError::from((ScErrorType::Context, ScErrorCode::InternalError))) + } + + fn is_reusable(&self) -> bool { + matches!(self, Self::UnmeteredReusableMap(_)) + } + + fn contains_key(&self, key: &Hash, budget: &Budget) -> Result { + match self { + Self::MeteredSingleUseMap(map) => map.contains_key(key, budget), + Self::UnmeteredReusableMap(map) => Ok(Self::lock_map(map)?.contains_key(key)), + } + } + + fn get(&self, key: &Hash, budget: &Budget) -> Result>, HostError> { + match self { + Self::MeteredSingleUseMap(map) => Ok(map.get(key, budget)?.map(|rc| rc.clone())), + Self::UnmeteredReusableMap(map) => { + Ok(Self::lock_map(map)?.get(key).map(|rc| rc.clone())) + } + } + } + + fn insert( + &mut self, + key: Hash, + value: Arc, + budget: &Budget, + ) -> Result<(), HostError> { + match self { + Self::MeteredSingleUseMap(map) => { + *map = map.insert(key, value, budget)?; + } + Self::UnmeteredReusableMap(map) => { + Self::lock_map(map)?.insert(key, value); + } + } + Ok(()) + } } impl ModuleCache { pub fn new(host: &Host) -> Result { - let config = get_wasmi_config(host.as_budget())?; - let engine = Engine::new(&config); - let modules = MeteredOrdMap::new(); - let mut cache = Self { engine, modules }; + let wasmi_config = get_wasmi_config(host.as_budget())?; + let wasmi_engine = wasmi::Engine::new(&wasmi_config); + + let modules = ModuleCacheMap::MeteredSingleUseMap(MeteredOrdMap::new()); + let wasmi_linker = wasmi::Linker::new(&wasmi_engine); + let mut cache = Self { + wasmi_engine, + modules, + wasmi_linker, + }; + + // Now add the contracts and rebuild linkers restricted to them. cache.add_stored_contracts(host)?; + cache.wasmi_linker = cache.make_minimal_wasmi_linker_for_cached_modules(host)?; Ok(cache) } + pub fn new_reusable(context: &Ctx) -> Result { + let wasmi_config = get_wasmi_config(context.as_budget())?; + let wasmi_engine = wasmi::Engine::new(&wasmi_config); + + let modules = ModuleCacheMap::UnmeteredReusableMap(Arc::new(Mutex::new(BTreeMap::new()))); + + let wasmi_linker = Host::make_maximal_wasmi_linker(context, &wasmi_engine)?; + + Ok(Self { + wasmi_engine, + modules, + wasmi_linker, + }) + } + + pub fn is_reusable(&self) -> bool { + self.modules.is_reusable() + } + pub fn add_stored_contracts(&mut self, host: &Host) -> Result<(), HostError> { use crate::xdr::{ContractCodeEntry, ContractCodeEntryExt, LedgerEntryData, LedgerKey}; let storage = host.try_borrow_storage()?; @@ -73,7 +180,13 @@ impl ModuleCache { v1.cost_inputs.metered_clone(host.as_budget())?, ), }; - self.parse_and_cache_module(host, hash, code, code_cost_inputs)?; + self.parse_and_cache_module( + host, + host.get_ledger_protocol_version()?, + hash, + code, + code_cost_inputs, + )?; } } } @@ -81,35 +194,82 @@ impl ModuleCache { Ok(()) } - pub fn parse_and_cache_module( + pub fn parse_and_cache_module_simple( &mut self, - host: &Host, + context: &Ctx, + curr_ledger_protocol: u32, + wasm: &[u8], + ) -> Result<(), HostError> { + let contract_id = Hash(crate::crypto::sha256_hash_from_bytes_raw( + wasm, + context.as_budget(), + )?); + self.parse_and_cache_module( + context, + curr_ledger_protocol, + &contract_id, + wasm, + VersionedContractCodeCostInputs::V0 { + wasm_bytes: wasm.len(), + }, + ) + } + + pub fn parse_and_cache_module( + &mut self, + context: &Ctx, + curr_ledger_protocol: u32, contract_id: &Hash, wasm: &[u8], cost_inputs: VersionedContractCodeCostInputs, ) -> Result<(), HostError> { - if self.modules.contains_key(contract_id, host)? { - return Err(host.err( - ScErrorType::Context, - ScErrorCode::InternalError, - "module cache already contains contract", - &[], - )); + if self + .modules + .contains_key(contract_id, context.as_budget())? + { + if self.modules.is_reusable() { + return Ok(()); + } else { + return Err(context.error( + crate::Error::from_type_and_code( + ScErrorType::Context, + ScErrorCode::InternalError, + ), + "module cache already contains contract", + &[], + )); + } } - let parsed_module = ParsedModule::new(host, &self.engine, &wasm, cost_inputs)?; - self.modules = - self.modules - .insert(contract_id.metered_clone(host)?, parsed_module, host)?; + let parsed_module = ParsedModule::new( + context, + curr_ledger_protocol, + &self.wasmi_engine, + &wasm, + cost_inputs, + )?; + self.modules.insert( + contract_id.metered_clone(context.as_budget())?, + parsed_module, + context.as_budget(), + )?; Ok(()) } - pub fn with_import_symbols( + fn with_minimal_import_symbols( &self, host: &Host, callback: impl FnOnce(&BTreeSet<(&str, &str)>) -> Result, ) -> Result { let mut import_symbols = BTreeSet::new(); - for module in self.modules.values(host)? { + let ModuleCacheMap::MeteredSingleUseMap(modules) = &self.modules else { + return Err(host.err( + ScErrorType::Context, + ScErrorCode::InternalError, + "with_import_symbols called on non-MeteredSingleUseMap cache", + &[], + )); + }; + for module in modules.values(host.as_budget())? { module.with_import_symbols(host, |module_symbols| { for hf in HOST_FUNCTIONS { let sym = (hf.mod_str, hf.fn_str); @@ -131,16 +291,29 @@ impl ModuleCache { callback(&import_symbols) } - pub fn make_linker(&self, host: &Host) -> Result, HostError> { - self.with_import_symbols(host, |symbols| Host::make_linker(&self.engine, symbols)) + fn make_minimal_wasmi_linker_for_cached_modules( + &self, + host: &Host, + ) -> Result, HostError> { + self.with_minimal_import_symbols(host, |symbols| { + Host::make_minimal_wasmi_linker_for_symbols(host, &self.wasmi_engine, symbols) + }) } - pub fn get_module( + pub fn contains_module( &self, - host: &Host, wasm_hash: &Hash, - ) -> Result>, HostError> { - if let Some(m) = self.modules.get(wasm_hash, host)? { + context: &Ctx, + ) -> Result { + self.modules.contains_key(wasm_hash, context.as_budget()) + } + + pub fn get_module( + &self, + context: &Ctx, + wasm_hash: &Hash, + ) -> Result>, HostError> { + if let Some(m) = self.modules.get(wasm_hash, context.as_budget())? { Ok(Some(m.clone())) } else { Ok(None) diff --git a/soroban-env-host/src/vm/parsed_module.rs b/soroban-env-host/src/vm/parsed_module.rs index 3857e2f14..84f320172 100644 --- a/soroban-env-host/src/vm/parsed_module.rs +++ b/soroban-env-host/src/vm/parsed_module.rs @@ -1,4 +1,5 @@ use crate::{ + budget::AsBudget, err, host::metered_clone::MeteredContainer, meta, @@ -6,13 +7,11 @@ use crate::{ ContractCostType, Limited, ReadXdr, ScEnvMetaEntry, ScEnvMetaEntryInterfaceVersion, ScErrorCode, ScErrorType, }, - Host, HostError, DEFAULT_XDR_RW_LIMITS, + ErrorHandler, Host, HostError, Val, DEFAULT_XDR_RW_LIMITS, }; -use wasmi::{Engine, Module}; - -use super::Vm; -use std::{collections::BTreeSet, io::Cursor, rc::Rc}; +use super::{Vm, HOST_FUNCTIONS}; +use std::{collections::BTreeSet, io::Cursor, sync::Arc}; #[derive(Debug, Clone)] pub enum VersionedContractCodeCostInputs { @@ -27,49 +26,50 @@ impl VersionedContractCodeCostInputs { Self::V1(_) => false, } } - pub fn charge_for_parsing(&self, host: &Host) -> Result<(), HostError> { + pub fn charge_for_parsing(&self, budget: &impl AsBudget) -> Result<(), HostError> { + let budget = budget.as_budget(); match self { Self::V0 { wasm_bytes } => { - host.charge_budget(ContractCostType::VmInstantiation, Some(*wasm_bytes as u64))?; + budget.charge(ContractCostType::VmInstantiation, Some(*wasm_bytes as u64))?; } Self::V1(inputs) => { - host.charge_budget( + budget.charge( ContractCostType::ParseWasmInstructions, Some(inputs.n_instructions as u64), )?; - host.charge_budget( + budget.charge( ContractCostType::ParseWasmFunctions, Some(inputs.n_functions as u64), )?; - host.charge_budget( + budget.charge( ContractCostType::ParseWasmGlobals, Some(inputs.n_globals as u64), )?; - host.charge_budget( + budget.charge( ContractCostType::ParseWasmTableEntries, Some(inputs.n_table_entries as u64), )?; - host.charge_budget( + budget.charge( ContractCostType::ParseWasmTypes, Some(inputs.n_types as u64), )?; - host.charge_budget( + budget.charge( ContractCostType::ParseWasmDataSegments, Some(inputs.n_data_segments as u64), )?; - host.charge_budget( + budget.charge( ContractCostType::ParseWasmElemSegments, Some(inputs.n_elem_segments as u64), )?; - host.charge_budget( + budget.charge( ContractCostType::ParseWasmImports, Some(inputs.n_imports as u64), )?; - host.charge_budget( + budget.charge( ContractCostType::ParseWasmExports, Some(inputs.n_exports as u64), )?; - host.charge_budget( + budget.charge( ContractCostType::ParseWasmDataSegmentBytes, Some(inputs.n_data_segment_bytes as u64), )?; @@ -134,26 +134,36 @@ impl VersionedContractCodeCostInputs { } } +// A `CompilationContext` abstracts over the necessary budgeting and +// error-reporting dimensions of both the `Host` (when building a +// contract for throwaway use in an isolated context like contract-upload) +// and other contexts that might want to compile code (like embedders that +// precompile contracts). +pub trait CompilationContext: AsBudget + ErrorHandler {} +impl CompilationContext for Host {} + /// A [ParsedModule] contains the parsed [wasmi::Module] for a given Wasm blob, /// as well as a protocol number and set of [ContractCodeCostInputs] extracted /// from the module when it was parsed. pub struct ParsedModule { - pub module: Module, + pub wasmi_module: wasmi::Module, pub proto_version: u32, pub cost_inputs: VersionedContractCodeCostInputs, } impl ParsedModule { - pub fn new( - host: &Host, - engine: &Engine, + pub fn new( + context: &Ctx, + curr_ledger_protocol: u32, + wasmi_engine: &wasmi::Engine, wasm: &[u8], cost_inputs: VersionedContractCodeCostInputs, - ) -> Result, HostError> { - cost_inputs.charge_for_parsing(host)?; - let (module, proto_version) = Self::parse_wasm(host, engine, wasm)?; - Ok(Rc::new(Self { - module, + ) -> Result, HostError> { + cost_inputs.charge_for_parsing(context.as_budget())?; + let (wasmi_module, proto_version) = + Self::parse_wasm(context, curr_ledger_protocol, wasmi_engine, wasm)?; + Ok(Arc::new(Self { + wasmi_module, proto_version, cost_inputs, })) @@ -170,7 +180,7 @@ impl ParsedModule { // is to not be introducing a DoS vector. const SYM_LEN_LIMIT: usize = 10; let symbols: BTreeSet<(&str, &str)> = self - .module + .wasmi_module .imports() .filter_map(|i| { if i.ty().func().is_some() { @@ -183,6 +193,7 @@ impl ParsedModule { None }) .collect(); + // We approximate the cost of `BTreeSet` with the cost of initializng a // `Vec` with the same elements, and we are doing it after the set has // been created. The element count has been limited/charged during the @@ -193,9 +204,9 @@ impl ParsedModule { callback(&symbols) } - pub fn make_linker(&self, host: &Host) -> Result, HostError> { + pub fn make_wasmi_linker(&self, host: &Host) -> Result, HostError> { self.with_import_symbols(host, |symbols| { - Host::make_linker(self.module.engine(), symbols) + Host::make_minimal_wasmi_linker_for_symbols(host, self.wasmi_module.engine(), symbols) }) } @@ -203,44 +214,56 @@ impl ParsedModule { host: &Host, wasm: &[u8], cost_inputs: VersionedContractCodeCostInputs, - ) -> Result, HostError> { + ) -> Result, HostError> { use crate::budget::AsBudget; - let config = crate::vm::get_wasmi_config(host.as_budget())?; - let engine = Engine::new(&config); - Self::new(host, &engine, wasm, cost_inputs) + let wasmi_config = crate::vm::get_wasmi_config(host.as_budget())?; + let wasmi_engine = wasmi::Engine::new(&wasmi_config); + + Self::new( + host, + host.get_ledger_protocol_version()?, + &wasmi_engine, + wasm, + cost_inputs, + ) } /// Parse the Wasm blob into a [Module] and its protocol number, checking its interface version - fn parse_wasm(host: &Host, engine: &Engine, wasm: &[u8]) -> Result<(Module, u32), HostError> { + fn parse_wasm( + context: &Ctx, + curr_ledger_protocol: u32, + wasmi_engine: &wasmi::Engine, + wasm: &[u8], + ) -> Result<(wasmi::Module, u32), HostError> { let module = { - let _span0 = tracy_span!("parse module"); - host.map_err(Module::new(&engine, wasm))? + let _span = tracy_span!("wasmi::Module::new"); + context.map_err(wasmi::Module::new(&wasmi_engine, wasm))? }; - - Self::check_max_args(host, &module)?; - let interface_version = Self::check_meta_section(host, &module)?; + Self::check_max_args(context, &module)?; + let interface_version = Self::check_meta_section(context, curr_ledger_protocol, &module)?; let contract_proto = interface_version.protocol; Ok((module, contract_proto)) } - fn check_contract_interface_version( - host: &Host, + fn check_contract_interface_version( + context: &Ctx, + curr_ledger_protocol: u32, interface_version: &ScEnvMetaEntryInterfaceVersion, ) -> Result<(), HostError> { let want_proto = { - let ledger_proto = host.get_ledger_protocol_version()?; let env_proto = meta::INTERFACE_VERSION.protocol; - if ledger_proto <= env_proto { + if curr_ledger_protocol <= env_proto { // ledger proto should be before or equal to env proto - ledger_proto + curr_ledger_protocol } else { - return Err(err!( - host, - (ScErrorType::Context, ScErrorCode::InternalError), + return Err(context.error( + (ScErrorType::Context, ScErrorCode::InternalError).into(), "ledger protocol number is ahead of supported env protocol number", - ledger_proto, - env_proto + &[ + Val::from_u32(curr_ledger_protocol).to_val(), + Val::from_u32(env_proto).to_val(), + ], )); } }; @@ -262,11 +285,10 @@ impl ParsedModule { // stellar-core, so bypassing this check for "next" is safe. #[cfg(not(feature = "next"))] if got_pre != 0 { - return Err(err!( - host, - (ScErrorType::WasmVm, ScErrorCode::InvalidInput), + return Err(context.error( + (ScErrorType::WasmVm, ScErrorCode::InvalidInput).into(), "contract pre-release number for old protocol is nonzero", - got_pre + &[Val::from_u32(got_pre).to_val()], )); } } else if got_proto == want_proto { @@ -279,12 +301,13 @@ impl ParsedModule { // allow it only if it matches the current prerelease exactly. let want_pre = meta::INTERFACE_VERSION.pre_release; if want_pre != got_pre { - return Err(err!( - host, - (ScErrorType::WasmVm, ScErrorCode::InvalidInput), + return Err(context.error( + (ScErrorType::WasmVm, ScErrorCode::InvalidInput).into(), "contract pre-release number for current protocol does not match host", - got_pre, - want_pre + &[ + Val::from_u32(got_pre).to_val(), + Val::from_u32(want_pre).to_val(), + ], )); } } @@ -295,17 +318,66 @@ impl ParsedModule { // that the "future" protocol semantics baked in to a contract // differ from the final semantics chosen by the network, so to be // conservative we avoid even allowing this. - return Err(err!( - host, - (ScErrorType::WasmVm, ScErrorCode::InvalidInput), + return Err(context.error( + (ScErrorType::WasmVm, ScErrorCode::InvalidInput).into(), "contract protocol number is newer than host", - got_proto + &[Val::from_u32(got_proto).to_val()], )); } Ok(()) } - fn module_custom_section(m: &Module, name: impl AsRef) -> Option<&[u8]> { + pub(crate) fn check_contract_imports_match_host_protocol(&self, host: &Host) -> Result<(), HostError> { + // We perform instantiation-time protocol version gating of + // all module-imported symbols here. + // Reasons for doing link-time instead of run-time check: + // 1. VM instantiation is performed in both contract upload and + // execution, thus any errorous contract will be rejected at + // upload time. + // 2. If a contract contains a call to an outdated host function, + // i.e. `contract_protocol > hf.max_supported_protocol`, failing + // early is preferred from resource usage perspective. + // 3. If a contract contains a call to an non-existent host + // function, the current (correct) behavior is to return + // `Wasmi::errors::LinkerError::MissingDefinition` error (which gets + // converted to a `(WasmVm, InvalidAction)`). If that host + // function is defined in a later protocol, and we replay that + // contract (in the earlier protocol where it belongs), we need + // to return the same error. + let _span = tracy_span!("ParsedModule::check_contract_imports_match_host_protocol"); + let ledger_proto = host.with_ledger_info(|li| Ok(li.protocol_version))?; + self.with_import_symbols(host, |module_symbols| { + for hf in HOST_FUNCTIONS { + if !module_symbols.contains(&(hf.mod_str, hf.fn_str)) { + continue; + } + if let Some(min_proto) = hf.min_proto { + if self.proto_version < min_proto || ledger_proto < min_proto { + return Err(host.err( + ScErrorType::WasmVm, + ScErrorCode::InvalidAction, + "contract calls a host function not yet supported by current protocol", + &[], + )); + } + } + if let Some(max_proto) = hf.max_proto { + if self.proto_version > max_proto || ledger_proto > max_proto { + return Err(host.err( + ScErrorType::WasmVm, + ScErrorCode::InvalidAction, + "contract calls a host function no longer supported in the current protocol", + &[], + )); + } + } + } + Ok(()) + })?; + Ok(()) + } + + fn module_custom_section(m: &wasmi::Module, name: impl AsRef) -> Option<&[u8]> { m.custom_sections().iter().find_map(|s| { if &*s.name == name.as_ref() { Some(&*s.data) @@ -318,12 +390,13 @@ impl ParsedModule { /// Returns the raw bytes content of a named custom section from the Wasm /// module loaded into the [Vm], or `None` if no such custom section exists. pub fn custom_section(&self, name: impl AsRef) -> Option<&[u8]> { - Self::module_custom_section(&self.module, name) + Self::module_custom_section(&self.wasmi_module, name) } - fn check_meta_section( - host: &Host, - m: &Module, + fn check_meta_section( + context: &Ctx, + curr_ledger_protocol: u32, + m: &wasmi::Module, ) -> Result { if let Some(env_meta) = Self::module_custom_section(m, meta::ENV_META_V0_SECTION_NAME) { let mut limits = DEFAULT_XDR_RW_LIMITS; @@ -331,45 +404,41 @@ impl ParsedModule { let mut cursor = Limited::new(Cursor::new(env_meta), limits); if let Some(env_meta_entry) = ScEnvMetaEntry::read_xdr_iter(&mut cursor).next() { let ScEnvMetaEntry::ScEnvMetaKindInterfaceVersion(v) = - host.map_err(env_meta_entry)?; - Self::check_contract_interface_version(host, &v)?; + context.map_err(env_meta_entry)?; + Self::check_contract_interface_version(context, curr_ledger_protocol, &v)?; Ok(v) } else { - Err(host.err( - ScErrorType::WasmVm, - ScErrorCode::InvalidInput, + Err(context.error( + (ScErrorType::WasmVm, ScErrorCode::InvalidInput).into(), "contract missing environment interface version", &[], )) } } else { - Err(host.err( - ScErrorType::WasmVm, - ScErrorCode::InvalidInput, + Err(context.error( + (ScErrorType::WasmVm, ScErrorCode::InvalidInput).into(), "contract missing metadata section", &[], )) } } - fn check_max_args(host: &Host, m: &Module) -> Result<(), HostError> { + fn check_max_args(handler: &E, m: &wasmi::Module) -> Result<(), HostError> { for e in m.exports() { match e.ty() { wasmi::ExternType::Func(f) => { if f.results().len() > Vm::MAX_VM_ARGS { - return Err(err!( - host, - (ScErrorType::WasmVm, ScErrorCode::InvalidInput), + return Err(handler.error( + (ScErrorType::WasmVm, ScErrorCode::InvalidInput).into(), "Too many return values in Wasm export", - f.results().len() + &[Val::from_u32(f.results().len() as u32).to_val()], )); } if f.params().len() > Vm::MAX_VM_ARGS { - return Err(err!( - host, - (ScErrorType::WasmVm, ScErrorCode::InvalidInput), + return Err(handler.error( + (ScErrorType::WasmVm, ScErrorCode::InvalidInput).into(), "Too many arguments Wasm export", - f.params().len() + &[Val::from_u32(f.params().len() as u32).to_val()], )); } }