From 6d12921660fa557f82b1bc6cc9ea2474c906c844 Mon Sep 17 00:00:00 2001 From: Graydon Hoare Date: Tue, 26 Sep 2023 11:40:56 -0700 Subject: [PATCH] Host code review, minor cleanups and code reorganization (#1086) This round does not involve any significant functional changes, though some might be observable. It's mostly cleanup, code reorg (including a new file `lifecycle.rs`), a bit of refactoring and some making stuff private. --- soroban-env-host/src/auth.rs | 105 ++++ soroban-env-host/src/cost_runner/runner.rs | 4 +- soroban-env-host/src/host.rs | 596 ++------------------- soroban-env-host/src/host/crypto.rs | 4 +- soroban-env-host/src/host/data_helper.rs | 171 +++++- soroban-env-host/src/host/error.rs | 10 + soroban-env-host/src/host/lifecycle.rs | 255 +++++++++ soroban-env-host/src/host/validity.rs | 50 +- 8 files changed, 607 insertions(+), 588 deletions(-) create mode 100644 soroban-env-host/src/host/lifecycle.rs diff --git a/soroban-env-host/src/auth.rs b/soroban-env-host/src/auth.rs index ebdc083a6..1bf28325c 100644 --- a/soroban-env-host/src/auth.rs +++ b/soroban-env-host/src/auth.rs @@ -1827,6 +1827,111 @@ impl Host { ) }) } + + // Returns the recorded per-address authorization payloads that would cover the + // top-level contract function invocation in the enforcing mode. + // This should only be called in the recording authorization mode, i.e. only + // if `switch_to_recording_auth` has been called. + pub fn get_recorded_auth_payloads(&self) -> Result, HostError> { + #[cfg(not(any(test, feature = "testutils")))] + { + self.try_borrow_authorization_manager()? + .get_recorded_auth_payloads(self) + } + #[cfg(any(test, feature = "testutils"))] + { + self.try_borrow_previous_authorization_manager()? + .as_ref() + .ok_or_else(|| { + self.err( + ScErrorType::Auth, + ScErrorCode::InvalidAction, + "previous invocation is missing - no auth data to get", + &[], + ) + })? + .get_recorded_auth_payloads(self) + } + } +} + +#[cfg(any(test, feature = "testutils"))] +use crate::{host::frame::ContractReentryMode, xdr::SorobanAuthorizedInvocation, BytesObject}; +#[cfg(any(test, feature = "testutils"))] +impl Host { + /// Invokes the reserved `__check_auth` function on a provided contract. + /// + /// This is useful for testing the custom account contracts. Otherwise, the + /// host prohibits calling `__check_auth` outside of internal implementation + /// of `require_auth[_for_args]` calls. + pub fn call_account_contract_check_auth( + &self, + contract: BytesObject, + args: VecObject, + ) -> Result { + use crate::native_contract::account_contract::ACCOUNT_CONTRACT_CHECK_AUTH_FN_NAME; + + let contract_id = self.hash_from_bytesobj_input("contract", contract)?; + let args_vec = self.call_args_from_obj(args)?; + let res = self.call_n_internal( + &contract_id, + ACCOUNT_CONTRACT_CHECK_AUTH_FN_NAME.try_into_val(self)?, + args_vec.as_slice(), + ContractReentryMode::Prohibited, + true, + ); + if let Err(e) = &res { + self.error( + e.error, + "check auth invocation for a custom account contract failed", + &[contract.to_val(), args.to_val()], + ); + } + res + } + + /// Returns the current state of the authorization manager. + /// + /// Use this in conjunction with `set_auth_manager` to do authorized + /// operations without breaking the current authorization state (useful for + /// preserving the auth state while doing the generic test setup). + pub fn snapshot_auth_manager(&self) -> Result { + Ok(self.try_borrow_authorization_manager()?.clone()) + } + + /// Replaces authorization manager with the provided new instance. + /// + /// Use this in conjunction with `snapshot_auth_manager` to do authorized + /// operations without breaking the current authorization state (useful for + /// preserving the auth state while doing the generic test setup). + pub fn set_auth_manager(&self, auth_manager: AuthorizationManager) -> Result<(), HostError> { + *self.try_borrow_authorization_manager_mut()? = auth_manager; + Ok(()) + } + + // Returns the authorizations that have been authenticated for the last + // contract invocation. + // + // Authenticated means that either the authorization was authenticated using + // the actual authorization logic for that authorization in enforced mode, + // or that it was recorded in recording mode and authorization was assumed + // successful. + pub fn get_authenticated_authorizations( + &self, + ) -> Result, HostError> { + Ok(self + .try_borrow_previous_authorization_manager_mut()? + .as_mut() + .ok_or_else(|| { + self.err( + ScErrorType::Auth, + ScErrorCode::InvalidAction, + "previous invocation is missing - no auth data to get", + &[], + ) + })? + .get_authenticated_authorizations(self)) + } } // metering: free for testutils diff --git a/soroban-env-host/src/cost_runner/runner.rs b/soroban-env-host/src/cost_runner/runner.rs index 910d0b597..e326b2760 100644 --- a/soroban-env-host/src/cost_runner/runner.rs +++ b/soroban-env-host/src/cost_runner/runner.rs @@ -1,6 +1,6 @@ use std::hint::black_box; -use crate::{xdr::ContractCostType, Host}; +use crate::{budget::AsBudget, xdr::ContractCostType, Host}; /// `CostRunner` is an interface to running a host cost entity of a `CostType` (usually a block of /// WASM bytecode or a host function), given a sample of `SampleType`. pub trait CostRunner: Sized { @@ -66,6 +66,6 @@ pub trait CostRunner: Sized { /// actual input from the host's perspective. So use it carefully. This should be /// after the `run`, outside of the CPU-and-memory tracking machineary. fn get_tracker(host: &Host) -> (u64, Option) { - host.0.budget.get_tracker(Self::COST_TYPE).unwrap() + host.as_budget().get_tracker(Self::COST_TYPE).unwrap() } } diff --git a/soroban-env-host/src/host.rs b/soroban-env-host/src/host.rs index f664caaf7..bd88e82af 100644 --- a/soroban-env-host/src/host.rs +++ b/soroban-env-host/src/host.rs @@ -5,18 +5,16 @@ use core::{cell::RefCell, cmp::Ordering, fmt::Debug}; use std::rc::Rc; use crate::{ - auth::{AuthorizationManager, RecordedAuthPayload}, + auth::AuthorizationManager, budget::{AsBudget, Budget}, - err, events::{diagnostic::DiagnosticLevel, Events, InternalEventsBuffer}, host_object::{HostMap, HostObject, HostObjectType, HostVec}, impl_bignum_host_fns_rhs_u32, impl_wrapping_obj_from_num, impl_wrapping_obj_to_num, num::*, - storage::{InstanceStorageMap, Storage}, + storage::Storage, xdr::{ - int128_helpers, AccountId, Asset, ContractCodeEntry, ContractCostType, ContractDataEntry, - ContractEventType, ContractExecutable, CreateContractArgs, Duration, ExtensionPoint, Hash, - LedgerEntryData, LedgerKey, LedgerKeyContractCode, PublicKey, ScAddress, ScBytes, + int128_helpers, AccountId, Asset, ContractCostType, ContractEventType, ContractExecutable, + CreateContractArgs, Duration, Hash, LedgerEntryData, PublicKey, ScAddress, ScBytes, ScErrorType, ScString, ScSymbol, ScVal, TimePoint, }, AddressObject, Bool, BytesObject, ConversionError, Error, I128Object, I256Object, MapObject, @@ -27,14 +25,15 @@ use crate::{ use crate::Vm; use crate::{EnvBase, Object, Symbol, Val}; -pub(crate) mod comparison; +mod comparison; mod conversion; pub(crate) mod crypto; mod data_helper; -pub(crate) mod declared_size; +mod declared_size; pub(crate) mod error; pub(crate) mod frame; pub(crate) mod ledger_info_helper; +mod lifecycle; mod mem_helper; pub(crate) mod metered_clone; pub(crate) mod metered_map; @@ -45,14 +44,10 @@ mod prng; pub use prng::{Seed, SEED_BYTES}; mod validity; pub use error::HostError; -use soroban_env_common::xdr::{ - ContractDataDurability, ContractIdPreimage, ContractIdPreimageFromAddress, ScContractInstance, - ScErrorCode, -}; +use soroban_env_common::xdr::{ContractIdPreimage, ContractIdPreimageFromAddress, ScErrorCode}; use self::{ frame::{Context, ContractReentryMode}, - metered_clone::MeteredAlloc, prng::Prng, }; use self::{ @@ -60,16 +55,10 @@ use self::{ metered_xdr::metered_write_xdr, }; use crate::impl_bignum_host_fns; -#[cfg(any(test, feature = "testutils"))] -use crate::storage::{AccessType, Footprint}; use crate::Compare; #[cfg(any(test, feature = "testutils"))] -use crate::TryIntoVal; -#[cfg(any(test, feature = "testutils"))] pub use frame::ContractFunctionSet; pub(crate) use frame::Frame; -#[cfg(any(test, feature = "testutils"))] -use soroban_env_common::xdr::SorobanAuthorizedInvocation; /// Defines the maximum depth for recursive calls in the host, i.e. `Val` conversion, comparison, /// and deep clone, to prevent stack overflow. @@ -106,22 +95,22 @@ pub struct LedgerInfo { } #[derive(Clone, Default)] -pub(crate) struct HostImpl { +struct HostImpl { source_account: RefCell>, ledger: RefCell>, - pub(crate) objects: RefCell>, + objects: RefCell>, storage: RefCell, - pub(crate) context: RefCell>, + context: RefCell>, // Note: budget is refcounted and is _not_ deep-cloned when you call HostImpl::deep_clone, // mainly because it's not really possible to achieve (the same budget is connected to many // metered sub-objects) but also because it's plausible that the person calling deep_clone // actually wants their clones to be metered by "the same" total budget // FIXME: deep_clone is gone, maybe Budget should not be separately refcounted? - pub(crate) budget: Budget, - pub(crate) events: RefCell, + budget: Budget, + events: RefCell, authorization_manager: RefCell, - pub(crate) diagnostic_level: RefCell, - pub(crate) base_prng: RefCell>, + diagnostic_level: RefCell, + base_prng: RefCell>, // Note: we're not going to charge metering for testutils because it's out of the scope // of what users will be charged for in production -- it's scaffolding for testing a contract, // but shouldn't be charged to the contract itself (and will never be compiled-in to @@ -139,7 +128,7 @@ pub(crate) struct HostImpl { } // Host is a newtype on Rc so we can impl Env for it below. #[derive(Clone)] -pub struct Host(pub(crate) Rc); +pub struct Host(Rc); #[allow(clippy::derivable_impls)] impl Default for Host { @@ -251,8 +240,6 @@ impl Host { pub fn with_storage_and_budget(storage: Storage, budget: Budget) -> Self { #[cfg(all(not(target_family = "wasm"), feature = "tracy"))] let _client = tracy_client::Client::start(); - // Here we are missing charge for the Rc::new but that is once for the - // lifetime of the host, so that should be okay. Self(Rc::new(HostImpl { source_account: RefCell::new(None), ledger: RefCell::new(None), @@ -383,59 +370,6 @@ impl Host { self.0.budget.clone().charge(ty, input) } - pub fn with_mut_storage(&self, f: F) -> Result - where - F: FnOnce(&mut Storage) -> Result, - { - f(&mut *self.try_borrow_storage_mut()?) - } - - /// Immutable accessor to the instance storage of the currently running - /// contract. - /// Performs lazy initialization of instance storage on access. - pub(crate) fn with_instance_storage(&self, f: F) -> Result - where - F: FnOnce(&InstanceStorageMap) -> Result, - { - self.with_current_context_mut(|ctx| { - self.maybe_init_instance_storage(ctx)?; - f(ctx.storage.as_ref().ok_or_else(|| { - self.err( - ScErrorType::Context, - ScErrorCode::InternalError, - "missing instance storage", - &[], - ) - })?) - }) - } - - /// Mutable accessor to the instance storage of the currently running - /// contract. - /// Performs lazy initialization of instance storage on access. - pub(crate) fn with_mut_instance_storage(&self, f: F) -> Result - where - F: FnOnce(&mut InstanceStorageMap) -> Result, - { - self.with_current_context_mut(|ctx| { - self.maybe_init_instance_storage(ctx)?; - let storage = ctx.storage.as_mut().ok_or_else(|| { - self.err( - ScErrorType::Context, - ScErrorCode::InternalError, - "missing instance storage", - &[], - ) - })?; - // Consider any mutable access to be modifying the instance storage. - // This way we would provide consistent footprint (RO for read-only - // ops using `with_instance_storage` and RW for potentially - // mutating ops using `with_mut_instance_storage`). - storage.is_modified = true; - f(storage) - }) - } - /// Accept a _unique_ (refcount = 1) host reference and destroy the /// underlying [`HostImpl`], returning its finalized components containing /// processing side effects to the caller as a tuple wrapped in `Ok(...)`. @@ -451,396 +385,13 @@ impl Host { }) } - /// Invokes the reserved `__check_auth` function on a provided contract. - /// - /// This is useful for testing the custom account contracts. Otherwise, the - /// host prohibits calling `__check_auth` outside of internal implementation - /// of `require_auth[_for_args]` calls. - #[cfg(any(test, feature = "testutils"))] - pub fn call_account_contract_check_auth( - &self, - contract: BytesObject, - args: VecObject, - ) -> Result { - use crate::native_contract::account_contract::ACCOUNT_CONTRACT_CHECK_AUTH_FN_NAME; - - let contract_id = self.hash_from_bytesobj_input("contract", contract)?; - let args_vec = self.call_args_from_obj(args)?; - let res = self.call_n_internal( - &contract_id, - ACCOUNT_CONTRACT_CHECK_AUTH_FN_NAME.try_into_val(self)?, - args_vec.as_slice(), - ContractReentryMode::Prohibited, - true, - ); - if let Err(e) = &res { - self.error( - e.error, - "check auth invocation for a custom account contract failed", - &[contract.to_val(), args.to_val()], - ); - } - res - } - - #[cfg(any(test, feature = "testutils"))] - /// Returns the current state of the authorization manager. - /// - /// Use this in conjunction with `set_auth_manager` to do authorized - /// operations without breaking the current authorization state (useful for - /// preserving the auth state while doing the generic test setup). - pub fn snapshot_auth_manager(&self) -> Result { - Ok(self.try_borrow_authorization_manager()?.clone()) - } - - /// Replaces authorization manager with the provided new instance. - /// - /// Use this in conjunction with `snapshot_auth_manager` to do authorized - /// operations without breaking the current authorization state (useful for - /// preserving the auth state while doing the generic test setup). - #[cfg(any(test, feature = "testutils"))] - pub fn set_auth_manager(&self, auth_manager: AuthorizationManager) -> Result<(), HostError> { - *self.try_borrow_authorization_manager_mut()? = auth_manager; - Ok(()) - } - // Testing interface to create values directly for later use via Env functions. // It needs to be a `pub` method because benches are considered a separate crate. + #[cfg(any(test, feature = "testutils"))] pub fn inject_val(&self, v: &ScVal) -> Result { self.to_host_val(v).map(Into::into) } - // Notes on metering: this is covered by the called components. - fn create_contract_with_id( - &self, - contract_id: Hash, - contract_executable: ContractExecutable, - ) -> Result<(), HostError> { - let storage_key = self.contract_instance_ledger_key(&contract_id)?; - if self - .try_borrow_storage_mut()? - .has(&storage_key, self.as_budget()) - .map_err(|e| self.decorate_contract_instance_storage_error(e, &contract_id))? - { - return Err(self.err( - ScErrorType::Storage, - ScErrorCode::ExistingValue, - "contract already exists", - &[self - .add_host_object(self.scbytes_from_hash(&contract_id)?)? - .into()], - )); - } - // Make sure the contract code exists. Without this check it would be - // possible to accidentally create a contract that never may be invoked - // (just by providing a bad hash). - if let ContractExecutable::Wasm(wasm_hash) = &contract_executable { - if !self.wasm_exists(wasm_hash)? { - return Err(err!( - self, - (ScErrorType::Storage, ScErrorCode::MissingValue), - "Wasm does not exist", - *wasm_hash - )); - } - } - let instance = ScContractInstance { - executable: contract_executable, - storage: Default::default(), - }; - self.store_contract_instance(instance, contract_id, &storage_key)?; - Ok(()) - } - - fn maybe_initialize_asset_token( - &self, - contract_id: &Hash, - id_preimage: &ContractIdPreimage, - ) -> Result<(), HostError> { - if let ContractIdPreimage::Asset(asset) = id_preimage { - let mut asset_bytes: Vec = Default::default(); - metered_write_xdr(self.budget_ref(), asset, &mut asset_bytes)?; - self.call_n_internal( - contract_id, - Symbol::try_from_val(self, &"init_asset")?, - &[self - .add_host_object(self.scbytes_from_vec(asset_bytes)?)? - .into()], - ContractReentryMode::Prohibited, - false, - )?; - Ok(()) - } else { - Ok(()) - } - } - - fn create_contract_internal( - &self, - deployer: Option, - args: CreateContractArgs, - ) -> Result { - let has_deployer = deployer.is_some(); - if has_deployer { - self.try_borrow_authorization_manager()? - .push_create_contract_host_fn_frame(self, args.metered_clone(self)?)?; - } - // Make sure that even in case of operation failure we still pop the - // stack frame. - // This is hacky, but currently this is the only instance where we need - // to manually manage auth manager frames (we don't need to authorize - // any other host fns and it doesn't seem useful to create extra frames - // for them just to make auth work in a single case). - let res = self.create_contract_with_optional_auth(deployer, args); - if has_deployer { - self.try_borrow_authorization_manager()?.pop_frame(self)?; - } - res - } - - fn create_contract_with_optional_auth( - &self, - deployer: Option, - args: CreateContractArgs, - ) -> Result { - if let Some(deployer_address) = deployer { - self.try_borrow_authorization_manager()?.require_auth( - self, - deployer_address, - Default::default(), - )?; - } - - let id_preimage = - self.get_full_contract_id_preimage(args.contract_id_preimage.metered_clone(self)?)?; - let hash_id = Hash(self.metered_hash_xdr(&id_preimage)?); - self.create_contract_with_id(hash_id.metered_clone(self)?, args.executable)?; - self.maybe_initialize_asset_token(&hash_id, &args.contract_id_preimage)?; - self.add_host_object(ScAddress::Contract(hash_id)) - } - - pub(crate) fn get_contract_id_hash( - &self, - deployer: AddressObject, - salt: BytesObject, - ) -> Result { - let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress { - address: self.visit_obj(deployer, |addr: &ScAddress| addr.metered_clone(self))?, - salt: self.u256_from_bytesobj_input("contract_id_salt", salt)?, - }); - - let id_preimage = - self.get_full_contract_id_preimage(contract_id_preimage.metered_clone(self)?)?; - Ok(Hash(self.metered_hash_xdr(&id_preimage)?)) - } - - pub(crate) fn get_asset_contract_id_hash(&self, asset: Asset) -> Result { - let id_preimage = self.get_full_contract_id_preimage(ContractIdPreimage::Asset(asset))?; - let id_arr: [u8; 32] = self.metered_hash_xdr(&id_preimage)?; - Ok(Hash(id_arr)) - } - - // "testutils" is not covered by budget metering. - #[cfg(any(test, feature = "testutils"))] - pub fn register_test_contract( - &self, - contract_address: AddressObject, - contract_fns: Rc, - ) -> Result<(), HostError> { - let contract_id = self.contract_id_from_address(contract_address)?; - let instance_key = self.contract_instance_ledger_key(&contract_id)?; - // Test contract might be overriding an already registered Wasm - // contract, in which case we should preserve the instance entry. - if self - .retrieve_contract_instance_from_storage(&instance_key) - .is_err() - { - // Use empty Wasm as default executable. It shouldn't be observable, - // but allows exercising bump logic and make operations like Wasm - // update more meaningful. - let wasm_hash_obj = self.upload_contract_wasm(vec![])?; - let wasm_hash = self.hash_from_bytesobj_input("wasm_hash", wasm_hash_obj)?; - let instance = ScContractInstance { - executable: ContractExecutable::Wasm(wasm_hash), - storage: None, - }; - self.store_contract_instance(instance, contract_id.clone(), &instance_key)?; - }; - let mut contracts = self.try_borrow_contracts_mut()?; - contracts.insert(contract_id, contract_fns); - Ok(()) - } - - // Writes an arbitrary ledger entry to storage. - // "testutils" are not covered by budget metering. - #[cfg(any(test, feature = "testutils"))] - pub fn add_ledger_entry( - &self, - key: &Rc, - val: &Rc, - expiration_ledger: Option, - ) -> Result<(), HostError> { - self.as_budget().with_free_budget(|| { - self.with_mut_storage(|storage| { - storage.put(key, val, expiration_ledger, self.as_budget()) - }) - }) - } - - // Performs the necessary setup to access the provided ledger key/entry in - // enforcing storage mode. - // "testutils" are not covered by budget metering. - #[cfg(any(test, feature = "testutils"))] - pub fn setup_storage_entry( - &self, - key: Rc, - val: Option<(Rc, Option)>, - access_type: AccessType, - ) -> Result<(), HostError> { - self.as_budget().with_free_budget(|| { - self.with_mut_storage(|storage| { - storage - .footprint - .record_access(&key, access_type, self.as_budget())?; - storage.map = storage.map.insert(key, val, self.as_budget())?; - Ok(()) - }) - }) - } - - // Performs the necessary setup to access all the entries in provided - // footprint in enforcing mode. - // "testutils" are not covered by budget metering. - #[cfg(any(test, feature = "testutils"))] - pub fn setup_storage_footprint(&self, footprint: Footprint) -> Result<(), HostError> { - for (key, access_type) in footprint.0.map { - self.setup_storage_entry(key, None, access_type)?; - } - Ok(()) - } - - // Returns the authorizations that have been authenticated for the last - // contract invocation. - // - // Authenticated means that either the authorization was authenticated using - // the actual authorization logic for that authorization in enforced mode, - // or that it was recorded in recording mode and authorization was assumed - // successful. - #[cfg(any(test, feature = "testutils"))] - pub fn get_authenticated_authorizations( - &self, - ) -> Result, HostError> { - Ok(self - .try_borrow_previous_authorization_manager_mut()? - .as_mut() - .ok_or_else(|| { - self.err( - ScErrorType::Auth, - ScErrorCode::InvalidAction, - "previous invocation is missing - no auth data to get", - &[], - ) - })? - .get_authenticated_authorizations(self)) - } - - fn upload_contract_wasm(&self, wasm: Vec) -> Result { - let hash_bytes: [u8; 32] = crypto::sha256_hash_from_bytes(wasm.as_slice(), self)? - .try_into() - .map_err(|_| { - self.err( - ScErrorType::Value, - ScErrorCode::InternalError, - "unexpected hash length", - &[], - ) - })?; - - // Check size before instantiation. - let wasm_bytes_m: crate::xdr::BytesM = wasm.try_into().map_err(|_| { - self.err( - ScErrorType::Value, - ScErrorCode::ExceededLimit, - "Wasm code is too large", - &[], - ) - })?; - - // Instantiate a temporary / throwaway VM using this wasm. This will do - // both quick checks like "does this wasm have the right protocol number - // to run on this network" and also a full parse-and-link pass to check - // that the wasm is basically not garbage. It might still fail to run - // but it will at least instantiate. This might seem a bit heavyweight - // but really "instantiating a VM" is mostly just "parsing the module - // and doing those checks" anyway. Revisit in the future if you want to - // try to split these costs up some. - if cfg!(any(test, feature = "testutils")) && wasm_bytes_m.as_slice().is_empty() { - // Allow a zero-byte contract when testing, as this is used to make - // native test contracts behave like wasm. They will never be - // instantiated, this is just to exercise their storage logic. - } else { - let _check_vm = Vm::new( - self, - Hash(hash_bytes.metered_clone(self)?), - wasm_bytes_m.as_slice(), - )?; - } - - let hash_obj = self.add_host_object(self.scbytes_from_slice(hash_bytes.as_slice())?)?; - let code_key = Rc::metered_new( - LedgerKey::ContractCode(LedgerKeyContractCode { - hash: Hash(hash_bytes.metered_clone(self)?), - }), - self, - )?; - if !self - .try_borrow_storage_mut()? - .has(&code_key, self.as_budget()) - .map_err(|e| self.decorate_contract_code_storage_error(e, &Hash(hash_bytes)))? - { - self.with_mut_storage(|storage| { - let data = ContractCodeEntry { - hash: Hash(hash_bytes), - ext: ExtensionPoint::V0, - code: wasm_bytes_m, - }; - storage.put( - &code_key, - &Host::new_contract_code(self, data)?, - Some(self.get_min_expiration_ledger(ContractDataDurability::Persistent)?), - self.as_budget(), - ) - })?; - } - Ok(hash_obj) - } - - // Returns the recorded per-address authorization payloads that would cover the - // top-level contract function invocation in the enforcing mode. - // This should only be called in the recording authorization mode, i.e. only - // if `switch_to_recording_auth` has been called. - pub fn get_recorded_auth_payloads(&self) -> Result, HostError> { - #[cfg(not(any(test, feature = "testutils")))] - { - self.try_borrow_authorization_manager()? - .get_recorded_auth_payloads(self) - } - #[cfg(any(test, feature = "testutils"))] - { - self.try_borrow_previous_authorization_manager()? - .as_ref() - .ok_or_else(|| { - self.err( - ScErrorType::Auth, - ScErrorCode::InvalidAction, - "previous invocation is missing - no auth data to get", - &[], - ) - })? - .get_recorded_auth_payloads(self) - } - } - fn symbol_matches(&self, s: &[u8], sym: Symbol) -> Result { if let Ok(ss) = SymbolSmall::try_from(sym) { let sstr: SymbolStr = ss.into(); @@ -870,70 +421,6 @@ impl Host { )) } } - - fn put_contract_data_into_ledger( - &self, - k: Val, - v: Val, - t: StorageType, - ) -> Result<(), HostError> { - let durability: ContractDataDurability = t.try_into()?; - let key = self.contract_data_key_from_rawval(k, durability)?; - // Currently the storage stores the whole ledger entries, while this - // operation might only modify only the internal `ScVal` value. Thus we - // need to only overwrite the value in case if there is already an - // existing ledger entry value for the key in the storage. - if self - .try_borrow_storage_mut()? - .has(&key, self.as_budget()) - .map_err(|e| self.decorate_contract_data_storage_error(e, k))? - { - let (current, expiration_ledger) = self - .try_borrow_storage_mut()? - .get_with_expiration(&key, self.as_budget()) - .map_err(|e| self.decorate_contract_data_storage_error(e, k))?; - let mut current = (*current).metered_clone(self)?; - match current.data { - LedgerEntryData::ContractData(ref mut entry) => { - entry.val = self.from_host_val(v)?; - } - _ => { - return Err(self.err( - ScErrorType::Storage, - ScErrorCode::InternalError, - "expected DataEntry", - &[], - )); - } - } - self.try_borrow_storage_mut()? - .put( - &key, - &Rc::metered_new(current, self)?, - expiration_ledger, - self.as_budget(), - ) - .map_err(|e| self.decorate_contract_data_storage_error(e, k))?; - } else { - let data = ContractDataEntry { - contract: ScAddress::Contract(self.get_current_contract_id_internal()?), - key: self.from_host_val(k)?, - val: self.from_host_val(v)?, - durability, - ext: ExtensionPoint::V0, - }; - self.try_borrow_storage_mut()? - .put( - &key, - &Host::new_contract_data(self, data)?, - Some(self.get_min_expiration_ledger(durability)?), - self.as_budget(), - ) - .map_err(|e| self.decorate_contract_data_storage_error(e, k))?; - } - - Ok(()) - } } // Notes on metering: these are called from the guest and thus charged on the VM instructions. @@ -2391,7 +1878,7 @@ impl VmCallerEnv for Host { // Notes on metering: covered by the components. fn try_call( &self, - vmcaller: &mut VmCaller, + _vmcaller: &mut VmCaller, contract_address: AddressObject, func: Symbol, args: VecObject, @@ -2574,7 +2061,7 @@ impl VmCallerEnv for Host { match found { None => Err(self.err( ScErrorType::Value, - ScErrorCode::InvalidInput, + ScErrorCode::MissingValue, "symbol not found in linear memory slices", &[sym.to_val()], )), @@ -2625,7 +2112,7 @@ impl VmCallerEnv for Host { let i: u32 = iv.into(); self.visit_obj(b, |hv: &ScBytes| { hv.get(i as usize) - .map(|u| Into::::into(Into::::into(*u))) + .map(|u| U32Val::from(u32::from(*u))) .ok_or_else(|| { self.err( ScErrorType::Object, @@ -2699,7 +2186,7 @@ impl VmCallerEnv for Host { let vnew = self.visit_obj(b, |hv: &ScBytes| { // we allocate the new vector to be able to hold `len + 1` bytes, so that the push // will not trigger a reallocation, causing data to be cloned twice. - let len = hv.len().saturating_add(1); + let len = self.validate_usize_sum_fits_in_u32(hv.len(), 1)?; Vec::::charge_bulk_init_cpy(len as u64, self)?; let mut vnew: Vec = Vec::with_capacity(len); vnew.extend_from_slice(hv.as_slice()); @@ -2740,7 +2227,7 @@ impl VmCallerEnv for Host { ) -> Result { self.visit_obj(b, |hv: &ScBytes| { hv.first() - .map(|u| Into::::into(Into::::into(*u))) + .map(|u| U32Val::from(u32::from(*u))) .ok_or_else(|| { self.err( ScErrorType::Object, @@ -2760,7 +2247,7 @@ impl VmCallerEnv for Host { ) -> Result { self.visit_obj(b, |hv: &ScBytes| { hv.last() - .map(|u| Into::::into(Into::::into(*u))) + .map(|u| U32Val::from(u32::from(*u))) .ok_or_else(|| { self.err( ScErrorType::Object, @@ -2783,9 +2270,9 @@ impl VmCallerEnv for Host { let u = self.u8_from_u32val_input("u", u)?; let vnew = self.visit_obj(b, |hv: &ScBytes| { self.validate_index_le_bound(i, hv.len())?; - // we allocate the new vector to be able to hold `len + 1` bytes, so that the push + // we allocate the new vector to be able to hold `len + 1` bytes, so that the insert // will not trigger a reallocation, causing data to be cloned twice. - let len = hv.len().saturating_add(1); + let len = self.validate_usize_sum_fits_in_u32(hv.len(), 1)?; Vec::::charge_bulk_init_cpy(len as u64, self)?; let mut vnew: Vec = Vec::with_capacity(len); vnew.extend_from_slice(hv.as_slice()); @@ -2803,14 +2290,9 @@ impl VmCallerEnv for Host { ) -> Result { let vnew = self.visit_obj(b1, |sb1: &ScBytes| { self.visit_obj(b2, |sb2: &ScBytes| { - if sb2.len() > u32::MAX as usize - sb1.len() { - return Err(self.err_arith_overflow()); - } // we allocate large enough memory to hold the new combined vector, so that // allocation only happens once, and charge for it upfront. - // we already checked above that `len` will not overflow, here using - // saturating_add just in case. - let len = sb1.len().saturating_add(sb2.len()); + let len = self.validate_usize_sum_fits_in_u32(sb1.len(), sb2.len())?; Vec::::charge_bulk_init_cpy(len as u64, self)?; let mut vnew: Vec = Vec::with_capacity(len); vnew.extend_from_slice(sb1.as_slice()); @@ -2832,7 +2314,11 @@ impl VmCallerEnv for Host { let end: u32 = end.into(); let vnew = self.visit_obj(b, |hv: &ScBytes| { let range = self.valid_range_from_start_end_bound(start, end, hv.len())?; - self.metered_slice_to_vec(&hv.as_slice()[range]) + self.metered_slice_to_vec( + &hv.as_slice() + .get(range) + .ok_or_else(|| self.err_oob_object_index(None))?, + ) })?; self.add_host_object(self.scbytes_from_vec(vnew)?) } @@ -2868,10 +2354,10 @@ impl VmCallerEnv for Host { x: BytesObject, s: BytesObject, ) -> Result { - let public_key = self.ed25519_pub_key_from_bytesobj_input(k)?; + let verifying_key = self.ed25519_pub_key_from_bytesobj_input(k)?; let sig = self.ed25519_signature_from_bytesobj_input("sig", s)?; let res = self.visit_obj(x, |payload: &ScBytes| { - self.verify_sig_ed25519_internal(payload.as_slice(), &public_key, &sig) + self.verify_sig_ed25519_internal(payload.as_slice(), &verifying_key, &sig) }); Ok(res?.into()) } @@ -2892,7 +2378,7 @@ impl VmCallerEnv for Host { // endregion "crypto" module functions // region: "test" module functions - fn dummy0(&self, vmcaller: &mut VmCaller) -> Result { + fn dummy0(&self, _vmcaller: &mut VmCaller) -> Result { Ok(().into()) } @@ -2901,7 +2387,7 @@ impl VmCallerEnv for Host { fn require_auth_for_args( &self, - vmcaller: &mut VmCaller, + _vmcaller: &mut VmCaller, address: AddressObject, args: VecObject, ) -> Result { @@ -2914,7 +2400,7 @@ impl VmCallerEnv for Host { fn require_auth( &self, - vmcaller: &mut VmCaller, + _vmcaller: &mut VmCaller, address: AddressObject, ) -> Result { let args = self.with_current_frame(|f| { @@ -2943,7 +2429,7 @@ impl VmCallerEnv for Host { fn authorize_as_curr_contract( &self, - vmcaller: &mut VmCaller, + _vmcaller: &mut VmCaller, auth_entries: VecObject, ) -> Result { Ok(self @@ -3003,7 +2489,7 @@ impl VmCallerEnv for Host { fn prng_reseed( &self, - vmcaller: &mut VmCaller, + _vmcaller: &mut VmCaller, seed: BytesObject, ) -> Result { self.visit_obj(seed, |bytes: &ScBytes| { @@ -3035,7 +2521,7 @@ impl VmCallerEnv for Host { fn prng_bytes_new( &self, - vmcaller: &mut VmCaller, + _vmcaller: &mut VmCaller, length: U32Val, ) -> Result { self.add_host_object( @@ -3045,7 +2531,7 @@ impl VmCallerEnv for Host { fn prng_u64_in_inclusive_range( &self, - vmcaller: &mut VmCaller, + _vmcaller: &mut VmCaller, lo: u64, hi: u64, ) -> Result { @@ -3054,7 +2540,7 @@ impl VmCallerEnv for Host { fn prng_vec_shuffle( &self, - vmcaller: &mut VmCaller, + _vmcaller: &mut VmCaller, vec: VecObject, ) -> Result { let vnew = self.visit_obj(vec, |v: &HostVec| { diff --git a/soroban-env-host/src/host/crypto.rs b/soroban-env-host/src/host/crypto.rs index 5db712d32..07bb42325 100644 --- a/soroban-env-host/src/host/crypto.rs +++ b/soroban-env-host/src/host/crypto.rs @@ -63,7 +63,7 @@ impl Host { pub(crate) fn verify_sig_ed25519_internal( &self, payload: &[u8], - public_key: &ed25519_dalek::VerifyingKey, + verifying_key: &ed25519_dalek::VerifyingKey, sig: &ed25519_dalek::Signature, ) -> Result<(), HostError> { let _span = tracy_span!("ed25519 verify"); @@ -71,7 +71,7 @@ impl Host { ContractCostType::VerifyEd25519Sig, Some(payload.len() as u64), )?; - public_key.verify_strict(payload, sig).map_err(|_| { + verifying_key.verify_strict(payload, sig).map_err(|_| { self.err( ScErrorType::Crypto, ScErrorCode::InvalidInput, diff --git a/soroban-env-host/src/host/data_helper.rs b/soroban-env-host/src/host/data_helper.rs index e166f88b5..c686297e8 100644 --- a/soroban-env-host/src/host/data_helper.rs +++ b/soroban-env-host/src/host/data_helper.rs @@ -6,9 +6,10 @@ use soroban_env_common::xdr::{ ExtensionPoint, HashIdPreimageContractId, ScAddress, ScContractInstance, ScErrorCode, ScErrorType, }; -use soroban_env_common::{AddressObject, Env, U32Val}; +use soroban_env_common::{AddressObject, Env, StorageType, U32Val, Val}; use crate::budget::AsBudget; +use crate::storage::{InstanceStorageMap, Storage}; use crate::xdr::{ AccountEntry, AccountId, ContractDataEntry, Hash, HashIdPreimage, LedgerEntry, LedgerEntryData, LedgerEntryExt, LedgerKey, LedgerKeyAccount, LedgerKeyContractCode, LedgerKeyContractData, @@ -20,6 +21,59 @@ use crate::{err, Host, HostError}; use super::metered_clone::{MeteredAlloc, MeteredClone}; impl Host { + pub fn with_mut_storage(&self, f: F) -> Result + where + F: FnOnce(&mut Storage) -> Result, + { + f(&mut *self.try_borrow_storage_mut()?) + } + + /// Immutable accessor to the instance storage of the currently running + /// contract. + /// Performs lazy initialization of instance storage on access. + pub(crate) fn with_instance_storage(&self, f: F) -> Result + where + F: FnOnce(&InstanceStorageMap) -> Result, + { + self.with_current_context_mut(|ctx| { + self.maybe_init_instance_storage(ctx)?; + f(ctx.storage.as_ref().ok_or_else(|| { + self.err( + ScErrorType::Context, + ScErrorCode::InternalError, + "missing instance storage", + &[], + ) + })?) + }) + } + + /// Mutable accessor to the instance storage of the currently running + /// contract. + /// Performs lazy initialization of instance storage on access. + pub(crate) fn with_mut_instance_storage(&self, f: F) -> Result + where + F: FnOnce(&mut InstanceStorageMap) -> Result, + { + self.with_current_context_mut(|ctx| { + self.maybe_init_instance_storage(ctx)?; + let storage = ctx.storage.as_mut().ok_or_else(|| { + self.err( + ScErrorType::Context, + ScErrorCode::InternalError, + "missing instance storage", + &[], + ) + })?; + // Consider any mutable access to be modifying the instance storage. + // This way we would provide consistent footprint (RO for read-only + // ops using `with_instance_storage` and RW for potentially + // mutating ops using `with_mut_instance_storage`). + storage.is_modified = true; + f(storage) + }) + } + pub fn contract_instance_ledger_key( &self, contract_id: &Hash, @@ -372,4 +426,119 @@ impl Host { self.contract_id_from_scaddress(addr.metered_clone(self)?) }) } + + pub(super) fn put_contract_data_into_ledger( + &self, + k: Val, + v: Val, + t: StorageType, + ) -> Result<(), HostError> { + let durability: ContractDataDurability = t.try_into()?; + let key = self.contract_data_key_from_rawval(k, durability)?; + // Currently the storage stores the whole ledger entries, while this + // operation might only modify only the internal `ScVal` value. Thus we + // need to only overwrite the value in case if there is already an + // existing ledger entry value for the key in the storage. + if self + .try_borrow_storage_mut()? + .has(&key, self.as_budget()) + .map_err(|e| self.decorate_contract_data_storage_error(e, k))? + { + let (current, expiration_ledger) = self + .try_borrow_storage_mut()? + .get_with_expiration(&key, self.as_budget()) + .map_err(|e| self.decorate_contract_data_storage_error(e, k))?; + let mut current = (*current).metered_clone(self)?; + match current.data { + LedgerEntryData::ContractData(ref mut entry) => { + entry.val = self.from_host_val(v)?; + } + _ => { + return Err(self.err( + ScErrorType::Storage, + ScErrorCode::InternalError, + "expected DataEntry", + &[], + )); + } + } + self.try_borrow_storage_mut()? + .put( + &key, + &Rc::metered_new(current, self)?, + expiration_ledger, + self.as_budget(), + ) + .map_err(|e| self.decorate_contract_data_storage_error(e, k))?; + } else { + let data = ContractDataEntry { + contract: ScAddress::Contract(self.get_current_contract_id_internal()?), + key: self.from_host_val(k)?, + val: self.from_host_val(v)?, + durability, + ext: ExtensionPoint::V0, + }; + self.try_borrow_storage_mut()? + .put( + &key, + &Host::new_contract_data(self, data)?, + Some(self.get_min_expiration_ledger(durability)?), + self.as_budget(), + ) + .map_err(|e| self.decorate_contract_data_storage_error(e, k))?; + } + + Ok(()) + } +} + +#[cfg(any(test, feature = "testutils"))] +use crate::storage::{AccessType, Footprint}; + +#[cfg(any(test, feature = "testutils"))] +impl Host { + // Writes an arbitrary ledger entry to storage. + // "testutils" are not covered by budget metering. + pub fn add_ledger_entry( + &self, + key: &Rc, + val: &Rc, + expiration_ledger: Option, + ) -> Result<(), HostError> { + self.as_budget().with_free_budget(|| { + self.with_mut_storage(|storage| { + storage.put(key, val, expiration_ledger, self.as_budget()) + }) + }) + } + + // Performs the necessary setup to access the provided ledger key/entry in + // enforcing storage mode. + // "testutils" are not covered by budget metering. + pub fn setup_storage_entry( + &self, + key: Rc, + val: Option<(Rc, Option)>, + access_type: AccessType, + ) -> Result<(), HostError> { + self.as_budget().with_free_budget(|| { + self.with_mut_storage(|storage| { + storage + .footprint + .record_access(&key, access_type, self.as_budget())?; + storage.map = storage.map.insert(key, val, self.as_budget())?; + Ok(()) + }) + }) + } + + // Performs the necessary setup to access all the entries in provided + // footprint in enforcing mode. + // "testutils" are not covered by budget metering. + pub fn setup_storage_footprint(&self, footprint: Footprint) -> Result<(), HostError> { + for (key, access_type) in footprint.0.map { + self.setup_storage_entry(key, None, access_type)?; + } + Ok(()) + } } diff --git a/soroban-env-host/src/host/error.rs b/soroban-env-host/src/host/error.rs index ab0d8269f..cb670cd96 100644 --- a/soroban-env-host/src/host/error.rs +++ b/soroban-env-host/src/host/error.rs @@ -285,6 +285,16 @@ impl Host { ) } + pub(crate) fn err_oob_object_index(&self, index: Option) -> HostError { + let type_ = ScErrorType::Object; + let code = ScErrorCode::IndexBounds; + let msg = "object index out of bounds"; + match index { + None => self.err(type_, code, msg, &[]), + Some(index) => self.err(type_, code, msg, &[U32Val::from(index).to_val()]), + } + } + pub(crate) fn err_wasmi_fuel_metering_disabled(&self) -> HostError { self.err( ScErrorType::WasmVm, diff --git a/soroban-env-host/src/host/lifecycle.rs b/soroban-env-host/src/host/lifecycle.rs new file mode 100644 index 000000000..87b78edcc --- /dev/null +++ b/soroban-env-host/src/host/lifecycle.rs @@ -0,0 +1,255 @@ +use crate::{ + budget::AsBudget, + err, + host::{ + metered_clone::{MeteredAlloc, MeteredClone}, + metered_write_xdr, ContractReentryMode, CreateContractArgs, + }, + xdr::{ + Asset, ContractCodeEntry, ContractDataDurability, ContractExecutable, ContractIdPreimage, + ContractIdPreimageFromAddress, ExtensionPoint, Hash, LedgerKey, LedgerKeyContractCode, + ScAddress, ScContractInstance, ScErrorCode, ScErrorType, + }, + AddressObject, BytesObject, Host, HostError, Symbol, TryFromVal, Vm, +}; +use std::rc::Rc; + +impl Host { + // Notes on metering: this is covered by the called components. + fn create_contract_with_id( + &self, + contract_id: Hash, + contract_executable: ContractExecutable, + ) -> Result<(), HostError> { + let storage_key = self.contract_instance_ledger_key(&contract_id)?; + if self + .try_borrow_storage_mut()? + .has(&storage_key, self.as_budget()) + .map_err(|e| self.decorate_contract_instance_storage_error(e, &contract_id))? + { + return Err(self.err( + ScErrorType::Storage, + ScErrorCode::ExistingValue, + "contract already exists", + &[self + .add_host_object(self.scbytes_from_hash(&contract_id)?)? + .into()], + )); + } + // Make sure the contract code exists. Without this check it would be + // possible to accidentally create a contract that never may be invoked + // (just by providing a bad hash). + if let ContractExecutable::Wasm(wasm_hash) = &contract_executable { + if !self.wasm_exists(wasm_hash)? { + return Err(err!( + self, + (ScErrorType::Storage, ScErrorCode::MissingValue), + "Wasm does not exist", + *wasm_hash + )); + } + } + let instance = ScContractInstance { + executable: contract_executable, + storage: Default::default(), + }; + self.store_contract_instance(instance, contract_id, &storage_key)?; + Ok(()) + } + + fn maybe_initialize_asset_token( + &self, + contract_id: &Hash, + id_preimage: &ContractIdPreimage, + ) -> Result<(), HostError> { + if let ContractIdPreimage::Asset(asset) = id_preimage { + let mut asset_bytes: Vec = Default::default(); + metered_write_xdr(self.budget_ref(), asset, &mut asset_bytes)?; + self.call_n_internal( + contract_id, + Symbol::try_from_val(self, &"init_asset")?, + &[self + .add_host_object(self.scbytes_from_vec(asset_bytes)?)? + .into()], + ContractReentryMode::Prohibited, + false, + )?; + Ok(()) + } else { + Ok(()) + } + } + + pub(crate) fn create_contract_internal( + &self, + deployer: Option, + args: CreateContractArgs, + ) -> Result { + let has_deployer = deployer.is_some(); + if has_deployer { + self.try_borrow_authorization_manager()? + .push_create_contract_host_fn_frame(self, args.metered_clone(self)?)?; + } + // Make sure that even in case of operation failure we still pop the + // stack frame. + // This is hacky, but currently this is the only instance where we need + // to manually manage auth manager frames (we don't need to authorize + // any other host fns and it doesn't seem useful to create extra frames + // for them just to make auth work in a single case). + let res = self.create_contract_with_optional_auth(deployer, args); + if has_deployer { + self.try_borrow_authorization_manager()?.pop_frame(self)?; + } + res + } + + fn create_contract_with_optional_auth( + &self, + deployer: Option, + args: CreateContractArgs, + ) -> Result { + if let Some(deployer_address) = deployer { + self.try_borrow_authorization_manager()?.require_auth( + self, + deployer_address, + Default::default(), + )?; + } + + let id_preimage = + self.get_full_contract_id_preimage(args.contract_id_preimage.metered_clone(self)?)?; + let hash_id = Hash(self.metered_hash_xdr(&id_preimage)?); + self.create_contract_with_id(hash_id.metered_clone(self)?, args.executable)?; + self.maybe_initialize_asset_token(&hash_id, &args.contract_id_preimage)?; + self.add_host_object(ScAddress::Contract(hash_id)) + } + + pub(crate) fn get_contract_id_hash( + &self, + deployer: AddressObject, + salt: BytesObject, + ) -> Result { + let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress { + address: self.visit_obj(deployer, |addr: &ScAddress| addr.metered_clone(self))?, + salt: self.u256_from_bytesobj_input("contract_id_salt", salt)?, + }); + + let id_preimage = + self.get_full_contract_id_preimage(contract_id_preimage.metered_clone(self)?)?; + Ok(Hash(self.metered_hash_xdr(&id_preimage)?)) + } + + pub(crate) fn get_asset_contract_id_hash(&self, asset: Asset) -> Result { + let id_preimage = self.get_full_contract_id_preimage(ContractIdPreimage::Asset(asset))?; + let id_arr: [u8; 32] = self.metered_hash_xdr(&id_preimage)?; + Ok(Hash(id_arr)) + } + + pub(crate) fn upload_contract_wasm(&self, wasm: Vec) -> Result { + let hash_bytes: [u8; 32] = crypto::sha256_hash_from_bytes(wasm.as_slice(), self)? + .try_into() + .map_err(|_| { + self.err( + ScErrorType::Value, + ScErrorCode::InternalError, + "unexpected hash length", + &[], + ) + })?; + + // Check size before instantiation. + let wasm_bytes_m: crate::xdr::BytesM = wasm.try_into().map_err(|_| { + self.err( + ScErrorType::Value, + ScErrorCode::ExceededLimit, + "Wasm code is too large", + &[], + ) + })?; + + // Instantiate a temporary / throwaway VM using this wasm. This will do + // both quick checks like "does this wasm have the right protocol number + // to run on this network" and also a full parse-and-link pass to check + // that the wasm is basically not garbage. It might still fail to run + // but it will at least instantiate. This might seem a bit heavyweight + // but really "instantiating a VM" is mostly just "parsing the module + // and doing those checks" anyway. Revisit in the future if you want to + // try to split these costs up some. + if cfg!(any(test, feature = "testutils")) && wasm_bytes_m.as_slice().is_empty() { + // Allow a zero-byte contract when testing, as this is used to make + // native test contracts behave like wasm. They will never be + // instantiated, this is just to exercise their storage logic. + } else { + let _check_vm = Vm::new( + self, + Hash(hash_bytes.metered_clone(self)?), + wasm_bytes_m.as_slice(), + )?; + } + + let hash_obj = self.add_host_object(self.scbytes_from_slice(hash_bytes.as_slice())?)?; + let code_key = Rc::metered_new( + LedgerKey::ContractCode(LedgerKeyContractCode { + hash: Hash(hash_bytes.metered_clone(self)?), + }), + self, + )?; + if !self + .try_borrow_storage_mut()? + .has(&code_key, self.as_budget()) + .map_err(|e| self.decorate_contract_code_storage_error(e, &Hash(hash_bytes)))? + { + self.with_mut_storage(|storage| { + let data = ContractCodeEntry { + hash: Hash(hash_bytes), + ext: ExtensionPoint::V0, + code: wasm_bytes_m, + }; + storage.put( + &code_key, + &Host::new_contract_code(self, data)?, + Some(self.get_min_expiration_ledger(ContractDataDurability::Persistent)?), + self.as_budget(), + ) + })?; + } + Ok(hash_obj) + } +} + +use super::crypto; +#[cfg(any(test, feature = "testutils"))] +use super::ContractFunctionSet; + +// "testutils" is not covered by budget metering. +#[cfg(any(test, feature = "testutils"))] +impl Host { + pub fn register_test_contract( + &self, + contract_address: AddressObject, + contract_fns: Rc, + ) -> Result<(), HostError> { + let contract_id = self.contract_id_from_address(contract_address)?; + let instance_key = self.contract_instance_ledger_key(&contract_id)?; + // Test contract might be overriding an already registered Wasm + // contract, in which case we should preserve the instance entry. + if self + .retrieve_contract_instance_from_storage(&instance_key) + .is_err() + { + // Use empty Wasm as default executable. It shouldn't be observable, + // but allows exercising bump logic and make operations like Wasm + // update more meaningful. + let wasm_hash_obj = self.upload_contract_wasm(vec![])?; + let wasm_hash = self.hash_from_bytesobj_input("wasm_hash", wasm_hash_obj)?; + let instance = ScContractInstance { + executable: ContractExecutable::Wasm(wasm_hash), + storage: None, + }; + self.store_contract_instance(instance, contract_id.clone(), &instance_key)?; + }; + let mut contracts = self.try_borrow_contracts_mut()?; + contracts.insert(contract_id, contract_fns); + Ok(()) + } +} diff --git a/soroban-env-host/src/host/validity.rs b/soroban-env-host/src/host/validity.rs index fc9e50190..40d397931 100644 --- a/soroban-env-host/src/host/validity.rs +++ b/soroban-env-host/src/host/validity.rs @@ -13,12 +13,7 @@ impl Host { bound: usize, ) -> Result<(), HostError> { if index as usize >= bound { - return Err(self.err( - ScErrorType::Object, - ScErrorCode::IndexBounds, - "index out of bound", - &[U32Val::from(index).to_val()], - )); + return Err(self.err_oob_object_index(Some(index))); } Ok(()) } @@ -30,12 +25,7 @@ impl Host { bound: usize, ) -> Result<(), HostError> { if index as usize > bound { - return Err(self.err( - ScErrorType::Object, - ScErrorCode::IndexBounds, - "index out of bound", - &[U32Val::from(index).to_val()], - )); + return Err(self.err_oob_object_index(Some(index))); } Ok(()) } @@ -47,22 +37,8 @@ impl Host { end: u32, bound: usize, ) -> Result, HostError> { - if start as usize > bound { - return Err(self.err( - ScErrorType::Object, - ScErrorCode::IndexBounds, - "start index out of bound", - &[U32Val::from(start).to_val()], - )); - } - if end as usize > bound { - return Err(self.err( - ScErrorType::Object, - ScErrorCode::IndexBounds, - "end index out of bound", - &[U32Val::from(end).to_val()], - )); - } + self.validate_index_le_bound(start, bound)?; + self.validate_index_le_bound(end, bound)?; if start > end { return Err(self.err( ScErrorType::Object, @@ -76,4 +52,22 @@ impl Host { end: end as usize, }) } + + pub(crate) fn validate_usize_sum_fits_in_u32( + &self, + a: usize, + b: usize, + ) -> Result { + let lim = u32::MAX as usize; + if a > lim || b > lim || a > (lim - b) { + Err(self.err( + ScErrorType::Value, + ScErrorCode::ExceededLimit, + "sum of sizes exceeds u32::MAX", + &[], + )) + } else { + Ok(a + b) + } + } }