From ad0d6d27dab5a1711eaaf8af8783fdf203742eb3 Mon Sep 17 00:00:00 2001 From: Graydon Hoare Date: Fri, 18 Oct 2024 16:13:33 -0700 Subject: [PATCH 1/2] Surface module cache for reuse. --- soroban-env-host/src/auth.rs | 1 + .../src/builtin_contracts/account_contract.rs | 2 +- .../stellar_asset_contract/allowance.rs | 2 +- .../stellar_asset_contract/balance.rs | 2 +- .../stellar_asset_contract/contract.rs | 2 +- .../src/cost_runner/cost_types/vm_ops.rs | 10 +- soroban-env-host/src/crypto/bls12_381.rs | 4 +- soroban-env-host/src/e2e_invoke.rs | 43 +++- soroban-env-host/src/host.rs | 46 +++- soroban-env-host/src/host/conversion.rs | 2 + soroban-env-host/src/host/data_helper.rs | 2 +- soroban-env-host/src/host/declared_size.rs | 5 + soroban-env-host/src/host/error.rs | 110 ++++---- soroban-env-host/src/host/frame.rs | 4 +- soroban-env-host/src/host/mem_helper.rs | 1 + soroban-env-host/src/host/metered_clone.rs | 4 + soroban-env-host/src/host/metered_map.rs | 12 +- soroban-env-host/src/host/metered_xdr.rs | 2 + soroban-env-host/src/lib.rs | 5 +- soroban-env-host/src/test/budget_metering.rs | 8 +- soroban-env-host/src/test/event.rs | 3 +- soroban-env-host/src/test/lifecycle.rs | 4 +- soroban-env-host/src/test/map.rs | 4 +- soroban-env-host/src/test/vec.rs | 3 +- soroban-env-host/src/testutils.rs | 1 + soroban-env-host/src/vm.rs | 209 +++++++-------- soroban-env-host/src/vm/dispatch.rs | 7 +- soroban-env-host/src/vm/module_cache.rs | 237 ++++++++++++++--- soroban-env-host/src/vm/parsed_module.rs | 240 ++++++++++++------ 29 files changed, 655 insertions(+), 320 deletions(-) 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..2ab62c813 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,69 @@ 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 +393,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 +407,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()], )); } } From 976fa5d88da6c12e42452ef1c7671484d54592a9 Mon Sep 17 00:00:00 2001 From: Graydon Hoare Date: Fri, 10 Jan 2025 11:34:22 -0800 Subject: [PATCH 2/2] Add wasmtime support and make wasmi optional. --- Cargo.lock | 639 +++++++++++++++++- Cargo.toml | 7 + soroban-env-common/Cargo.toml | 2 + soroban-env-common/src/error.rs | 43 ++ soroban-env-common/src/lib.rs | 2 + soroban-env-common/src/val.rs | 56 ++ soroban-env-common/src/vmcaller_env.rs | 53 +- soroban-env-host/Cargo.toml | 8 +- soroban-env-host/src/budget.rs | 8 + .../src/budget/wasmtime_helper.rs | 15 + .../src/cost_runner/cost_types/vm_ops.rs | 1 + soroban-env-host/src/host.rs | 17 + soroban-env-host/src/host/error.rs | 56 ++ soroban-env-host/src/host/mem_helper.rs | 99 ++- soroban-env-host/src/lib.rs | 8 + soroban-env-host/src/vm.rs | 244 ++++++- soroban-env-host/src/vm/dispatch.rs | 190 +++++- soroban-env-host/src/vm/fuel_refillable.rs | 71 +- soroban-env-host/src/vm/func_info.rs | 10 + soroban-env-host/src/vm/module_cache.rs | 30 +- soroban-env-host/src/vm/parsed_module.rs | 56 +- 21 files changed, 1514 insertions(+), 101 deletions(-) create mode 100644 soroban-env-host/src/budget/wasmtime_helper.rs diff --git a/Cargo.lock b/Cargo.lock index 279b8109f..49dd718b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,7 @@ version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ - "gimli", + "gimli 0.28.0", ] [[package]] @@ -101,7 +101,7 @@ dependencies = [ "ark-std", "derivative", "hashbrown 0.13.2", - "itertools", + "itertools 0.10.5", "num-traits", "zeroize", ] @@ -118,7 +118,7 @@ dependencies = [ "ark-std", "derivative", "digest", - "itertools", + "itertools 0.10.5", "num-bigint", "num-traits", "paste", @@ -212,7 +212,7 @@ dependencies = [ "cfg-if", "libc", "miniz_oxide", - "object", + "object 0.32.1", "rustc-demangle", ] @@ -257,9 +257,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" @@ -307,6 +307,12 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "cobs" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" + [[package]] name = "colored" version = "2.0.4" @@ -315,7 +321,7 @@ checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" dependencies = [ "is-terminal", "lazy_static", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -339,6 +345,103 @@ dependencies = [ "libc", ] +[[package]] +name = "cranelift-bforest" +version = "0.115.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "cranelift-entity", +] + +[[package]] +name = "cranelift-bitset" +version = "0.115.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-codegen" +version = "0.115.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "bumpalo", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli 0.31.1", + "hashbrown 0.14.5", + "log", + "regalloc2", + "rustc-hash", + "serde", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.115.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "cranelift-codegen-shared", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.115.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" + +[[package]] +name = "cranelift-control" +version = "0.115.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.115.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-frontend" +version = "0.115.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.115.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" + +[[package]] +name = "cranelift-native" +version = "0.115.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + [[package]] name = "crate-git-revision" version = "0.0.6" @@ -350,6 +453,15 @@ dependencies = [ "serde_json", ] +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + [[package]] name = "crypto-bigint" version = "0.5.2" @@ -586,6 +698,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "equivalent" version = "1.0.1" @@ -594,12 +718,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.6" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -624,6 +748,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + [[package]] name = "ff" version = "0.13.0" @@ -646,6 +776,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" + [[package]] name = "generator" version = "0.8.3" @@ -689,6 +825,17 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +dependencies = [ + "fallible-iterator", + "indexmap 2.0.2", + "stable_deref_trait", +] + [[package]] name = "group" version = "0.13.0" @@ -717,9 +864,28 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.1" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "serde", +] + +[[package]] +name = "hashbrown" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +dependencies = [ + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" @@ -774,6 +940,12 @@ dependencies = [ "cc", ] +[[package]] +name = "id-arena" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" + [[package]] name = "ident_case" version = "1.0.1" @@ -798,7 +970,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" dependencies = [ "equivalent", - "hashbrown 0.14.1", + "hashbrown 0.14.5", "serde", ] @@ -816,7 +988,7 @@ checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi", "rustix", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -828,6 +1000,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.6" @@ -878,9 +1059,9 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.150" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "libm" @@ -890,9 +1071,9 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "linux-raw-sys" -version = "0.4.11" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "log" @@ -922,6 +1103,15 @@ dependencies = [ "nalgebra", ] +[[package]] +name = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] + [[package]] name = "matchers" version = "0.1.0" @@ -947,6 +1137,15 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "memfd" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2cffa4ad52c6f791f4f8b15f0c05f9824b2ced1160e88cc393d64fff9a8ac64" +dependencies = [ + "rustix", +] + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -1078,6 +1277,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "crc32fast", + "hashbrown 0.15.1", + "indexmap 2.0.2", + "memchr", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -1143,6 +1354,18 @@ dependencies = [ "spki", ] +[[package]] +name = "postcard" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7f0a8d620d71c457dd1d47df76bb18960378da56af4527aaa10f515eee732e" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1192,6 +1415,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psm" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa37f80ca58604976033fae9515a8a2989fc13797d953f7c04fb8fa36a11f205" +dependencies = [ + "cc", +] + +[[package]] +name = "pulley-interpreter" +version = "28.0.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "cranelift-bitset", + "log", + "sptr", +] + [[package]] name = "quote" version = "1.0.33" @@ -1237,6 +1479,19 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" +[[package]] +name = "regalloc2" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12908dbeb234370af84d0579b9f68258a0f67e201412dd9a2814e6f45b2fc0f0" +dependencies = [ + "hashbrown 0.14.5", + "log", + "rustc-hash", + "slice-group-by", + "smallvec", +] + [[package]] name = "regex" version = "1.10.2" @@ -1306,6 +1561,12 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + [[package]] name = "rustc_version" version = "0.4.0" @@ -1317,15 +1578,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.23" +version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb93593068e9babdad10e4fce47dc9b3ac25315a72a59766ffd9e9a71996a04" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -1488,11 +1749,20 @@ dependencies = [ "wide", ] +[[package]] +name = "slice-group-by" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7" + [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +dependencies = [ + "serde", +] [[package]] name = "soroban-bench-utils" @@ -1508,7 +1778,7 @@ dependencies = [ name = "soroban-builtin-sdk-macros" version = "22.1.3" dependencies = [ - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", "syn 2.0.39", @@ -1530,7 +1800,8 @@ dependencies = [ "static_assertions", "stellar-xdr", "tracy-client", - "wasmparser", + "wasmparser 0.116.1", + "wasmtime", ] [[package]] @@ -1562,7 +1833,7 @@ dependencies = [ "hex", "hex-literal", "hmac", - "itertools", + "itertools 0.10.5", "k256", "lstsq", "more-asserts", @@ -1594,9 +1865,10 @@ dependencies = [ "textplots", "thousands", "tracy-client", - "wasm-encoder", - "wasmparser", - "wasmprinter", + "wasm-encoder 0.36.2", + "wasmparser 0.116.1", + "wasmprinter 0.2.72", + "wasmtime", "wycheproof", ] @@ -1604,7 +1876,7 @@ dependencies = [ name = "soroban-env-macros" version = "22.1.3" dependencies = [ - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", "serde", @@ -1636,9 +1908,9 @@ dependencies = [ "soroban-env-common", "soroban-env-macros", "stellar-xdr", - "wasm-encoder", - "wasmparser", - "wasmprinter", + "wasm-encoder 0.36.2", + "wasmparser 0.116.1", + "wasmprinter 0.2.72", ] [[package]] @@ -1673,6 +1945,18 @@ dependencies = [ "der", ] +[[package]] +name = "sptr" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -1754,6 +2038,21 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "test_no_std" version = "22.1.3" @@ -1773,18 +2072,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "6e3de26b0965292219b4287ff031fcba86837900fe9cd2b34ea8ad893c0953d2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "268026685b2be38d7103e9e507c938a1fcb3d7e6eb15e87870b617bf37b6d581" dependencies = [ "proc-macro2", "quote", @@ -1950,6 +2249,12 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "valuable" version = "0.1.0" @@ -2031,6 +2336,16 @@ dependencies = [ "leb128", ] +[[package]] +name = "wasm-encoder" +version = "0.219.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29cbbd772edcb8e7d524a82ee8cef8dd046fc14033796a754c3ad246d019fa54" +dependencies = [ + "leb128", + "wasmparser 0.219.1", +] + [[package]] name = "wasmi_arena" version = "0.4.0" @@ -2057,6 +2372,20 @@ dependencies = [ "semver", ] +[[package]] +name = "wasmparser" +version = "0.219.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c771866898879073c53b565a6c7b49953795159836714ac56a5befb581227c5" +dependencies = [ + "ahash", + "bitflags", + "hashbrown 0.14.5", + "indexmap 2.0.2", + "semver", + "serde", +] + [[package]] name = "wasmparser-nostd" version = "0.100.2" @@ -2073,7 +2402,184 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aff4df0cdf1906ec040e97d78c3fc8fd26d3f8d70adaac81f07f80957b63b54" dependencies = [ "anyhow", - "wasmparser", + "wasmparser 0.116.1", +] + +[[package]] +name = "wasmprinter" +version = "0.219.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228cdc1f30c27816da225d239ce4231f28941147d34713dee8f1fff7cb330e54" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.219.1", +] + +[[package]] +name = "wasmtime" +version = "28.0.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "anyhow", + "bitflags", + "bumpalo", + "cc", + "cfg-if", + "hashbrown 0.14.5", + "indexmap 2.0.2", + "libc", + "libm", + "log", + "mach2", + "memfd", + "object 0.36.5", + "once_cell", + "paste", + "postcard", + "psm", + "pulley-interpreter", + "rustix", + "serde", + "serde_derive", + "smallvec", + "sptr", + "target-lexicon", + "wasmparser 0.219.1", + "wasmtime-asm-macros", + "wasmtime-component-macro", + "wasmtime-environ", + "wasmtime-jit-icache-coherence", + "wasmtime-slab", + "wasmtime-versioned-export-macros", + "wasmtime-winch", + "windows-sys 0.59.0", +] + +[[package]] +name = "wasmtime-asm-macros" +version = "28.0.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "wasmtime-component-macro" +version = "28.0.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn 2.0.39", + "wasmtime-component-util", + "wasmtime-wit-bindgen", + "wit-parser", +] + +[[package]] +name = "wasmtime-component-util" +version = "28.0.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" + +[[package]] +name = "wasmtime-cranelift" +version = "28.0.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "anyhow", + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli 0.31.1", + "itertools 0.12.1", + "log", + "object 0.36.5", + "smallvec", + "target-lexicon", + "thiserror", + "wasmparser 0.219.1", + "wasmtime-environ", + "wasmtime-versioned-export-macros", +] + +[[package]] +name = "wasmtime-environ" +version = "28.0.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "anyhow", + "cranelift-bitset", + "cranelift-entity", + "gimli 0.31.1", + "indexmap 2.0.2", + "log", + "object 0.36.5", + "postcard", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasm-encoder 0.219.1", + "wasmparser 0.219.1", + "wasmprinter 0.219.1", +] + +[[package]] +name = "wasmtime-jit-icache-coherence" +version = "28.0.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "anyhow", + "cfg-if", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "wasmtime-slab" +version = "28.0.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" + +[[package]] +name = "wasmtime-versioned-export-macros" +version = "28.0.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "wasmtime-winch" +version = "28.0.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "anyhow", + "cranelift-codegen", + "gimli 0.31.1", + "object 0.36.5", + "target-lexicon", + "wasmparser 0.219.1", + "wasmtime-cranelift", + "wasmtime-environ", + "winch-codegen", +] + +[[package]] +name = "wasmtime-wit-bindgen" +version = "28.0.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.0.2", + "wit-parser", ] [[package]] @@ -2102,12 +2608,37 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "winch-codegen" +version = "28.0.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "anyhow", + "cranelift-codegen", + "gimli 0.31.1", + "regalloc2", + "smallvec", + "target-lexicon", + "wasmparser 0.219.1", + "wasmtime-cranelift", + "wasmtime-environ", +] + [[package]] name = "windows" version = "0.58.0" @@ -2190,6 +2721,24 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -2320,6 +2869,24 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-parser" +version = "0.219.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a86f669283257e8e424b9a4fc3518e3ade0b95deb9fbc0f93a1876be3eda598" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.0.2", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.219.1", +] + [[package]] name = "wycheproof" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index a32761bd8..d18f8a238 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,13 @@ soroban-builtin-sdk-macros = { version = "=22.1.3", path = "soroban-builtin-sdk- # NB: this must match the wasmparser version wasmi is using wasmparser = "=0.116.1" +[workspace.dependencies.wasmtime] +version = "28.0" +default-features = false +features = ["runtime", "winch"] +git = "https://github.com/bytecodealliance/wasmtime" +rev = "b5627a86a7740ffc732f4c22b9f0b2c66252638b" + # NB: When updating, also update the version in rs-soroban-env dev-dependencies [workspace.dependencies.stellar-xdr] version = "=22.1.0" diff --git a/soroban-env-common/Cargo.toml b/soroban-env-common/Cargo.toml index cd58077ff..10b7f65fe 100644 --- a/soroban-env-common/Cargo.toml +++ b/soroban-env-common/Cargo.toml @@ -24,6 +24,7 @@ ethnum = "1.5.0" arbitrary = { version = "1.3.2", features = ["derive"], optional = true } num-traits = {version = "0.2.17", default-features = false} num-derive = "0.4.1" +wasmtime = { workspace = true, optional = true} [target.'cfg(not(target_family = "wasm"))'.dependencies] tracy-client = { version = "0.17.0", features = ["enable", "timer-fallback"], default-features = false, optional = true } @@ -36,6 +37,7 @@ num-traits = "0.2.17" std = ["stellar-xdr/std", "stellar-xdr/base64"] serde = ["dep:serde", "stellar-xdr/serde"] wasmi = ["dep:wasmi", "dep:wasmparser"] +wasmtime = ["dep:wasmtime"] testutils = ["dep:arbitrary", "stellar-xdr/arbitrary"] next = ["stellar-xdr/next", "soroban-env-macros/next"] tracy = ["dep:tracy-client"] diff --git a/soroban-env-common/src/error.rs b/soroban-env-common/src/error.rs index 56983ea44..4e6cd326c 100644 --- a/soroban-env-common/src/error.rs +++ b/soroban-env-common/src/error.rs @@ -283,6 +283,49 @@ impl From for Error { } } +#[cfg(feature = "wasmtime")] +impl From for Error { + #[allow(clippy::wildcard_in_or_patterns)] + fn from(trap: wasmtime::Trap) -> Self { + let ec = match trap { + wasmtime::Trap::UnreachableCodeReached => ScErrorCode::InvalidAction, + + wasmtime::Trap::MemoryOutOfBounds | wasmtime::Trap::TableOutOfBounds => { + ScErrorCode::IndexBounds + } + + wasmtime::Trap::IndirectCallToNull => ScErrorCode::MissingValue, + + wasmtime::Trap::IntegerDivisionByZero + | wasmtime::Trap::IntegerOverflow + | wasmtime::Trap::BadConversionToInteger => ScErrorCode::ArithDomain, + + wasmtime::Trap::BadSignature => ScErrorCode::UnexpectedType, + + wasmtime::Trap::StackOverflow + | wasmtime::Trap::Interrupt + | wasmtime::Trap::OutOfFuel => { + return Error::from_type_and_code(ScErrorType::Budget, ScErrorCode::ExceededLimit) + } + + wasmtime::Trap::HeapMisaligned + | wasmtime::Trap::AlwaysTrapAdapter + | wasmtime::Trap::AtomicWaitNonSharedMemory + | wasmtime::Trap::NullReference + | wasmtime::Trap::CannotEnterComponent + | _ => ScErrorCode::InvalidAction, + }; + Error::from_type_and_code(ScErrorType::WasmVm, ec) + } +} + +#[cfg(feature = "wasmtime")] +impl From for Error { + fn from(_: wasmtime::MemoryAccessError) -> Self { + Error::from_type_and_code(ScErrorType::WasmVm, ScErrorCode::IndexBounds) + } +} + impl Error { // NB: we don't provide a "get_type" to avoid casting a bad bit-pattern into // an ScErrorType. Instead we provide an "is_type" to check any specific diff --git a/soroban-env-common/src/lib.rs b/soroban-env-common/src/lib.rs index 9ff21e563..38f1d5534 100644 --- a/soroban-env-common/src/lib.rs +++ b/soroban-env-common/src/lib.rs @@ -113,6 +113,8 @@ pub use val::{ConversionError, Tag, Val}; #[cfg(feature = "wasmi")] pub use val::WasmiMarshal; +#[cfg(feature = "wasmtime")] +pub use val::WasmtimeMarshal; pub use val::{AddressObject, MapObject, VecObject}; pub use val::{Bool, Void}; diff --git a/soroban-env-common/src/val.rs b/soroban-env-common/src/val.rs index 4224b4c0b..6fc688498 100644 --- a/soroban-env-common/src/val.rs +++ b/soroban-env-common/src/val.rs @@ -465,6 +465,62 @@ impl WasmiMarshal for i64 { } } +#[cfg(feature = "wasmtime")] +pub trait WasmtimeMarshal: Sized { + fn try_marshal_from_wasmtime_value(v: wasmtime::Val) -> Option; + fn marshal_wasmtime_from_self(self) -> wasmtime::Val; +} + +#[cfg(feature = "wasmtime")] +impl WasmtimeMarshal for Val { + fn try_marshal_from_wasmtime_value(v: wasmtime::Val) -> Option { + if let wasmtime::Val::I64(i) = v { + let v = Val::from_payload(i as u64); + if v.is_good() { + Some(v) + } else { + None + } + } else { + None + } + } + + fn marshal_wasmtime_from_self(self) -> wasmtime::Val { + wasmtime::Val::I64(self.get_payload() as i64) + } +} + +#[cfg(feature = "wasmtime")] +impl WasmtimeMarshal for u64 { + fn try_marshal_from_wasmtime_value(v: wasmtime::Val) -> Option { + if let wasmtime::Val::I64(i) = v { + Some(i as u64) + } else { + None + } + } + + fn marshal_wasmtime_from_self(self) -> wasmtime::Val { + wasmtime::Val::I64(self as i64) + } +} + +#[cfg(feature = "wasmtime")] +impl WasmtimeMarshal for i64 { + fn try_marshal_from_wasmtime_value(v: wasmtime::Val) -> Option { + if let wasmtime::Val::I64(i) = v { + Some(i) + } else { + None + } + } + + fn marshal_wasmtime_from_self(self) -> wasmtime::Val { + wasmtime::Val::I64(self) + } +} + // Manually implement all the residual pieces: ValConverts // and Froms. diff --git a/soroban-env-common/src/vmcaller_env.rs b/soroban-env-common/src/vmcaller_env.rs index 933040a40..6006856ce 100644 --- a/soroban-env-common/src/vmcaller_env.rs +++ b/soroban-env-common/src/vmcaller_env.rs @@ -25,26 +25,55 @@ use core::marker::PhantomData; /// allows code to import and use `Env` directly (such as the native /// contract) to call host methods without having to write `VmCaller::none()` /// everywhere. -#[cfg(feature = "wasmi")] -pub struct VmCaller<'a, T>(pub Option>); -#[cfg(feature = "wasmi")] + +#[cfg(any(feature = "wasmi", feature = "wasmtime"))] +pub enum VmCaller<'a, T> { + #[cfg(feature = "wasmi")] + WasmiCaller(wasmi::Caller<'a, T>), + #[cfg(feature = "wasmtime")] + WasmtimeCaller(wasmtime::Caller<'a, T>), + NoCaller, +} +#[cfg(any(feature = "wasmi", feature = "wasmtime"))] impl<'a, T> VmCaller<'a, T> { pub fn none() -> Self { - VmCaller(None) + VmCaller::NoCaller } + #[cfg(feature = "wasmi")] pub fn try_ref(&self) -> Result<&wasmi::Caller<'a, T>, Error> { - match &self.0 { - Some(caller) => Ok(caller), - None => Err(Error::from_type_and_code( + match self { + VmCaller::WasmiCaller(caller) => Ok(caller), + _ => Err(Error::from_type_and_code( ScErrorType::Context, ScErrorCode::InternalError, )), } } + #[cfg(feature = "wasmi")] pub fn try_mut(&mut self) -> Result<&mut wasmi::Caller<'a, T>, Error> { - match &mut self.0 { - Some(caller) => Ok(caller), - None => Err(Error::from_type_and_code( + match self { + VmCaller::WasmiCaller(caller) => Ok(caller), + _ => Err(Error::from_type_and_code( + ScErrorType::Context, + ScErrorCode::InternalError, + )), + } + } + #[cfg(feature = "wasmtime")] + pub fn try_ref_wasmtime(&self) -> Result<&wasmtime::Caller<'a, T>, Error> { + match self { + VmCaller::WasmtimeCaller(caller) => Ok(caller), + _ => Err(Error::from_type_and_code( + ScErrorType::Context, + ScErrorCode::InternalError, + )), + } + } + #[cfg(feature = "wasmtime")] + pub fn try_mut_wasmtime(&mut self) -> Result<&mut wasmtime::Caller<'a, T>, Error> { + match self { + VmCaller::WasmtimeCaller(caller) => Ok(caller), + _ => Err(Error::from_type_and_code( ScErrorType::Context, ScErrorCode::InternalError, )), @@ -52,11 +81,11 @@ impl<'a, T> VmCaller<'a, T> { } } -#[cfg(not(feature = "wasmi"))] +#[cfg(not(any(feature = "wasmi", feature = "wasmtime")))] pub struct VmCaller<'a, T> { _nothing: PhantomData<&'a T>, } -#[cfg(not(feature = "wasmi"))] +#[cfg(not(any(feature = "wasmi", feature = "wasmtime")))] impl<'a, T> VmCaller<'a, T> { pub fn none() -> Self { VmCaller { diff --git a/soroban-env-host/Cargo.toml b/soroban-env-host/Cargo.toml index a5174cbf6..5633e1b04 100644 --- a/soroban-env-host/Cargo.toml +++ b/soroban-env-host/Cargo.toml @@ -15,8 +15,9 @@ exclude = ["observations/"] [dependencies] soroban-builtin-sdk-macros = { workspace = true } -soroban-env-common = { workspace = true, features = ["std", "wasmi", "shallow-val-hash"] } -wasmi = { workspace = true } +soroban-env-common = { workspace = true, features = ["std", "shallow-val-hash"] } +wasmi = { workspace = true, optional = true } +wasmtime = { workspace = true, optional = true } wasmparser = { workspace = true } stellar-strkey = "0.0.9" static_assertions = "1.1.0" @@ -90,6 +91,9 @@ default-features = false features = ["arbitrary"] [features] +default = ["wasmi", "wasmtime"] +wasmi = ["dep:wasmi", "soroban-env-common/wasmi"] +wasmtime = ["dep:wasmtime", "soroban-env-common/wasmtime"] testutils = ["soroban-env-common/testutils", "recording_mode"] backtrace = ["dep:backtrace"] next = ["soroban-env-common/next", "stellar-xdr/next"] diff --git a/soroban-env-host/src/budget.rs b/soroban-env-host/src/budget.rs index 453f77f55..9373017b1 100644 --- a/soroban-env-host/src/budget.rs +++ b/soroban-env-host/src/budget.rs @@ -2,13 +2,21 @@ mod dimension; mod limits; mod model; mod util; +#[cfg(feature = "wasmi")] mod wasmi_helper; +#[cfg(feature = "wasmtime")] +mod wasmtime_helper; pub(crate) use limits::DepthLimiter; pub use limits::{DEFAULT_HOST_DEPTH_LIMIT, DEFAULT_XDR_RW_LIMITS}; pub use model::{MeteredCostComponent, ScaledU64}; + +#[cfg(feature = "wasmi")] pub(crate) use wasmi_helper::{get_wasmi_config, load_calibrated_fuel_costs}; +#[cfg(feature = "wasmtime")] +pub(crate) use wasmtime_helper::get_wasmtime_config; + use std::{ cell::{RefCell, RefMut}, fmt::{Debug, Display}, diff --git a/soroban-env-host/src/budget/wasmtime_helper.rs b/soroban-env-host/src/budget/wasmtime_helper.rs new file mode 100644 index 000000000..4cad3afe9 --- /dev/null +++ b/soroban-env-host/src/budget/wasmtime_helper.rs @@ -0,0 +1,15 @@ +use crate::{budget::Budget, HostError}; + +pub(crate) fn get_wasmtime_config(_budget: &Budget) -> Result { + let mut config = wasmtime::Config::new(); + config + .strategy(wasmtime::Strategy::Winch) + .debug_info(false) + .generate_address_map(false) + .consume_fuel(true) + .wasm_bulk_memory(true) + .wasm_multi_value(false) + .wasm_simd(false) + .wasm_tail_call(false); + Ok(config) +} 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 76dee7c99..1f252e238 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 @@ -86,6 +86,7 @@ mod v21 { host.get_ledger_protocol_version() .expect("protocol version"), sample.module.wasmi_module.engine(), + sample.module.wasmtime_module.engine(), &sample.wasm[..], sample.module.cost_inputs.clone(), ) diff --git a/soroban-env-host/src/host.rs b/soroban-env-host/src/host.rs index f64506d63..a89631711 100644 --- a/soroban-env-host/src/host.rs +++ b/soroban-env-host/src/host.rs @@ -92,6 +92,7 @@ pub(crate) const MIN_LEDGER_PROTOCOL_VERSION: u32 = 22; #[derive(Clone, Default)] struct HostImpl { module_cache: RefCell>, + last_vm_fuel: RefCell, source_account: RefCell>, ledger: RefCell>, objects: RefCell>, @@ -216,6 +217,12 @@ impl_checked_borrow_helpers!( try_borrow_module_cache, try_borrow_module_cache_mut ); +impl_checked_borrow_helpers!( + last_vm_fuel, + u64, + try_borrow_last_vm_fuel, + try_borrow_last_vm_fuel_mut +); impl_checked_borrow_helpers!( source_account, Option, @@ -353,6 +360,7 @@ impl Host { let _client = tracy_client::Client::start(); Self(Rc::new(HostImpl { module_cache: RefCell::new(None), + last_vm_fuel: RefCell::new(0), source_account: RefCell::new(None), ledger: RefCell::new(None), objects: Default::default(), @@ -428,6 +436,15 @@ impl Host { }) } + pub(crate) fn get_last_vm_fuel(&self) -> Result { + Ok(*self.try_borrow_last_vm_fuel()?) + } + + pub(crate) fn set_last_vm_fuel(&self, fuel: u64) -> Result<(), HostError> { + *self.try_borrow_last_vm_fuel_mut()? = fuel; + Ok(()) + } + #[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 { diff --git a/soroban-env-host/src/host/error.rs b/soroban-env-host/src/host/error.rs index a9275c820..a329a0848 100644 --- a/soroban-env-host/src/host/error.rs +++ b/soroban-env-host/src/host/error.rs @@ -176,6 +176,31 @@ impl HostError { true } + + // Wasmtime uses anyhow::Error for its error type which may carry either a + // HostError or a wasmtime::Trap, or "something else entirely" since it's a + // dyn Error type. This is a somewhat different pattern to what we have in + // wasmi. + #[cfg(feature = "wasmtime")] + pub fn map_wasmtime_error(r: Result) -> Result { + match r { + Ok(t) => Ok(t), + Err(e) => match e.downcast::() { + Ok(hosterror) => Err(hosterror), + Err(e) => { + let e = if let Some(trap) = e.root_cause().downcast_ref::() { + HostError::from(Error::from(*trap)) + } else { + HostError::from(Error::from_type_and_code( + ScErrorType::WasmVm, + ScErrorCode::InvalidAction, + )) + }; + Err(e) + } + }, + } + } } impl From for HostError @@ -245,6 +270,8 @@ pub trait ErrorHandler { where Error: From, E: Debug; + #[cfg(feature = "wasmtime")] + fn map_wasmtime_error(&self, r: Result) -> Result; fn error(&self, error: Error, msg: &str, args: &[Val]) -> HostError; } @@ -285,6 +312,35 @@ impl ErrorHandler for Host { }) } + // Wasmtime uses anyhow::Error for its error type which may carry either a + // HostError or a wasmtime::Trap, or "something else entirely" since it's a + // dyn Error type. This is a somewhat different pattern to what we have in + // wasmi. + #[cfg(feature = "wasmtime")] + fn map_wasmtime_error(&self, r: Result) -> Result { + match r { + Ok(t) => Ok(t), + Err(e) => match e.downcast::() { + Ok(hosterror) => Err(hosterror), + Err(e) => { + let e = if let Some(trap) = e.root_cause().downcast_ref::() { + self.error(Error::from(*trap), "wasmtime trap", &[]) + } else { + self.error( + Error::from_type_and_code( + ScErrorType::WasmVm, + ScErrorCode::InvalidAction, + ), + "wasmtime error", + &[], + ) + }; + Err(e) + } + }, + } + } + /// At minimum constructs and returns a [HostError] built from the provided /// [Error], and when running in [DiagnosticMode::Debug] additionally /// records a diagnostic event with the provided `msg` and `args` and then diff --git a/soroban-env-host/src/host/mem_helper.rs b/soroban-env-host/src/host/mem_helper.rs index 788bd704f..000f5f55a 100644 --- a/soroban-env-host/src/host/mem_helper.rs +++ b/soroban-env-host/src/host/mem_helper.rs @@ -81,11 +81,24 @@ impl Host { buf: &[u8], ) -> Result<(), HostError> { self.charge_budget(ContractCostType::MemCpy, Some(buf.len() as u64))?; - let mem = vm.get_memory(self)?; - self.map_err( - mem.write(vmcaller.try_mut()?, mem_pos as usize, buf) - .map_err(|me| wasmi::Error::Memory(me)), - ) + match vmcaller { + VmCaller::WasmiCaller(ctx) => { + let mem = vm.get_memory(self)?; + self.map_err( + mem.write(ctx, mem_pos as usize, buf) + .map_err(|me| wasmi::Error::Memory(me)), + ) + } + VmCaller::WasmtimeCaller(ctx) => { + let mem = vm.get_wasmtime_memory(self)?; + self.map_err(mem.write(ctx, mem_pos as usize, buf)) + } + _ => Err(crate::Error::from_type_and_code( + ScErrorType::Context, + ScErrorCode::InternalError, + ) + .into()), + } } pub(crate) fn metered_vm_read_bytes_from_linear_memory( @@ -96,11 +109,71 @@ impl Host { buf: &mut [u8], ) -> Result<(), HostError> { self.charge_budget(ContractCostType::MemCpy, Some(buf.len() as u64))?; - let mem = vm.get_memory(self)?; - self.map_err( - mem.read(vmcaller.try_mut()?, mem_pos as usize, buf) - .map_err(|me| wasmi::Error::Memory(me)), - ) + + match vmcaller { + VmCaller::WasmiCaller(ctx) => { + let mem = vm.get_memory(self)?; + self.map_err( + mem.read(ctx, mem_pos as usize, buf) + .map_err(|me| wasmi::Error::Memory(me)), + ) + } + VmCaller::WasmtimeCaller(ctx) => { + let mem = vm.get_wasmtime_memory(self)?; + self.map_err(mem.read(ctx, mem_pos as usize, buf)) + } + _ => Err(crate::Error::from_type_and_code( + ScErrorType::Context, + ScErrorCode::InternalError, + ) + .into()), + } + } + + #[allow(clippy::needless_lifetimes)] + fn get_data_mut<'host, 'caller, 'vm>( + &'host self, + vmcaller: &'caller mut VmCaller, + vm: &'vm Rc, + ) -> Result<&'caller mut [u8], HostError> { + match vmcaller { + VmCaller::WasmiCaller(ctx) => { + let mem = vm.get_memory(self)?; + Ok(mem.data_mut(ctx)) + } + VmCaller::WasmtimeCaller(ctx) => { + let mem = vm.get_wasmtime_memory(self)?; + Ok(mem.data_mut(ctx)) + } + _ => Err(crate::Error::from_type_and_code( + ScErrorType::Context, + ScErrorCode::InternalError, + ) + .into()), + } + } + + #[allow(clippy::needless_lifetimes)] + fn get_data<'host, 'caller, 'vm>( + &'host self, + vmcaller: &'caller VmCaller, + vm: &'vm Rc, + ) -> Result<&'caller [u8], HostError> { + match vmcaller { + VmCaller::WasmiCaller(ctx) => { + let mem = vm.get_memory(self)?; + Ok(mem.data(ctx)) + } + VmCaller::WasmtimeCaller(ctx) => { + let mem = vm.get_wasmtime_memory(self)?; + Ok(mem.data(ctx)) + } + _ => Err(crate::Error::from_type_and_code( + ScErrorType::Context, + ScErrorCode::InternalError, + ) + .into()), + } } // Note on metering: covers the cost of memcpy from bytes into the linear memory. @@ -125,7 +198,7 @@ impl Host { .ok_or_else(|| self.err_arith_overflow())?; let mem_range = (mem_pos as usize)..(mem_end as usize); - let mem_data = vm.get_memory(self)?.data_mut(vmcaller.try_mut()?); + let mem_data = self.get_data_mut(vmcaller, vm)?; let mem_slice = mem_data .get_mut(mem_range) .ok_or_else(|| self.err_oob_linear_memory())?; @@ -169,7 +242,7 @@ impl Host { .ok_or_else(|| self.err_arith_overflow())?; let mem_range = (mem_pos as usize)..(mem_end as usize); - let mem_data = vm.get_memory(self)?.data(vmcaller.try_mut()?); + let mem_data = self.get_data(vmcaller, vm)?; let mem_slice = mem_data .get(mem_range) .ok_or_else(|| self.err_oob_linear_memory())?; @@ -218,7 +291,7 @@ impl Host { num_slices: usize, mut callback: impl FnMut(usize, &[u8]) -> Result<(), HostError>, ) -> Result<(), HostError> { - let mem_data = vm.get_memory(self)?.data(vmcaller.try_mut()?); + let mem_data = self.get_data(vmcaller, vm)?; // charge the cost of copying the slices (pointers to the content, not // the content themselves) upfront. self.charge_budget( diff --git a/soroban-env-host/src/lib.rs b/soroban-env-host/src/lib.rs index 9c0343cfa..c310eeeda 100644 --- a/soroban-env-host/src/lib.rs +++ b/soroban-env-host/src/lib.rs @@ -54,6 +54,14 @@ pub mod fees; #[doc(hidden)] pub use host::{TraceEvent, TraceHook, TraceRecord, TraceState}; +#[doc(hidden)] +#[cfg(feature = "wasmi")] +pub use wasmi; + +#[doc(hidden)] +#[cfg(feature = "wasmtime")] +pub use wasmtime; + #[cfg(feature = "bench")] #[doc(hidden)] pub mod cost_runner; diff --git a/soroban-env-host/src/vm.rs b/soroban-env-host/src/vm.rs index 4949f9f27..5dfa58106 100644 --- a/soroban-env-host/src/vm.rs +++ b/soroban-env-host/src/vm.rs @@ -18,9 +18,10 @@ mod parsed_module; pub(crate) use dispatch::dummy0; #[cfg(test)] pub(crate) use dispatch::protocol_gated_dummy; +use soroban_env_common::WasmtimeMarshal; use crate::{ - budget::{get_wasmi_config, AsBudget, Budget}, + budget::{get_wasmi_config, get_wasmtime_config, AsBudget, Budget}, host::{ error::TryBorrowOrErr, metered_clone::MeteredContainer, @@ -89,6 +90,10 @@ pub struct Vm { wasmi_store: RefCell>, wasmi_instance: wasmi::Instance, pub(crate) wasmi_memory: Option, + + wasmtime_store: RefCell>, + wasmtime_instance: wasmtime::Instance, + pub(crate) wasmtime_memory: Option, } impl std::hash::Hash for Vm { @@ -125,6 +130,34 @@ impl Host { } Ok(linker) } + + // Make a wasmtime linker restricted to _only_ importing the symbols + // mentioned in `symbols`. + pub(crate) fn make_minimal_wasmtime_linker_for_symbols( + context: &Ctx, + engine: &wasmtime::Engine, + symbols: &BTreeSet<(&str, &str)>, + ) -> Result, HostError> { + let mut linker = wasmtime::Linker::new(engine); + for hf in HOST_FUNCTIONS { + if symbols.contains(&(hf.mod_str, hf.fn_str)) { + context.map_wasmtime_error((hf.wrap_wasmtime)(&mut linker))?; + } + } + Ok(linker) + } + + // Make a wasmtime linker that imports all the symbols. + pub(crate) fn make_maximal_wasmtime_linker( + context: &Ctx, + engine: &wasmtime::Engine, + ) -> Result, HostError> { + let mut linker = wasmtime::Linker::new(engine); + for hf in HOST_FUNCTIONS { + context.map_wasmtime_error((hf.wrap_wasmtime)(&mut linker))?; + } + Ok(linker) + } } // In one very narrow context -- when recording, and with a module cache -- we @@ -195,6 +228,40 @@ impl Vm { Ok((store, instance, memory)) } + fn instantiate_wasmtime( + host: &Host, + parsed_module: &Arc, + wasmtime_linker: &wasmtime::Linker, + ) -> Result< + ( + wasmtime::Store, + wasmtime::Instance, + Option, + ), + HostError, + > { + let _span = tracy_span!("Vm::instantiate_wasmtime"); + + let wasmtime_engine = parsed_module.wasmtime_module.engine(); + let mut wasmtime_store = { + let _span = tracy_span!("Vm::instantiate_wasmtime - store"); + wasmtime::Store::new(&wasmtime_engine, host.clone()) + }; + let wasmtime_instance = { + let _span = tracy_span!("Vm::instantiate_wasmtime - instantiate"); + host.map_wasmtime_error( + wasmtime_linker.instantiate(&mut wasmtime_store, &parsed_module.wasmtime_module), + )? + }; + let wasmtime_memory = + if let Some(ext) = wasmtime_instance.get_export(&mut wasmtime_store, "memory") { + ext.into_memory() + } else { + None + }; + Ok((wasmtime_store, wasmtime_instance, wasmtime_memory)) + } + /// Instantiates a VM given the arguments provided in [`Self::new`], /// or [`Self::new_from_module_cache`] fn instantiate( @@ -202,6 +269,7 @@ impl Vm { contract_id: Hash, parsed_module: Arc, wasmi_linker: &wasmi::Linker, + wasmtime_linker: &wasmtime::Linker, ) -> Result, HostError> { let _span = tracy_span!("Vm::instantiate"); @@ -214,6 +282,9 @@ impl Vm { let (wasmi_store, wasmi_instance, wasmi_memory) = Self::instantiate_wasmi(host, &parsed_module, wasmi_linker)?; + let (wasmtime_store, wasmtime_instance, wasmtime_memory) = + Self::instantiate_wasmtime(host, &parsed_module, wasmtime_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 // boundary. @@ -223,6 +294,9 @@ impl Vm { wasmi_store: RefCell::new(wasmi_store), wasmi_instance, wasmi_memory, + wasmtime_store: RefCell::new(wasmtime_store), + wasmtime_instance, + wasmtime_memory, })) } @@ -234,10 +308,23 @@ impl Vm { let _span = tracy_span!("Vm::from_parsed_module"); VmInstantiationTimer::new(host.clone()); if let Some(cache) = &*host.try_borrow_module_cache()? { - Self::instantiate(host, contract_id, parsed_module, &cache.wasmi_linker) + Self::instantiate( + host, + contract_id, + parsed_module, + &cache.wasmi_linker, + &cache.wasmtime_linker, + ) } else { let wasmi_linker = parsed_module.make_wasmi_linker(host)?; - Self::instantiate(host, contract_id, parsed_module, &wasmi_linker) + let wasmtime_linker = parsed_module.make_wasmtime_linker(host)?; + Self::instantiate( + host, + contract_id, + parsed_module, + &wasmi_linker, + &wasmtime_linker, + ) } } @@ -284,7 +371,14 @@ impl Vm { VmInstantiationTimer::new(host.clone()); let parsed_module = Self::parse_module(host, wasm, cost_inputs, cost_mode)?; let wasmi_linker = parsed_module.make_wasmi_linker(host)?; - Self::instantiate(host, contract_id, parsed_module, &wasmi_linker) + let wasmtime_linker = parsed_module.make_wasmtime_linker(host)?; + Self::instantiate( + host, + contract_id, + parsed_module, + &wasmi_linker, + &wasmtime_linker, + ) } #[cfg(not(any(test, feature = "recording_mode")))] @@ -361,6 +455,18 @@ impl Vm { } } + pub(crate) fn get_wasmtime_memory(&self, host: &Host) -> Result { + match self.wasmtime_memory { + Some(mem) => Ok(mem), + None => Err(host.err( + ScErrorType::WasmVm, + ScErrorCode::MissingValue, + "no linear memory named `memory`", + &[], + )), + } + } + // Wrapper for the [`Func`] call which is metered as a component. // Resolves the function entity, and takes care the conversion between and // tranfering of the host budget / VM fuel. This is where the host->VM->host @@ -420,6 +526,8 @@ impl Vm { self.wasmi_store .try_borrow_mut_or_err()? .add_fuel_to_vm(host)?; + host.set_last_vm_fuel(added_fuel)?; + // 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. @@ -433,9 +541,10 @@ 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. + let last_fuel = host.get_last_vm_fuel()?; self.wasmi_store .try_borrow_mut_or_err()? - .return_fuel_to_host(host)?; + .return_fuel_to_host(host, last_fuel)?; if let Err(e) = res { use std::borrow::Cow; @@ -483,6 +592,93 @@ impl Vm { ) } + pub(crate) fn metered_wasmtime_func_call( + self: &Rc, + host: &Host, + func_sym: &Symbol, + inputs: &[wasmtime::Val], + treat_missing_function_as_noop: bool, + ) -> Result { + let _span = tracy_span!("Vm::metered_wasmtime_func_call"); + + host.charge_budget(ContractCostType::InvokeVmFunction, None)?; + + // resolve the function entity to be called + let func_ss: SymbolStr = func_sym.try_into_val(host)?; + let ext = match self.wasmtime_instance.get_export( + &mut *self.wasmtime_store.try_borrow_mut_or_err()?, + func_ss.as_ref(), + ) { + None => { + if treat_missing_function_as_noop { + return Ok(Val::VOID.into()); + } else { + return Err(host.err( + ScErrorType::WasmVm, + ScErrorCode::MissingValue, + "trying to invoke non-existent contract function", + &[func_sym.to_val()], + )); + } + } + Some(e) => e, + }; + let func = match ext.into_func() { + None => { + return Err(host.err( + ScErrorType::WasmVm, + ScErrorCode::UnexpectedType, + "trying to invoke Wasm export that is not a function", + &[func_sym.to_val()], + )) + } + Some(e) => e, + }; + + if inputs.len() > Vm::MAX_VM_ARGS { + return Err(host.err( + ScErrorType::WasmVm, + ScErrorCode::InvalidInput, + "Too many arguments in Wasm invocation", + &[func_sym.to_val()], + )); + } + + // call the function + let mut wasm_ret: [wasmtime::Val; 1] = [wasmtime::Val::I64(0)]; + let added_fuel = self + .wasmtime_store + .try_borrow_mut_or_err()? + .add_fuel_to_vm(host)?; + host.set_last_vm_fuel(added_fuel)?; + + let res = { + let _span = tracy_span!("Vm::metered_wasmtime_func_call - actual call"); + func.call( + &mut *self.wasmtime_store.try_borrow_mut_or_err()?, + inputs, + &mut wasm_ret, + ) + }; + + let last_fuel = host.get_last_vm_fuel()?; + self.wasmtime_store + .try_borrow_mut_or_err()? + .return_fuel_to_host(host, last_fuel)?; + + if let Err(e) = res { + // FIXME: this needs to be fairly careful about correct propagation. + // currently we're just doing a crude downcast attempt. + return host.map_wasmtime_error(Err(e)); + } + host.relative_to_absolute( + Val::try_marshal_from_wasmtime_value(wasm_ret[0]).ok_or(ConversionError)?, + ) + } + + // FIXME: remove when/if we decide to commit to this transition. + const FIRST_PROTOCOL_TO_RUN_ON_WASMTIME: u32 = 21; + pub(crate) fn invoke_function_raw( self: &Rc, host: &Host, @@ -492,16 +688,32 @@ impl Vm { ) -> 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 - .iter() - .map(|i| host.absolute_to_relative(*i).map(|v| v.marshal_from_self())) - .collect::, HostError>>()?; - self.metered_func_call( - host, - func_sym, - wasm_args.as_slice(), - treat_missing_function_as_noop, - ) + if host.get_ledger_protocol_version()? >= Self::FIRST_PROTOCOL_TO_RUN_ON_WASMTIME { + let wasmtime_args: Vec = args + .iter() + .map(|i| { + host.absolute_to_relative(*i) + .map(|v| v.marshal_wasmtime_from_self()) + }) + .collect::, HostError>>()?; + self.metered_wasmtime_func_call( + host, + func_sym, + wasmtime_args.as_slice(), + treat_missing_function_as_noop, + ) + } else { + let wasm_args: Vec = args + .iter() + .map(|i| host.absolute_to_relative(*i).map(|v| v.marshal_from_self())) + .collect::, HostError>>()?; + self.metered_func_call( + host, + func_sym, + wasm_args.as_slice(), + treat_missing_function_as_noop, + ) + } } /// Returns the raw bytes content of a named custom section from the WASM @@ -520,7 +732,7 @@ impl Vm { 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.wasmi_instance)); - let mut vmcaller: VmCaller = VmCaller(Some(caller)); + let mut vmcaller: VmCaller = VmCaller::WasmiCaller(caller); f(&mut vmcaller) } diff --git a/soroban-env-host/src/vm/dispatch.rs b/soroban-env-host/src/vm/dispatch.rs index 39997adf6..5b04467f9 100644 --- a/soroban-env-host/src/vm/dispatch.rs +++ b/soroban-env-host/src/vm/dispatch.rs @@ -1,13 +1,12 @@ use super::FuelRefillable; use crate::{ xdr::{ContractCostType, ScErrorCode, ScErrorType}, - CheckedEnvArg, EnvBase, Host, HostError, VmCaller, VmCallerEnv, + CheckedEnvArg, EnvBase, ErrorHandler, Host, HostError, VmCaller, VmCallerEnv, }; use crate::{ - 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, + AddressObject, Bool, BytesObject, DurationObject, Error, 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}; @@ -223,13 +222,14 @@ macro_rules! generate_dispatch_functions { // This is where the VM -> Host boundary is crossed. // We first return all fuels from the VM back to the host such that // the host maintains control of the budget. - FuelRefillable::return_fuel_to_host(&mut caller, &host).map_err(|he| Trap::from(he))?; + let last_fuel = host.get_last_vm_fuel()?; + FuelRefillable::return_fuel_to_host(&mut caller, &host, last_fuel).map_err(|he| Trap::from(he))?; // Charge for the host function dispatching: conversion between VM fuel and // host budget, marshalling values. This does not account for the actual work // being done in those functions, which are metered individually by the implementation. host.charge_budget(ContractCostType::DispatchHostFunction, None)?; - let mut vmcaller = VmCaller(Some(caller)); + let mut vmcaller = VmCaller::WasmiCaller(caller); // The odd / seemingly-redundant use of `wasmi::Value` here // as intermediates -- rather than just passing Vals -- // has to do with the fact that some host functions are @@ -280,7 +280,8 @@ macro_rules! generate_dispatch_functions { // This is where the Host->VM boundary is crossed. // We supply the remaining host budget as fuel to the VM. let caller = vmcaller.try_mut().map_err(|e| Trap::from(HostError::from(e)))?; - FuelRefillable::add_fuel_to_vm(caller, &host).map_err(|he| Trap::from(he))?; + let added_fuel = FuelRefillable::add_fuel_to_vm(caller, &host).map_err(|he| Trap::from(he))?; + host.set_last_vm_fuel(added_fuel)?; res } @@ -291,3 +292,176 @@ macro_rules! generate_dispatch_functions { // Here we invoke the x-macro passing generate_dispatch_functions as its callback macro. call_macro_with_all_host_functions! { generate_dispatch_functions } + +pub(crate) mod wasmtime_dispatch { + use super::*; + + /////////////////////////////////////////////////////////////////////////////// + /// X-macro use: dispatch functions + /////////////////////////////////////////////////////////////////////////////// + + // This is a callback macro that pattern-matches the token-tree passed by the + // x-macro (call_macro_with_all_host_functions) and produces a suite of + // dispatch-function definitions. + macro_rules! generate_wasmtime_dispatch_functions { + { + $( + // This outer pattern matches a single 'mod' block of the token-tree + // passed from the x-macro to this macro. It is embedded in a `$()*` + // pattern-repetition matcher so that it will match all provided + // 'mod' blocks provided. + $(#[$mod_attr:meta])* + mod $mod_name:ident $mod_str:literal + { + $( + // This inner pattern matches a single function description + // inside a 'mod' block in the token-tree passed from the + // x-macro to this macro. It is embedded in a `$()*` + // pattern-repetition matcher so that it will match all such + // descriptions. + $(#[$fn_attr:meta])* + { $fn_str:literal, $($min_proto:literal)?, $($max_proto:literal)?, fn $fn_id:ident ($($arg:ident:$type:ty),*) -> $ret:ty } + )* + } + )* + } + + => // The part of the macro above this line is a matcher; below is its expansion. + + { + // This macro expands to multiple items: a set of free functions in the + // current module, which are called by functions registered with the VM + // to forward calls to the host. + $( + $( + // This defines a "dispatch function" that does several things: + // + // 1. Transfers the running "VM fuel" balance from wasmi to the + // host's CPU budget. + // 2. Charges the host budget for the call, failing if over. + // 3. Attempts to convert incoming wasmi i64 args to Vals or + // Val-wrappers expected by host functions, failing if any + // conversions fail. This step also does + // relative-to-absolute object reference conversion. + // 4. Calls the host function. + // 5. Augments any error result with this calling context, so + // that we get at minimum a "which host function failed" + // context on error. + // 6. Converts the result back to an i64 for wasmi, again + // converting from absolute object references to relative + // along the way. + // 7. Checks the result is Ok, or escalates Err to a VM Trap. + // 8. Transfers the residual CPU budget back to wasmi "VM + // fuel". + // + // It is embedded in two nested `$()*` pattern-repetition + // expanders that correspond to the pattern-repetition matchers + // in the match section, but we ignore the structure of the + // 'mod' block repetition-level from the outer pattern in the + // expansion, flattening all functions from all 'mod' blocks + // into a set of functions. + $(#[$fn_attr])* + pub(crate) fn $fn_id(mut caller: wasmtime::Caller<'_, Host>, $($arg:i64),*) -> + Result<(i64,), wasmtime::Error> + { + let _span = tracy_span!(core::stringify!($fn_id)); + + let host = caller.data().clone(); + + // This is an additional protocol version guardrail that + // should not be necessary. Any wasm contract containing a + // call to an out-of-protocol-range host function should + // have been rejected by the linker during VM instantiation. + // This is just an additional guard rail for future proof. + $( host.check_protocol_version_lower_bound($min_proto)?; )? + $( host.check_protocol_version_upper_bound($max_proto)?; )? + + if host.tracing_enabled() + { + #[allow(unused)] + let trace_args = ($( + match <$type>::try_marshal_from_relative_value(Value::I64($arg), &host) { + Ok(val) => TraceArg::Ok(val), + Err(_) => TraceArg::Bad($arg), + } + ),*); + let hook_args: &[&dyn std::fmt::Debug] = homogenize_tuple!(trace_args, ($($arg),*)); + host.trace_env_call(&core::stringify!($fn_id), hook_args)?; + } + + // This is where the VM -> Host boundary is crossed. + // We first return all fuels from the VM back to the host such that + // the host maintains control of the budget. + + let last_fuel = host.get_last_vm_fuel()?; + FuelRefillable::return_fuel_to_host(&mut caller, &host, last_fuel).map_err(|he| Trap::from(he))?; + + // Charge for the host function dispatching: conversion between VM fuel and + // host budget, marshalling values. This does not account for the actual work + // being done in those functions, which are metered individually by the implementation. + host.charge_budget(ContractCostType::DispatchHostFunction, None)?; + + let mut vmcaller = VmCaller::WasmtimeCaller(caller); + + // The odd / seemingly-redundant use of `wasmi::Value` here + // as intermediates -- rather than just passing Vals -- + // has to do with the fact that some host functions are + // typed as receiving or returning plain _non-val_ i64 or + // u64 values. So the call here has to be able to massage + // both types into and out of i64, and `wasmi::Value` + // happens to be a natural switching point for that: we have + // conversions to and from both Val and i64 / u64 for + // wasmi::Value. + let res: Result<_, HostError> = host.$fn_id(&mut vmcaller, $(<$type>::check_env_arg(<$type>::try_marshal_from_relative_value(Value::I64($arg), &host)?, &host)?),*); + + if host.tracing_enabled() + { + let dyn_res: Result<&dyn core::fmt::Debug,&HostError> = match &res { + Ok(ref ok) => Ok(ok), + Err(err) => Err(err) + }; + host.trace_env_ret(&core::stringify!($fn_id), &dyn_res)?; + } + + // On the off chance we got an error with no context, we can + // at least attach some here "at each host function call", + // fairly systematically. This will cause the context to + // propagate back through wasmi to its caller. + let res = host.augment_err_result(res); + + let res = match res { + Ok(ok) => { + let ok = ok.check_env_arg(&host)?; + let val: Value = ok.marshal_relative_from_self(&host)?; + if let Value::I64(v) = val { + Ok((v,)) + } else { + Err(BadSignature.into()) + } + }, + Err(hosterr) => { + // We make a new HostError here to capture the escalation event itself. + let escalation: HostError = + host.error(hosterr.error, + concat!("escalating error to VM trap from failed host function call: ", + stringify!($fn_id)), &[]); + let trap: Trap = escalation.into(); + Err(trap) + } + }; + + // This is where the Host->VM boundary is crossed. + // We supply the remaining host budget as fuel to the VM. + let caller = vmcaller.try_mut_wasmtime().map_err(|e| Trap::from(HostError::from(e)))?; + let added_fuel = FuelRefillable::add_fuel_to_vm(caller, &host).map_err(|he| Trap::from(he))?; + host.set_last_vm_fuel(added_fuel)?; + + Ok(res?) + } + )* + )* + }; +} + + call_macro_with_all_host_functions! { generate_wasmtime_dispatch_functions } +} diff --git a/soroban-env-host/src/vm/fuel_refillable.rs b/soroban-env-host/src/vm/fuel_refillable.rs index 68448fcef..5f36b9a69 100644 --- a/soroban-env-host/src/vm/fuel_refillable.rs +++ b/soroban-env-host/src/vm/fuel_refillable.rs @@ -4,10 +4,17 @@ use crate::{ Host, HostError, }; +use soroban_env_common::Error; use wasmi::{errors::FuelError, Caller, Store}; pub(crate) trait FuelRefillable { - fn fuel_consumed(&self) -> Result; + // Returns the amount of fuel consumed by the VM since the last call to + // `reset_fuel` / `add_fuel`. This is somewhat error-prone but the idea is + // that some VMs keep track of "how much fuel is left" and others keep track + // of "how much fuel was consumed". The former needs to be provided with the + // actual last fuel amount that the VM was filled with, in order to + // calculate the consumed amount. + fn fuel_consumed(&self, last_fuel: u64) -> Result; fn fuel_total(&self) -> Result; @@ -16,10 +23,13 @@ pub(crate) trait FuelRefillable { fn reset_fuel(&mut self) -> Result<(), HostError>; fn is_clean(&self) -> Result { - Ok(self.fuel_consumed()? == 0 && self.fuel_total()? == 0) + Ok(self.fuel_consumed(self.fuel_total()?)? == 0 && self.fuel_total()? == 0) } - fn add_fuel_to_vm(&mut self, host: &Host) -> Result<(), HostError> { + // Asserts that the VM has no fuel in it, then calculates the current amount + // of VM fuel the host's current CPU budget represents, and adds that fuel + // to the VM. Returns the amount of fuel added. + fn add_fuel_to_vm(&mut self, host: &Host) -> Result { if !self.is_clean()? { return Err(host.err( ScErrorType::WasmVm, @@ -29,13 +39,20 @@ pub(crate) trait FuelRefillable { )); } let fuel = host.as_budget().get_wasmi_fuel_remaining()?; - self.add_fuel(fuel) + self.add_fuel(fuel)?; + Ok(fuel) } - fn return_fuel_to_host(&mut self, host: &Host) -> Result<(), HostError> { - let fuel = self.fuel_consumed()?; + // Computes the amount of fuel consumed by the VM since the last call to + // `add_fuel_to_vm`, and charges it to the host's CPU budget, logically + // accounting for a "return" of the remainder that was _not_ consumed to the + // host for further use. Takes the last fuel amount supplied to the VM + // (which was returned from `add_fuel_to_vm`) as an argument, in case the VM + // does not keep track of its fuel consumption, only remaining balance. + fn return_fuel_to_host(&mut self, host: &Host, last_fuel: u64) -> Result<(), HostError> { + let fuel_consumed = self.fuel_consumed(last_fuel)?; host.as_budget() - .bulk_charge(ContractCostType::WasmInsnExec, fuel, None)?; + .bulk_charge(ContractCostType::WasmInsnExec, fuel_consumed, None)?; self.reset_fuel() } } @@ -43,7 +60,7 @@ pub(crate) trait FuelRefillable { macro_rules! impl_refillable_for_store { ($store: ty) => { impl<'a> FuelRefillable for $store { - fn fuel_consumed(&self) -> Result { + fn fuel_consumed(&self, _initial_fuel: u64) -> Result { self.fuel_consumed().ok_or_else(|| { HostError::from(wasmi::Error::Store(FuelError::FuelMeteringDisabled)) }) @@ -69,3 +86,41 @@ macro_rules! impl_refillable_for_store { } impl_refillable_for_store!(Store); impl_refillable_for_store!(Caller<'a, Host>); + +const VM_INTERNAL_ERROR: Error = + Error::from_type_and_code(ScErrorType::WasmVm, ScErrorCode::InternalError); + +const WASMTIME_FUEL_FACTOR: u64 = 1; + +macro_rules! impl_refillable_for_wasmtime_store { + ($store: ty) => { + impl<'a> FuelRefillable for $store { + fn fuel_consumed(&self, initial_fuel: u64) -> Result { + let fuel = self.fuel_total()?; + Ok(initial_fuel.saturating_sub(fuel)) + } + + fn fuel_total(&self) -> Result { + self.get_fuel() + .map(|fuel| fuel.saturating_div(WASMTIME_FUEL_FACTOR)) + .map_err(|_| HostError::from(VM_INTERNAL_ERROR)) + } + + fn add_fuel(&mut self, fuel: u64) -> Result<(), HostError> { + let existing_fuel = self.fuel_total()?; + let new_fuel = existing_fuel + .saturating_add(fuel) + .saturating_mul(WASMTIME_FUEL_FACTOR); + self.set_fuel(new_fuel) + .map_err(|_| HostError::from(VM_INTERNAL_ERROR)) + } + + fn reset_fuel(&mut self) -> Result<(), HostError> { + self.set_fuel(0) + .map_err(|_| HostError::from(VM_INTERNAL_ERROR)) + } + } + }; +} +impl_refillable_for_wasmtime_store!(wasmtime::Store); +impl_refillable_for_wasmtime_store!(wasmtime::Caller<'a, Host>); diff --git a/soroban-env-host/src/vm/func_info.rs b/soroban-env-host/src/vm/func_info.rs index d70d9faa5..1435543ae 100644 --- a/soroban-env-host/src/vm/func_info.rs +++ b/soroban-env-host/src/vm/func_info.rs @@ -20,6 +20,12 @@ pub(crate) struct HostFuncInfo { /// into a Func in the Linker. pub(crate) wrap: fn(&mut Linker) -> Result<&mut Linker, LinkerError>, + /// Function that takes a wasmtime::Linker and adds a dispatch function + /// for this host function, with the specific type of the dispatch function, + /// into a Func in the Linker. + pub(crate) wrap_wasmtime: + fn(&mut wasmtime::Linker) -> Result<&mut wasmtime::Linker, wasmtime::Error>, + /// Minimal supported protocol version of this host function pub(crate) min_proto: Option, @@ -46,6 +52,7 @@ macro_rules! host_function_info_helper { fn_str: $fn_id, arity: fn_arity!($args), wrap: |linker| linker.func_wrap($mod_str, $fn_id, dispatch::$func_id), + wrap_wasmtime: |linker| linker.func_wrap($mod_str, $fn_id, dispatch::wasmtime_dispatch::$func_id), min_proto: Some($min_proto), max_proto: Some($max_proto), } @@ -56,6 +63,7 @@ macro_rules! host_function_info_helper { fn_str: $fn_id, arity: fn_arity!($args), wrap: |linker| linker.func_wrap($mod_str, $fn_id, dispatch::$func_id), + wrap_wasmtime: |linker| linker.func_wrap($mod_str, $fn_id, dispatch::wasmtime_dispatch::$func_id), min_proto: Some($min_proto), max_proto: None, } @@ -66,6 +74,7 @@ macro_rules! host_function_info_helper { fn_str: $fn_id, arity: fn_arity!($args), wrap: |linker| linker.func_wrap($mod_str, $fn_id, dispatch::$func_id), + wrap_wasmtime: |linker| linker.func_wrap($mod_str, $fn_id, dispatch::wasmtime_dispatch::$func_id), min_proto: None, max_proto: Some($max_proto), } @@ -76,6 +85,7 @@ macro_rules! host_function_info_helper { fn_str: $fn_id, arity: fn_arity!($args), wrap: |linker| linker.func_wrap($mod_str, $fn_id, dispatch::$func_id), + wrap_wasmtime: |linker| linker.func_wrap($mod_str, $fn_id, dispatch::wasmtime_dispatch::$func_id), min_proto: None, max_proto: None, } diff --git a/soroban-env-host/src/vm/module_cache.rs b/soroban-env-host/src/vm/module_cache.rs index 1736ea018..cc695c5c6 100644 --- a/soroban-env-host/src/vm/module_cache.rs +++ b/soroban-env-host/src/vm/module_cache.rs @@ -3,10 +3,10 @@ use super::{ parsed_module::{CompilationContext, ParsedModule, VersionedContractCodeCostInputs}, }; use crate::{ - budget::{get_wasmi_config, AsBudget, Budget}, + budget::{get_wasmi_config, get_wasmtime_config, AsBudget, Budget}, host::metered_clone::{MeteredClone, MeteredContainer}, xdr::{Hash, ScErrorCode, ScErrorType}, - Host, HostError, MeteredOrdMap, + ErrorHandler, Host, HostError, MeteredOrdMap, }; use std::{ collections::{BTreeMap, BTreeSet}, @@ -21,7 +21,9 @@ use std::{ #[derive(Clone, Default)] pub struct ModuleCache { pub(crate) wasmi_engine: wasmi::Engine, + pub(crate) wasmtime_engine: wasmtime::Engine, pub(crate) wasmi_linker: wasmi::Linker, + pub(crate) wasmtime_linker: wasmtime::Linker, modules: ModuleCacheMap, } @@ -106,17 +108,24 @@ impl ModuleCache { let wasmi_config = get_wasmi_config(host.as_budget())?; let wasmi_engine = wasmi::Engine::new(&wasmi_config); + let wasmtime_config = get_wasmtime_config(host.as_budget())?; + let wasmtime_engine = host.map_wasmtime_error(wasmtime::Engine::new(&wasmtime_config))?; + let modules = ModuleCacheMap::MeteredSingleUseMap(MeteredOrdMap::new()); let wasmi_linker = wasmi::Linker::new(&wasmi_engine); + let wasmtime_linker = wasmtime::Linker::new(&wasmtime_engine); let mut cache = Self { wasmi_engine, + wasmtime_engine, modules, wasmi_linker, + wasmtime_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)?; + cache.wasmtime_linker = cache.make_minimal_wasmtime_linker_for_cached_modules(host)?; Ok(cache) } @@ -124,14 +133,21 @@ impl ModuleCache { let wasmi_config = get_wasmi_config(context.as_budget())?; let wasmi_engine = wasmi::Engine::new(&wasmi_config); + let wasmtime_config = get_wasmtime_config(context.as_budget())?; + let wasmtime_engine = + context.map_wasmtime_error(wasmtime::Engine::new(&wasmtime_config))?; + let modules = ModuleCacheMap::UnmeteredReusableMap(Arc::new(Mutex::new(BTreeMap::new()))); let wasmi_linker = Host::make_maximal_wasmi_linker(context, &wasmi_engine)?; + let wasmtime_linker = Host::make_maximal_wasmtime_linker(context, &wasmtime_engine)?; Ok(Self { wasmi_engine, + wasmtime_engine, modules, wasmi_linker, + wasmtime_linker, }) } @@ -244,6 +260,7 @@ impl ModuleCache { context, curr_ledger_protocol, &self.wasmi_engine, + &self.wasmtime_engine, &wasm, cost_inputs, )?; @@ -300,6 +317,15 @@ impl ModuleCache { }) } + fn make_minimal_wasmtime_linker_for_cached_modules( + &self, + host: &Host, + ) -> Result, HostError> { + self.with_minimal_import_symbols(host, |symbols| { + Host::make_minimal_wasmtime_linker_for_symbols(host, &self.wasmtime_engine, symbols) + }) + } + pub fn contains_module( &self, wasm_hash: &Hash, diff --git a/soroban-env-host/src/vm/parsed_module.rs b/soroban-env-host/src/vm/parsed_module.rs index 2ab62c813..24e52743a 100644 --- a/soroban-env-host/src/vm/parsed_module.rs +++ b/soroban-env-host/src/vm/parsed_module.rs @@ -3,6 +3,7 @@ use crate::{ err, host::metered_clone::MeteredContainer, meta, + vm::get_wasmtime_config, xdr::{ ContractCostType, Limited, ReadXdr, ScEnvMetaEntry, ScEnvMetaEntryInterfaceVersion, ScErrorCode, ScErrorType, @@ -147,6 +148,7 @@ impl CompilationContext for Host {} /// from the module when it was parsed. pub struct ParsedModule { pub wasmi_module: wasmi::Module, + pub wasmtime_module: wasmtime::Module, pub proto_version: u32, pub cost_inputs: VersionedContractCodeCostInputs, } @@ -156,14 +158,21 @@ impl ParsedModule { context: &Ctx, curr_ledger_protocol: u32, wasmi_engine: &wasmi::Engine, + wasmtime_engine: &wasmtime::Engine, wasm: &[u8], cost_inputs: VersionedContractCodeCostInputs, ) -> 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)?; + let (wasmi_module, wasmtime_module, proto_version) = Self::parse_wasm( + context, + curr_ledger_protocol, + wasmi_engine, + wasmtime_engine, + wasm, + )?; Ok(Arc::new(Self { wasmi_module, + wasmtime_module, proto_version, cost_inputs, })) @@ -179,6 +188,26 @@ impl ParsedModule { // we'll leave some future-proofing room here. The important point // is to not be introducing a DoS vector. const SYM_LEN_LIMIT: usize = 10; + + #[cfg(feature = "wasmtime")] + #[allow(unused_variables)] + let symbols: BTreeSet<(&str, &str)> = self + .wasmtime_module + .imports() + .filter_map(|i| { + if i.ty().func().is_some() { + let mod_str = i.module(); + let fn_str = i.name(); + if mod_str.len() < SYM_LEN_LIMIT && fn_str.len() < SYM_LEN_LIMIT { + return Some((mod_str, fn_str)); + } + } + None + }) + .collect(); + + #[cfg(feature = "wasmi")] + #[allow(unused_variables)] let symbols: BTreeSet<(&str, &str)> = self .wasmi_module .imports() @@ -210,6 +239,16 @@ impl ParsedModule { }) } + pub fn make_wasmtime_linker(&self, host: &Host) -> Result, HostError> { + self.with_import_symbols(host, |symbols| { + Host::make_minimal_wasmtime_linker_for_symbols( + host, + self.wasmtime_module.engine(), + symbols, + ) + }) + } + pub fn new_with_isolated_engine( host: &Host, wasm: &[u8], @@ -219,10 +258,14 @@ impl ParsedModule { let wasmi_config = crate::vm::get_wasmi_config(host.as_budget())?; let wasmi_engine = wasmi::Engine::new(&wasmi_config); + let wasmtime_config = get_wasmtime_config(host.as_budget())?; + let wasmtime_engine = host.map_wasmtime_error(wasmtime::Engine::new(&wasmtime_config))?; + Self::new( host, host.get_ledger_protocol_version()?, &wasmi_engine, + &wasmtime_engine, wasm, cost_inputs, ) @@ -233,17 +276,22 @@ impl ParsedModule { context: &Ctx, curr_ledger_protocol: u32, wasmi_engine: &wasmi::Engine, + wasmtime_engine: &wasmtime::Engine, wasm: &[u8], - ) -> Result<(wasmi::Module, u32), HostError> { + ) -> Result<(wasmi::Module, wasmtime::Module, u32), HostError> { let module = { let _span = tracy_span!("wasmi::Module::new"); context.map_err(wasmi::Module::new(&wasmi_engine, wasm))? }; + let wasmtime_module = { + let _span = tracy_span!("wasmtime::Module::new"); + context.map_wasmtime_error(wasmtime::Module::new(&wasmtime_engine, &wasm))? + }; 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)) + Ok((module, wasmtime_module, contract_proto)) } fn check_contract_interface_version(