From 4466087d1b52a6a9bb9f117404c728eb2a11125c Mon Sep 17 00:00:00 2001 From: Dmytro Kozhevin Date: Thu, 20 Jul 2023 14:07:13 -0400 Subject: [PATCH] Move all the metered code to invoke host functions into a single host-side helper. This helper can be invoked from any environment to get the metering data and results consistent with what Core will do. Also provide utilities to build input data for computing the rent fees. --- .cargo/config.toml | 2 +- .../src/cost_runner/cost_types/val_ser.rs | 7 +- soroban-env-host/src/e2e_invoke.rs | 486 ++++++++++++++++++ soroban-env-host/src/host/declared_size.rs | 10 +- soroban-env-host/src/host/metered_clone.rs | 3 +- soroban-env-host/src/host/metered_xdr.rs | 55 +- soroban-env-host/src/lib.rs | 3 +- soroban-env-host/src/test/budget_metering.rs | 6 +- soroban-env-host/src/test/complex.rs | 2 +- 9 files changed, 543 insertions(+), 31 deletions(-) create mode 100644 soroban-env-host/src/e2e_invoke.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index 49143201a..4bb3ce9d0 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -18,7 +18,7 @@ # target = "triple" # build for the target triple (ignored by `cargo install`) # target-dir = "target" # path of where to place all generated artifacts rustflags = [ - "-Dwarnings", + # "-Dwarnings", "-Aclippy::style", # "-Dclippy::pedantic", # "-Aclippy::module_name_repetitions", diff --git a/soroban-env-host/src/cost_runner/cost_types/val_ser.rs b/soroban-env-host/src/cost_runner/cost_types/val_ser.rs index ec496a27d..d870c21e5 100644 --- a/soroban-env-host/src/cost_runner/cost_types/val_ser.rs +++ b/soroban-env-host/src/cost_runner/cost_types/val_ser.rs @@ -1,6 +1,9 @@ use std::hint::black_box; -use crate::{cost_runner::CostRunner, xdr::ContractCostType, xdr::ScVal}; +use crate::{ + cost_runner::CostRunner, host::metered_xdr::metered_write_xdr, xdr::ContractCostType, + xdr::ScVal, +}; pub struct ValSerRun; @@ -18,7 +21,7 @@ impl CostRunner for ValSerRun { ) -> Self::RecycledType { // Note the sample.1 is an empty vector, so metered_write_xdr includes allocation // cost. This is how it's typically used so we are setting it up this way. - black_box(host.metered_write_xdr(&sample.0, &mut sample.1).unwrap()); + black_box(metered_write_xdr(host.budget_ref(), &sample.0, &mut sample.1).unwrap()); sample } diff --git a/soroban-env-host/src/e2e_invoke.rs b/soroban-env-host/src/e2e_invoke.rs new file mode 100644 index 000000000..bd3f175f6 --- /dev/null +++ b/soroban-env-host/src/e2e_invoke.rs @@ -0,0 +1,486 @@ +/// This module contains functionality to invoke host functions in embedder +/// environments using a clean host instance. +/// Also contains helpers for processing the ledger changes caused by these +/// host functions. +use std::{cmp::max, rc::Rc}; + +use soroban_env_common::{ + xdr::{ + AccountId, ContractCodeEntryBody, ContractDataDurability, ContractDataEntryBody, + ContractEntryBodyType, ContractEventType, DiagnosticEvent, HostFunction, LedgerEntry, + LedgerEntryData, LedgerFootprint, LedgerKey, LedgerKeyAccount, LedgerKeyContractCode, + LedgerKeyContractData, LedgerKeyTrustLine, ScErrorCode, ScErrorType, + SorobanAuthorizationEntry, SorobanResources, + }, + Error, +}; + +use crate::{ + budget::Budget, + events::Events, + fees::LedgerEntryRentChange, + host::{ + ledger_info_helper::{get_entry_expiration, get_key_durability, set_entry_expiration}, + metered_clone::{charge_container_bulk_init_with_elts, MeteredClone}, + metered_xdr::{metered_from_xdr_with_budget, metered_write_xdr}, + }, + storage::{AccessType, Footprint, FootprintMap, SnapshotSource, Storage, StorageMap}, + DiagnosticLevel, Host, HostError, LedgerInfo, +}; + +/// Result of invoking a single host function prepared for embedder consumption. +pub struct InvokeHostFunctionResult { + /// Result value of the function, encoded `ScVal` XDR on success, or error. + pub encoded_invoke_result: Result, HostError>, + /// All the ledger changes caused by this invocation, including no-ops. + /// This contains an entry for *every* item in the input footprint, even if + /// it wasn't modified at all. + /// + /// Read-only entry can only have their expiration ledger increased. + /// Read-write entries can be modified arbitrarily or removed. + /// + /// Empty when invocation fails. + pub ledger_changes: Vec, + /// All the events that contracts emitted during invocation, encoded as + /// `ContractEvent` XDR. + /// + /// Empty when invocation fails. + pub encoded_contract_events: Vec>, +} + +/// Represents a change of the ledger entry from 'old' value to the 'new' one. +/// Only contains the final value of the entry (if any) and some minimal +/// information about the old entry for convenience. +#[derive(Default)] +pub struct LedgerEntryChange { + /// Whether the ledger entry is read-only, as defined by the footprint. + pub read_only: bool, + + /// Entry key encoded as `LedgerKey` XDR. + pub encoded_key: Vec, + /// Size of the old entry in bytes. This is size of `LedgerEntry` encoded + /// XDR. + pub old_entry_size_bytes: u32, + /// New value of the ledger entry encoded as `LedgerEntry` XDR. + /// Only set for non-removed, non-readonly values, otherwise `None`. + pub encoded_new_value: Option>, + /// Change of the expiration state of the entry. + /// Only set for entries that have expiration, otherwise `None`. + pub expiration_change: Option, +} + +/// Represents of the expiration-related state of the entry. +pub struct LedgerEntryExpirationChange { + /// Durability of the entry. + pub durability: ContractDataDurability, + /// Expiration ledger of the old entry. + pub old_expiration_ledger: u32, + /// Expiration ledger of the new entry. Guaranteed to always be greater than + /// or equal to `old_expiration_ledger`. + pub new_expiration_ledger: u32, +} + +/// Returns the difference between the `storage` and its initial snapshot as +/// `LedgerEntryChanges`. +/// Returns an entry for every item in `storage` footprint. +pub fn get_ledger_changes( + budget: &Budget, + storage: &Storage, + init_storage_snapshot: &T, +) -> Result, HostError> { + let mut changes = vec![]; + // Skip allocation metering for this for the sake of simplicity - the + // bounding factor here is XDR decoding which is metered. + changes.reserve(storage.map.len()); + + let footprint_map = &storage.footprint.0; + // We return any invariant errors here as internal errors, as they would + // typically mean inconsistency between storage and snapshot that shouldn't + // happen in embedder environments, or simply fundamental invariant bugs. + let internal_error: HostError = + Error::from_type_and_code(ScErrorType::Storage, ScErrorCode::InternalError).into(); + for (key, entry) in storage.map.iter(budget)? { + let mut entry_change = LedgerEntryChange::default(); + metered_write_xdr(budget, key.as_ref(), &mut entry_change.encoded_key)?; + let durability = get_key_durability(key); + if let Some(durability) = durability { + entry_change.expiration_change = Some(LedgerEntryExpirationChange { + durability, + old_expiration_ledger: 0, + new_expiration_ledger: 0, + }); + } + if init_storage_snapshot.has(key)? { + let old_entry = init_storage_snapshot.get(key)?; + let mut buf = vec![]; + metered_write_xdr(budget, old_entry.as_ref(), &mut buf)?; + entry_change.old_entry_size_bytes = buf.len() as u32; + + if let Some(ref mut expiration_change) = &mut entry_change.expiration_change { + expiration_change.old_expiration_ledger = get_entry_expiration(old_entry.as_ref()) + .ok_or_else(|| internal_error.clone())?; + } + } + if let Some(entry) = entry { + if let Some(ref mut expiration_change) = &mut entry_change.expiration_change { + // Never reduce the expiration ledger. This is also handled when + // processing RW entries. + expiration_change.new_expiration_ledger = max( + get_entry_expiration(entry.as_ref()).ok_or_else(|| internal_error.clone())?, + expiration_change.old_expiration_ledger, + ); + } + } + let maybe_access_type: Option = + footprint_map.get::>(key, budget)?.copied(); + match maybe_access_type { + Some(AccessType::ReadOnly) => { + entry_change.read_only = true; + } + Some(AccessType::ReadWrite) => { + if let Some(entry) = entry { + // Handle the edge case where due to combinations of + // deletions/creations the entry expiration has been + // reduced. + let mut entry_written = false; + if let (Some(expiration_change), Some(new_expiration)) = ( + &entry_change.expiration_change, + get_entry_expiration(entry.as_ref()), + ) { + if expiration_change.old_expiration_ledger > new_expiration { + let mut new_entry: LedgerEntry = + entry.as_ref().metered_clone(budget)?; + set_entry_expiration(&mut new_entry, new_expiration); + let mut entry_buf = vec![]; + metered_write_xdr(budget, &new_entry, &mut entry_buf)?; + entry_change.encoded_new_value = Some(entry_buf); + entry_written = true; + } + } + if !entry_written { + let mut entry_buf = vec![]; + metered_write_xdr(budget, entry.as_ref(), &mut entry_buf)?; + entry_change.encoded_new_value = Some(entry_buf); + } + } + } + None => { + return Err(internal_error); + } + } + changes.push(entry_change); + } + Ok(changes) +} + +/// Extracts the rent-related changes from the provided ledger changes. +/// +/// Only meaningful changes are returned (i.e. no-op changes are skipped). +/// +/// Extracted changes can be used to compute the rent fee via `fees::compute_rent_fee`. +pub fn extract_rent_changes(ledger_changes: &Vec) -> Vec { + ledger_changes + .iter() + .filter_map(|entry_change| { + // Rent changes are only relevant to non-removed entries with + // expiration. + if let (Some(expiration_change), Some(encoded_new_value)) = ( + &entry_change.expiration_change, + &entry_change.encoded_new_value, + ) { + // Skip non-bumped entries as well. + if expiration_change.old_expiration_ledger + >= expiration_change.new_expiration_ledger + { + return None; + } + let key_size = entry_change.encoded_key.len() as u32; + let old_size_bytes = if entry_change.old_entry_size_bytes > 0 { + key_size.saturating_add(entry_change.old_entry_size_bytes) + } else { + 0 + }; + Some(LedgerEntryRentChange { + is_persistent: matches!( + expiration_change.durability, + ContractDataDurability::Persistent + ), + old_size_bytes, + new_size_bytes: key_size.saturating_add(encoded_new_value.len() as u32), + old_expiration_ledger: expiration_change.old_expiration_ledger, + new_expiration_ledger: expiration_change.new_expiration_ledger, + }) + } else { + None + } + }) + .collect() +} + +/// Invokes a host function within a fresh host instance. +/// +/// This collects the necessary inputs as encoded XDR and returns the outputs +/// as encoded XDR as well. This is supposed to encapsulate all the metered +/// operations needed to invoke a host function, including the input/output +/// decoding/encoding. +/// +/// In order to get clean budget metering data, a clean budget has to be +/// provided as an input. It can then be examined immediately after execution in +/// order to get the precise metering data. Budget is not reset in case of +/// errors. +/// +/// This may only fail when budget is exceeded or if there is an internal error. +/// Host function invocation errors are stored within +/// `Ok(InvokeHostFunctionResult)`. +/// +/// When diagnostics are enabled, we try to populate `diagnostic_events` +/// even if the `InvokeHostFunctionResult` fails for any reason. +pub fn invoke_host_function, 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, + base_prng_seed: T, + diagnostic_events: &mut Vec, +) -> Result { + let _span0 = tracy_span!("invoke_host_function"); + + let resources: SorobanResources = + metered_from_xdr_with_budget(encoded_resources.as_ref(), &budget)?; + let footprint = build_storage_footprint_from_xdr(&budget, resources.footprint)?; + let map = + build_storage_map_from_xdr_ledger_entries(&budget, &footprint, encoded_ledger_entries)?; + let init_storage_map = map.metered_clone(&budget)?; + + let storage = Storage::with_enforcing_footprint_and_map(footprint, map); + let host = Host::with_storage_and_budget(storage, budget.clone()); + let auth_entries = host.build_auth_entries_from_xdr(encoded_auth_entries)?; + let host_function: HostFunction = host.metered_from_xdr(encoded_host_fn.as_ref())?; + let source_account: AccountId = host.metered_from_xdr(encoded_source_account.as_ref())?; + host.set_source_account(source_account)?; + host.set_ledger_info(ledger_info.into())?; + host.set_authorization_entries(auth_entries)?; + let seed32: [u8; 32] = base_prng_seed.as_ref().try_into().map_err(|_| { + host.err( + ScErrorType::Context, + ScErrorCode::InternalError, + "base PRNG seed is not 32-bytes long", + &[], + ) + })?; + host.set_base_prng_seed(seed32)?; + if enable_diagnostics { + host.set_diagnostic_level(DiagnosticLevel::Debug)?; + } + let result = { + let _span1 = tracy_span!("Host::invoke_function"); + host.invoke_function(host_function) + }; + let (storage, events) = host.try_finish()?; + if enable_diagnostics { + extract_diagnostic_events(&events, diagnostic_events); + } + let encoded_invoke_result = result.map(|res| { + let mut encoded_result_sc_val = vec![]; + metered_write_xdr(&budget, &res, &mut encoded_result_sc_val)?; + Ok(encoded_result_sc_val) + })?; + if encoded_invoke_result.is_ok() { + let init_storage_snapshot = StorageMapSnapshotSource { + budget: &budget, + map: &init_storage_map, + }; + let ledger_changes = get_ledger_changes(&budget, &storage, &init_storage_snapshot)?; + let encoded_contract_events = encode_contract_events(budget, &events)?; + Ok(InvokeHostFunctionResult { + encoded_invoke_result, + ledger_changes, + encoded_contract_events, + }) + } else { + Ok(InvokeHostFunctionResult { + encoded_invoke_result, + ledger_changes: vec![], + encoded_contract_events: vec![], + }) + } +} + +/// Encodes host events as `ContractEvent` XDR. +pub fn encode_contract_events(budget: &Budget, events: &Events) -> Result>, HostError> { + events + .0 + .iter() + .filter(|e| !e.failed_call && e.event.type_ != ContractEventType::Diagnostic) + .map(|e| { + let mut buf = vec![]; + metered_write_xdr(budget, &e.event, &mut buf)?; + Ok(buf) + }) + .collect() +} + +fn extract_diagnostic_events(events: &Events, diagnostic_events: &mut Vec) { + // Important: diagnostic events should be non-metered and not fallible in + // order to not cause unitentional change in transaction result. + for event in &events.0 { + diagnostic_events.push(DiagnosticEvent { + in_successful_contract_call: !event.failed_call, + event: event.event.clone(), + }); + } +} + +fn validate_footprint_key(key: &LedgerKey) -> Result<(), HostError> { + if !matches!( + key, + LedgerKey::Account(_) + | LedgerKey::Trustline(_) + | LedgerKey::ContractData(_) + | LedgerKey::ContractCode(_) + ) { + // We expect santized inputs here, so just indicate that something + // is setup incorrectly with internal ('should never happen') error. + return Err( + Error::from_type_and_code(ScErrorType::Storage, ScErrorCode::InternalError).into(), + ); + } + Ok(()) +} + +fn ledger_entry_to_ledger_key(le: &LedgerEntry, budget: &Budget) -> Result { + match &le.data { + LedgerEntryData::Account(a) => Ok(LedgerKey::Account(LedgerKeyAccount { + account_id: a.account_id.metered_clone(budget)?, + })), + LedgerEntryData::Trustline(tl) => Ok(LedgerKey::Trustline(LedgerKeyTrustLine { + account_id: tl.account_id.metered_clone(budget)?, + asset: tl.asset.metered_clone(budget)?, + })), + LedgerEntryData::ContractData(cd) => Ok(LedgerKey::ContractData(LedgerKeyContractData { + contract: cd.contract.metered_clone(budget)?, + key: cd.key.metered_clone(budget)?, + durability: cd.durability, + body_type: match &cd.body { + ContractDataEntryBody::DataEntry(_data) => ContractEntryBodyType::DataEntry, + ContractDataEntryBody::ExpirationExtension => { + ContractEntryBodyType::ExpirationExtension + } + }, + })), + LedgerEntryData::ContractCode(code) => Ok(LedgerKey::ContractCode(LedgerKeyContractCode { + hash: code.hash.clone(), + body_type: match &code.body { + ContractCodeEntryBody::DataEntry(_) => ContractEntryBodyType::DataEntry, + ContractCodeEntryBody::ExpirationExtension => { + return Err(Error::from_type_and_code( + ScErrorType::Storage, + ScErrorCode::InternalError, + ) + .into()); + } + }, + })), + _ => { + return Err(Error::from_type_and_code( + ScErrorType::Storage, + ScErrorCode::InternalError, + ) + .into()); + } + } +} + +fn build_storage_footprint_from_xdr( + budget: &Budget, + footprint: LedgerFootprint, +) -> Result { + let mut footprint_map = FootprintMap::new(); + + for key in footprint.read_write.to_vec() { + validate_footprint_key(&key)?; + footprint_map = footprint_map.insert(Rc::new(key), AccessType::ReadWrite, budget)?; + } + + for key in footprint.read_only.to_vec() { + validate_footprint_key(&key)?; + footprint_map = footprint_map.insert(Rc::new(key), AccessType::ReadOnly, budget)?; + } + Ok(Footprint(footprint_map)) +} + +fn build_storage_map_from_xdr_ledger_entries, I: ExactSizeIterator>( + budget: &Budget, + footprint: &Footprint, + encoded_ledger_entries: I, +) -> Result { + let mut map = StorageMap::new(); + for buf in encoded_ledger_entries { + let le = Rc::new(metered_from_xdr_with_budget::( + buf.as_ref(), + &budget, + )?); + let key = Rc::new(ledger_entry_to_ledger_key(&le, budget)?); + if !footprint.0.contains_key::(&key, budget)? { + return Err(Error::from_type_and_code( + ScErrorType::Storage, + ScErrorCode::InternalError, + ) + .into()); + } + map = map.insert(key, Some(le), budget)?; + } + + // Add non-existing entries from the footprint to the storage. + for k in footprint.0.keys(budget)? { + if !map.contains_key::(k, budget)? { + map = map.insert(k.clone(), None, budget)?; + } + } + Ok(map) +} + +impl Host { + fn build_auth_entries_from_xdr, I: ExactSizeIterator>( + &self, + encoded_contract_auth_entries: I, + ) -> Result, HostError> { + charge_container_bulk_init_with_elts::< + Vec, + SorobanAuthorizationEntry, + >( + encoded_contract_auth_entries.len() as u64, + self.budget_ref(), + )?; + encoded_contract_auth_entries + .map(|buf| self.metered_from_xdr::(buf.as_ref())) + .collect() + } +} + +struct StorageMapSnapshotSource<'a> { + budget: &'a Budget, + map: &'a StorageMap, +} + +impl<'a> SnapshotSource for StorageMapSnapshotSource<'a> { + fn get(&self, key: &Rc) -> Result, HostError> { + if let Some(Some(value)) = self.map.get::>(key, self.budget)? { + Ok(value.clone()) + } else { + Err(Error::from_type_and_code(ScErrorType::Storage, ScErrorCode::InternalError).into()) + } + } + + fn has(&self, key: &Rc) -> Result { + if let Some(maybe_value) = self.map.get::>(key, self.budget)? { + Ok(maybe_value.is_some()) + } else { + Err(Error::from_type_and_code(ScErrorType::Storage, ScErrorCode::InternalError).into()) + } + } +} diff --git a/soroban-env-host/src/host/declared_size.rs b/soroban-env-host/src/host/declared_size.rs index 4306358d3..47903756a 100644 --- a/soroban-env-host/src/host/declared_size.rs +++ b/soroban-env-host/src/host/declared_size.rs @@ -1,5 +1,7 @@ use std::rc::Rc; +use soroban_env_common::xdr::SorobanAuthorizationEntry; + use crate::{ events::{EventError, HostEvent, InternalContractEvent, InternalEvent}, host::Events, @@ -147,7 +149,7 @@ impl_declared_size_type!(ContractEntryBodyType, 4); impl_declared_size_type!(ExtensionPoint, 0); impl_declared_size_type!(SorobanAuthorizedInvocation, 128); impl_declared_size_type!(ScContractInstance, 64); - +impl_declared_size_type!(SorobanAuthorizationEntry, 240); // composite types // Rc is an exception, nothing is being cloned. We approximate ref counter bump with the cost of @@ -338,6 +340,11 @@ mod test { expect!["8"].assert_eq(size_of::>().to_string().as_str()); expect!["64"].assert_eq(size_of::>().to_string().as_str()); expect!["64"].assert_eq(size_of::().to_string().as_str()); + expect!["240"].assert_eq( + size_of::() + .to_string() + .as_str(), + ); } // This is the actual test. @@ -460,6 +467,7 @@ mod test { assert_mem_size_le_declared_size!(ContractEntryBodyType); assert_mem_size_le_declared_size!(ExtensionPoint); assert_mem_size_le_declared_size!(SorobanAuthorizedInvocation); + assert_mem_size_le_declared_size!(SorobanAuthorizationEntry); // composite types assert_mem_size_le_declared_size!(&[ScVal]); assert_mem_size_le_declared_size!((Val, ScVal)); diff --git a/soroban-env-host/src/host/metered_clone.rs b/soroban-env-host/src/host/metered_clone.rs index a3004fe31..839c2176e 100644 --- a/soroban-env-host/src/host/metered_clone.rs +++ b/soroban-env-host/src/host/metered_clone.rs @@ -1,6 +1,6 @@ use std::{mem, rc::Rc}; -use soroban_env_common::xdr::DepthLimiter; +use soroban_env_common::xdr::{DepthLimiter, SorobanAuthorizationEntry}; use crate::{ budget::Budget, @@ -227,6 +227,7 @@ impl MeteredClone for EventError {} impl MeteredClone for CreateContractArgs {} impl MeteredClone for ContractIdPreimage {} impl MeteredClone for SorobanAuthorizedInvocation {} +impl MeteredClone for SorobanAuthorizationEntry {} // composite types impl MeteredClone for Rc {} impl MeteredClone for &[T] {} diff --git a/soroban-env-host/src/host/metered_xdr.rs b/soroban-env-host/src/host/metered_xdr.rs index 16ac54b7e..264cd2c7a 100644 --- a/soroban-env-host/src/host/metered_xdr.rs +++ b/soroban-env-host/src/host/metered_xdr.rs @@ -1,4 +1,5 @@ use crate::{ + budget::Budget, xdr::ContractCostType, xdr::{ReadXdr, ScBytes, WriteXdr}, BytesObject, Host, HostError, @@ -11,7 +12,7 @@ use soroban_env_common::xdr::{ }; struct MeteredWrite<'a, W: Write> { - host: &'a Host, + budget: &'a Budget, w: &'a mut W, } @@ -20,8 +21,8 @@ where W: Write, { fn write(&mut self, buf: &[u8]) -> std::io::Result { - self.host - .charge_budget(ContractCostType::ValSer, Some(buf.len() as u64)) + self.budget + .charge(ContractCostType::ValSer, Some(buf.len() as u64)) .map_err(Into::::into)?; self.w.write(buf) } @@ -32,30 +33,15 @@ where } impl Host { - pub(crate) fn metered_write_xdr( - &self, - obj: &impl WriteXdr, - w: &mut Vec, - ) -> Result<(), HostError> { - let _span = tracy_span!("write xdr"); - let mw = MeteredWrite { host: self, w }; - let mut w = DepthLimitedWrite::new(mw, DEFAULT_XDR_RW_DEPTH_LIMIT); - // MeteredWrite above turned any budget failure into an IO error; we turn it - // back to a budget failure here, since there's really no "IO error" that can - // occur when writing to a Vec. - obj.write_xdr(&mut w) - .map_err(|_| (ScErrorType::Budget, ScErrorCode::ExceededLimit).into()) - } - - pub(crate) fn metered_hash_xdr(&self, obj: &impl WriteXdr) -> Result<[u8; 32], HostError> { + pub fn metered_hash_xdr(&self, obj: &impl WriteXdr) -> Result<[u8; 32], HostError> { let _span = tracy_span!("hash xdr"); let mut buf = vec![]; - self.metered_write_xdr(obj, &mut buf)?; + metered_write_xdr(self.budget_ref(), obj, &mut buf)?; self.charge_budget(ContractCostType::ComputeSha256Hash, Some(buf.len() as u64))?; Ok(Sha256::digest(&buf).try_into()?) } - pub(crate) fn metered_from_xdr(&self, bytes: &[u8]) -> Result { + pub fn metered_from_xdr(&self, bytes: &[u8]) -> Result { let _span = tracy_span!("read xdr"); self.charge_budget(ContractCostType::ValDeser, Some(bytes.len() as u64))?; self.map_err(T::from_xdr(bytes)) @@ -68,3 +54,30 @@ impl Host { self.visit_obj(bytes, |hv: &ScBytes| self.metered_from_xdr(hv.as_slice())) } } + +pub fn metered_write_xdr( + budget: &Budget, + obj: &impl WriteXdr, + w: &mut Vec, +) -> Result<(), HostError> { + let _span = tracy_span!("write xdr"); + let mw = MeteredWrite { budget, w }; + let mut w = DepthLimitedWrite::new(mw, DEFAULT_XDR_RW_DEPTH_LIMIT); + // MeteredWrite above turned any budget failure into an IO error; we turn it + // back to a budget failure here, since there's really no "IO error" that can + // occur when writing to a Vec. + obj.write_xdr(&mut w) + .map_err(|_| (ScErrorType::Budget, ScErrorCode::ExceededLimit).into()) +} + +// Host-less metered XDR decoding. +// Prefer using `metered_from_xdr` when host is available for better error +// reporting. +pub fn metered_from_xdr_with_budget( + bytes: &[u8], + budget: &Budget, +) -> Result { + let _span = tracy_span!("read xdr with budget"); + budget.charge(ContractCostType::ValDeser, Some(bytes.len() as u64))?; + T::from_xdr(bytes).map_err(|e| e.into()) +} diff --git a/soroban-env-host/src/lib.rs b/soroban-env-host/src/lib.rs index 392d84a7c..83c97588b 100644 --- a/soroban-env-host/src/lib.rs +++ b/soroban-env-host/src/lib.rs @@ -22,7 +22,7 @@ //! - The [storage] module which is responsible for providing an interface //! between contracts and their durable storage. //! - +#![recursion_limit = "256"] #[cfg(all(not(target_family = "wasm"), feature = "tracy"))] macro_rules! tracy_span { () => { @@ -71,4 +71,5 @@ pub use host::{ }; pub use soroban_env_common::*; +pub mod e2e_invoke; pub mod fees; diff --git a/soroban-env-host/src/test/budget_metering.rs b/soroban-env-host/src/test/budget_metering.rs index 208da5dde..e6c5a6bf5 100644 --- a/soroban-env-host/src/test/budget_metering.rs +++ b/soroban-env-host/src/test/budget_metering.rs @@ -1,6 +1,6 @@ use crate::{ budget::AsBudget, - host::metered_clone::MeteredClone, + host::{metered_clone::MeteredClone, metered_xdr::metered_write_xdr}, xdr::{ContractCostType, ScMap, ScMapEntry, ScVal}, Env, Host, HostError, Symbol, Val, }; @@ -93,7 +93,7 @@ fn metered_xdr() -> Result<(), HostError> { .try_into(), )?; let mut w = Vec::::new(); - host.metered_write_xdr(&scmap, &mut w)?; + metered_write_xdr(host.budget_ref(), &scmap, &mut w)?; host.with_budget(|budget| { assert_eq!( budget.get_tracker(ContractCostType::ValSer)?.1, @@ -133,7 +133,7 @@ fn metered_xdr_out_of_budget() -> Result<(), HostError> { .try_into(), )?; let mut w = Vec::::new(); - let res = host.metered_write_xdr(&scmap, &mut w); + let res = metered_write_xdr(host.budget_ref(), &scmap, &mut w); let code = (ScErrorType::Budget, ScErrorCode::ExceededLimit); assert!(HostError::result_matches_err(res, code)); Ok(()) diff --git a/soroban-env-host/src/test/complex.rs b/soroban-env-host/src/test/complex.rs index a7ab0ae8f..ce1f1113d 100644 --- a/soroban-env-host/src/test/complex.rs +++ b/soroban-env-host/src/test/complex.rs @@ -36,7 +36,7 @@ fn run_complex() -> Result<(), HostError> { Symbol::try_from_small_str("go")?, host.add_host_object(HostVec::new())?, )?; - let (store, _, _) = host.try_finish().unwrap(); + let (store, _) = host.try_finish().unwrap(); store.footprint };