Repository: stellar/rs-soroban-env
Pull request #1354 (2024-02-24 01:53:57)

Refactor soroban-simulation to use the e2e_invoke
### What

Refactor soroban-simulation to use the e2e_invoke and also to be more modular/reusable.

- Use e2e_invoke for invoking host functions in recording mode
- Separate network config and adjustment config into separate structs for easier testing/caching etc.
- Clean up the storage logic to clearly distinguish between snapshots with and without archive access
- Added some documentation for public functions/structs
- Added some basic test coverage. I didn't try to verify the math for every number, but I did sanity-check them. The host function simulation accuracy is mostly covered on `e2e_invoke` side.
- Reuse host code for common test and non-test utilities

### Why

Improving simulation quality, as well as maintainability.

### Known limitations

N/A


2opremio: Another option is to do this in a two-step process, keeping an externally-compatible layer at first.
dmkozh: > Did you take into consideration the impact at https://github.com/stellar/soroban-rpc/blob/main/cmd/soroban-rpc/lib/preflight/src/lib.rs and whether it satisfies all the current functionality?

Of course, these changes are meant to be functionally no-op (besides the additional capabilities, like ledger diffs we've discussed before - but there is no need to use them immediately). I'll definitely help with the migration, but this PR has to be finalized first.

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index ee623ca8..e97a5c63 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1441,9 +1441,12 @@ name = "soroban-simulation"
 version = "20.2.2"
 dependencies = [
  "anyhow",
+ "pretty_assertions",
  "rand",
  "soroban-env-host",
+ "soroban-test-wasms",
  "static_assertions",
+ "tap",
  "thiserror",
 ]
 
@@ -1557,6 +1560,12 @@ dependencies = [
  "unicode-width",
 ]
 
+[[package]]
+name = "tap"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+
 [[package]]
 name = "test_no_std"
 version = "20.2.2"
diff --git a/Cargo.toml b/Cargo.toml
index 827308dc..30ad077b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,6 +17,9 @@ members = [
 exclude = ["soroban-test-wasms/wasm-workspace"]
 
 [workspace.package]
+# NB: When bumping the major version make sure to clean up the
+# code guarded by `unstable-*` features to make it enabled
+# unconditionally.
 version = "20.2.2"
 rust-version = "1.74.0"
 
diff --git a/soroban-env-host/Cargo.toml b/soroban-env-host/Cargo.toml
index 85258797..78fb829c 100644
--- a/soroban-env-host/Cargo.toml
+++ b/soroban-env-host/Cargo.toml
@@ -96,6 +96,14 @@ next = ["soroban-env-common/next"]
 tracy = ["dep:tracy-client", "soroban-env-common/tracy"]
 recording_mode = []
 bench = []
+# This feature guards the work-in-progress changes in soroban-env-host
+# API. Its main purpose is to be able to make API changes without bumping
+# the crate version.
+# These changes may be used by the 'Soroban-owned' crates, such as
+# stellar-core, soroban-simulation, and soroban-sdk.
+# When bumping the major version of soroban-env-host all the code guarded
+# by this feature should be enabled unconditionally.
+unstable-next-api = []
 
 [[bench]]
 required-features = ["bench"]
diff --git a/soroban-env-host/src/fees.rs b/soroban-env-host/src/fees.rs
index 7d1b9e68..b919cc3c 100644
--- a/soroban-env-host/src/fees.rs
+++ b/soroban-env-host/src/fees.rs
@@ -40,6 +40,8 @@ pub struct TransactionResources {
 /// This should be normally loaded from the ledger, with exception of the
 /// `fee_per_write_1kb`, that has to be computed via `compute_write_fee_per_1kb`
 /// function.
+
+#[derive(Debug, Default, PartialEq, Eq)]
 pub struct FeeConfiguration {
     /// Fee per `INSTRUCTIONS_INCREMENT=10000` instructions.
     pub fee_per_instruction_increment: i64,
@@ -64,6 +66,7 @@ pub struct FeeConfiguration {
 /// Network configuration used to determine the ledger write fee.
 ///
 /// This should be normally loaded from the ledger.
+#[derive(Debug, Default, PartialEq, Eq)]
 pub struct WriteFeeConfiguration {
     // Write fee grows linearly until bucket list reaches this size.
     pub bucket_list_target_size_bytes: i64,
@@ -104,6 +107,8 @@ pub struct LedgerEntryRentChange {
 /// This should be normally loaded from the ledger, with exception of the
 /// `fee_per_write_1kb`, that has to be computed via `compute_write_fee_per_1kb`
 /// function.
+
+#[derive(Debug, Default, PartialEq, Eq)]
 pub struct RentFeeConfiguration {
     /// Fee per 1KB written to ledger.
     /// This is the same field as in `FeeConfiguration` and it has to be
@@ -279,7 +284,7 @@ pub fn compute_write_fee_per_1kb(
 /// ledger changes. This is due to the fact that rent is managed automatically
 /// wihtout user-provided inputs.
 pub fn compute_rent_fee(
-    changed_entries: &Vec<LedgerEntryRentChange>,
+    changed_entries: &[LedgerEntryRentChange],
     fee_config: &RentFeeConfiguration,
     current_ledger_seq: u32,
 ) -> i64 {
diff --git a/soroban-env-host/src/host.rs b/soroban-env-host/src/host.rs
index 33b8aecc..ecd9a511 100644
--- a/soroban-env-host/src/host.rs
+++ b/soroban-env-host/src/host.rs
@@ -17,9 +17,9 @@ use crate::{
         ScSymbol, ScVal, TimePoint, Uint256,
     },
     AddressObject, Bool, BytesObject, Compare, ConversionError, EnvBase, Error, I128Object,
-    I256Object, MapObject, Object, StorageType, StringObject, Symbol, SymbolObject, TryFromVal,
-    U128Object, U256Object, U32Val, U64Val, Val, VecObject, VmCaller, VmCallerEnv, Void, I256,
-    U256,
+    I256Object, LedgerInfo, MapObject, Object, StorageType, StringObject, Symbol, SymbolObject,
+    TryFromVal, U128Object, U256Object, U32Val, U64Val, Val, VecObject, VmCaller, VmCallerEnv,
+    Void, I256, U256,
 };
 
 mod comparison;
@@ -61,18 +61,6 @@ pub(crate) use frame::Frame;
 use rand_chacha::ChaCha20Rng;
 use soroban_env_common::SymbolSmall;
 
-#[derive(Debug, Clone, Default)]
-pub struct LedgerInfo {
-    pub protocol_version: u32,
-    pub sequence_number: u32,
-    pub timestamp: u64,
-    pub network_id: [u8; 32],
-    pub base_reserve: u32,
-    pub min_temp_entry_ttl: u32,
-    pub min_persistent_entry_ttl: u32,
-    pub max_entry_ttl: u32,
-}
-
 #[cfg(any(test, feature = "testutils"))]
 #[derive(Clone, Copy)]
 pub enum ContractInvocationEvent {
diff --git a/soroban-env-host/src/host/ledger_info_helper.rs b/soroban-env-host/src/host/ledger_info_helper.rs
index c7a0fe62..3c262ea9 100644
--- a/soroban-env-host/src/host/ledger_info_helper.rs
+++ b/soroban-env-host/src/host/ledger_info_helper.rs
@@ -1,25 +1,15 @@
 use crate::{
-    xdr::{ContractDataDurability, LedgerKey, ScErrorCode, ScErrorType},
-    Error, Host, HostError, LedgerInfo,
+    xdr::{ContractDataDurability, ScErrorCode, ScErrorType},
+    Error, Host, HostError,
 };
 
 impl Host {
     pub(crate) fn get_min_live_until_ledger(
         &self,
-        storage_type: ContractDataDurability,
+        durability: ContractDataDurability,
     ) -> Result<u32, HostError> {
-        let ledger_seq = self.with_ledger_info(|li| Ok(li.sequence_number))?;
-        let min_live_until = match storage_type {
-            ContractDataDurability::Temporary => {
-                self.with_ledger_info(|li| Ok(li.min_temp_entry_ttl))?
-            }
-            ContractDataDurability::Persistent => {
-                self.with_ledger_info(|li: &LedgerInfo| Ok(li.min_persistent_entry_ttl))?
-            }
-        };
-        ledger_seq
-            .checked_add(min_live_until.saturating_sub(1))
-            .ok_or_else(|| {
+        self.with_ledger_info(|li| {
+            li.min_live_until_ledger_checked(durability).ok_or_else(|| {
                 // overflowing here means a misconfiguration of the network (the
                 // ttl is too large), in which case we immediately flag it as an
                 // unrecoverable `InternalError`, even though the source is
@@ -29,32 +19,21 @@ impl Host {
                     ScErrorCode::InternalError,
                 ))
             })
+        })
     }
 
     pub(crate) fn max_live_until_ledger(&self) -> Result<u32, HostError> {
         self.with_ledger_info(|li| {
-            li.sequence_number
-                // Entry can live for at most max_entry_live_until ledgers from
-                // now, counting the current one.
-                .checked_add(li.max_entry_ttl.saturating_sub(1))
-                .ok_or_else(|| {
-                    // overflowing here means a misconfiguration of the network
-                    // (the ttl is too large), in which case we immediately flag
-                    // it as an unrecoverable `InternalError`, even though the
-                    // source is external to the host.
-                    HostError::from(Error::from_type_and_code(
-                        ScErrorType::Context,
-                        ScErrorCode::InternalError,
-                    ))
-                })
+            li.max_live_until_ledger_checked().ok_or_else(|| {
+                // overflowing here means a misconfiguration of the network
+                // (the ttl is too large), in which case we immediately flag
+                // it as an unrecoverable `InternalError`, even though the
+                // source is external to the host.
+                HostError::from(Error::from_type_and_code(
+                    ScErrorType::Context,
+                    ScErrorCode::InternalError,
+                ))
+            })
         })
     }
 }
-
-pub fn get_key_durability(key: &LedgerKey) -> Option<ContractDataDurability> {
-    match &key {
-        LedgerKey::ContractData(d) => Some(d.durability),
-        LedgerKey::ContractCode(_) => Some(ContractDataDurability::Persistent),
-        _ => None,
-    }
-}
diff --git a/soroban-env-host/src/ledger_info.rs b/soroban-env-host/src/ledger_info.rs
new file mode 100644
index 00000000..aa7587b4
--- /dev/null
+++ b/soroban-env-host/src/ledger_info.rs
@@ -0,0 +1,39 @@
+use crate::xdr::{ContractDataDurability, LedgerKey};
+
+#[derive(Debug, Clone, Default, PartialEq, Eq)]
+pub struct LedgerInfo {
+    pub protocol_version: u32,
+    pub sequence_number: u32,
+    pub timestamp: u64,
+    pub network_id: [u8; 32],
+    pub base_reserve: u32,
+    pub min_temp_entry_ttl: u32,
+    pub min_persistent_entry_ttl: u32,
+    pub max_entry_ttl: u32,
+}
+
+impl LedgerInfo {
+    pub fn min_live_until_ledger_checked(&self, durability: ContractDataDurability) -> Option<u32> {
+        let min_live_until = match durability {
+            ContractDataDurability::Temporary => self.min_temp_entry_ttl,
+            ContractDataDurability::Persistent => self.min_persistent_entry_ttl,
+        };
+        self.sequence_number
+            .checked_add(min_live_until.saturating_sub(1))
+    }
+
+    pub fn max_live_until_ledger_checked(&self) -> Option<u32> {
+        self.sequence_number
+            // Entry can live for at most max_entry_live_until ledgers from
+            // now, counting the current one.
+            .checked_add(self.max_entry_ttl.saturating_sub(1))
+    }
+}
+
+pub fn get_key_durability(key: &LedgerKey) -> Option<ContractDataDurability> {
+    match &key {
+        LedgerKey::ContractData(d) => Some(d.durability),
+        LedgerKey::ContractCode(_) => Some(ContractDataDurability::Persistent),
+        _ => None,
+    }
+}
diff --git a/soroban-env-host/src/lib.rs b/soroban-env-host/src/lib.rs
index 67d0ff02..e5dafaea 100644
--- a/soroban-env-host/src/lib.rs
+++ b/soroban-env-host/src/lib.rs
@@ -37,11 +37,13 @@ pub use vm::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, LedgerInfo, Seed,
-    SEED_BYTES,
+    metered_map::MeteredOrdMap, metered_vector::MeteredVector, Host, HostError, Seed, SEED_BYTES,
 };
 pub use soroban_env_common::*;
 
+pub mod ledger_info;
+pub use ledger_info::LedgerInfo;
+
 pub mod e2e_invoke;
 pub mod fees;
 
@@ -59,5 +61,8 @@ pub use host::{ContractFunctionSet, ContractInvocationEvent};
 #[doc(hidden)]
 pub mod testutils;
 
+#[cfg(any(test, feature = "testutils"))]
+#[doc(hidden)]
+pub mod e2e_testutils;
 #[cfg(test)]
 mod test;
diff --git a/soroban-env-host/src/storage.rs b/soroban-env-host/src/storage.rs
index cd36a8b9..9a849094 100644
--- a/soroban-env-host/src/storage.rs
+++ b/soroban-env-host/src/storage.rs
@@ -11,7 +11,8 @@ use std::rc::Rc;
 
 use crate::{
     budget::Budget,
-    host::{ledger_info_helper::get_key_durability, metered_map::MeteredOrdMap},
+    host::metered_map::MeteredOrdMap,
+    ledger_info::get_key_durability,
     xdr::{ContractDataDurability, LedgerEntry, LedgerKey, ScErrorCode, ScErrorType},
     Env, Error, Host, HostError, Val,
 };
@@ -53,6 +54,15 @@ pub enum AccessType {
 
 /// A helper type used by [FootprintMode::Recording] to provide access
 /// to a stable read-snapshot of a ledger.
+/// The snapshot is expected to only return live ledger entries.
+#[cfg(any(test, feature = "unstable-next-api"))]
+pub trait SnapshotSource {
+    /// Returns the ledger entry for the key and its live_until ledger if entry
+    /// exists, or `None` otherwise.
+    fn get(&self, key: &Rc<LedgerKey>) -> Result<Option<EntryWithLiveUntil>, HostError>;
+}
+
+#[cfg(not(any(test, feature = "unstable-next-api")))]
 pub trait SnapshotSource {
     // Returns the ledger entry for the key and its live_until ledger.
     fn get(&self, key: &Rc<LedgerKey>) -> Result<EntryWithLiveUntil, HostError>;
@@ -461,11 +471,16 @@ impl Storage {
                 // In recording mode we treat the map as a cache
                 // that misses read-through to the underlying src.
                 if !self.map.contains_key::<Rc<LedgerKey>>(key, budget)? {
+                    #[cfg(any(test, feature = "unstable-next-api"))]
+                    let value = src.get(&key)?;
+
+                    #[cfg(not(any(test, feature = "unstable-next-api")))]
                     let value = if src.has(&key)? {
                         Some(src.get(key)?)
                     } else {
                         None
                     };
+
                     self.map = self.map.insert(key.clone(), value, budget)?;
                 }
             }
diff --git a/soroban-simulation/Cargo.toml b/soroban-simulation/Cargo.toml
index c3bf3bd9..68d66869 100644
--- a/soroban-simulation/Cargo.toml
+++ b/soroban-simulation/Cargo.toml
@@ -13,13 +13,21 @@ rust-version.workspace = true
 # Temporary until we publish, to satisfy cargo-semver-checks
 publish = true
 
+[features]
+testutils = ["soroban-env-host/testutils"]
+
 [dependencies]
-# To be removed
 anyhow = { version = "1.0.75", features = [] }
 thiserror = "1.0.46"
-soroban-env-host = { workspace = true,  features = ["recording_mode"]}
+soroban-env-host = { workspace = true,  features = ["recording_mode", "unstable-next-api"]}
 static_assertions = "1.1.0"
 rand = "0.8.5"
 
+[dev-dependencies]
+soroban-env-host = { workspace = true,  features = ["recording_mode", "testutils", "unstable-next-api"]}
+soroban-test-wasms = { package = "soroban-test-wasms", path = "../soroban-test-wasms" }
+pretty_assertions = "1.4"
+tap = "1.0.1"
+
 [package.metadata.docs.rs]
 all-features = true
diff --git a/soroban-simulation/src/fees.rs b/soroban-simulation/src/fees.rs
deleted file mode 100644
index 55f886e6..00000000
--- a/soroban-simulation/src/fees.rs
+++ /dev/null
@@ -1,497 +0,0 @@
-use super::ledger_storage::{LedgerGetter, LedgerStorage};
-use super::state_ttl::{get_restored_ledger_sequence, TTLLedgerEntry};
-use crate::ResourceConfig;
-use anyhow::{bail, ensure, Context, Error, Result};
-use soroban_env_host::budget::Budget;
-use soroban_env_host::e2e_invoke::{
-    extract_rent_changes, get_ledger_changes, LedgerEntryChange, TtlEntryMap,
-};
-use soroban_env_host::fees::{
-    compute_rent_fee, compute_transaction_resource_fee, compute_write_fee_per_1kb,
-    FeeConfiguration, LedgerEntryRentChange, RentFeeConfiguration, TransactionResources,
-    WriteFeeConfiguration,
-};
-use soroban_env_host::storage::{AccessType, Footprint, Storage};
-use soroban_env_host::xdr;
-use soroban_env_host::xdr::ContractDataDurability::Persistent;
-use soroban_env_host::xdr::{
-    ConfigSettingEntry, ConfigSettingId, ContractEventType, DecoratedSignature, DiagnosticEvent,
-    ExtendFootprintTtlOp, ExtensionPoint, InvokeHostFunctionOp, LedgerFootprint, LedgerKey, Limits,
-    Memo, MuxedAccount, MuxedAccountMed25519, Operation, OperationBody, Preconditions,
-    RestoreFootprintOp, ScVal, SequenceNumber, Signature, SignatureHint, SorobanResources,
-    SorobanTransactionData, Transaction, TransactionExt, TransactionV1Envelope, Uint256, WriteXdr,
-};
-use std::cmp::max;
-use std::convert::{TryFrom, TryInto};
-
-#[allow(clippy::too_many_arguments)]
-pub(crate) fn compute_host_function_transaction_data_and_min_fee(
-    op: &InvokeHostFunctionOp,
-    pre_storage: &LedgerStorage,
-    post_storage: &Storage,
-    budget: &Budget,
-    resource_config: &ResourceConfig,
-    events: &[DiagnosticEvent],
-    invocation_result: &ScVal,
-    bucket_list_size: u64,
-    current_ledger_seq: u32,
-) -> Result<(SorobanTransactionData, i64)> {
-    let ledger_changes = get_ledger_changes(budget, post_storage, pre_storage, TtlEntryMap::new())?;
-    let soroban_resources = calculate_host_function_soroban_resources(
-        &ledger_changes,
-        &post_storage.footprint,
-        budget,
-        resource_config,
-    )
-    .context("cannot compute host function resources")?;
-
-    let contract_events_size =
-        calculate_contract_events_size_bytes(events).context("cannot calculate events size")?;
-    let invocation_return_size = u32::try_from(invocation_result.to_xdr(Limits::none())?.len())?;
-    // This is totally unintuitive, but it's what's expected by the library
-    let final_contract_events_size = contract_events_size + invocation_return_size;
-
-    let transaction_resources = TransactionResources {
-        instructions: soroban_resources.instructions,
-        read_entries: u32::try_from(soroban_resources.footprint.read_only.as_vec().len())?,
-        write_entries: u32::try_from(soroban_resources.footprint.read_write.as_vec().len())?,
-        read_bytes: soroban_resources.read_bytes,
-        write_bytes: soroban_resources.write_bytes,
-        // Note: we could get a better transaction size if the full transaction was passed down
-        transaction_size_bytes: estimate_max_transaction_size_for_operation(
-            &OperationBody::InvokeHostFunction(op.clone()),
-            &soroban_resources.footprint,
-        )
-        .context("cannot estimate maximum transaction size")?,
-        contract_events_size_bytes: final_contract_events_size,
-    };
-    let rent_changes = extract_rent_changes(&ledger_changes);
-
-    finalize_transaction_data_and_min_fee(
-        pre_storage,
-        &transaction_resources,
-        soroban_resources,
-        &rent_changes,
-        current_ledger_seq,
-        bucket_list_size,
-    )
-}
-
-fn estimate_max_transaction_size_for_operation(
-    op: &OperationBody,
-    fp: &LedgerFootprint,
-) -> Result<u32> {
-    let source = MuxedAccount::MuxedEd25519(MuxedAccountMed25519 {
-        id: 0,
-        ed25519: Uint256([0; 32]),
-    });
-    // generate the maximum memo size and signature size
-    // TODO: is this being too conservative?
-    let memo_text: Vec<u8> = [0; 28].into();
-    let signatures: Vec<DecoratedSignature> = vec![
-        DecoratedSignature {
-            hint: SignatureHint([0; 4]),
-            signature: Signature::default(),
-        };
-        20
-    ];
-    let envelope = TransactionV1Envelope {
-        tx: Transaction {
-            source_account: source.clone(),
-            fee: 0,
-            seq_num: SequenceNumber(0),
-            cond: Preconditions::None,
-            memo: Memo::Text(memo_text.try_into()?),
-            operations: vec![Operation {
-                source_account: Some(source),
-                body: op.clone(),
-            }]
-            .try_into()?,
-            ext: TransactionExt::V1(SorobanTransactionData {
-                resources: SorobanResources {
-                    footprint: fp.clone(),
-                    instructions: 0,
-                    read_bytes: 0,
-                    write_bytes: 0,
-                },
-                resource_fee: 0,
-                ext: ExtensionPoint::V0,
-            }),
-        },
-        signatures: signatures.try_into()?,
-    };
-
-    let envelope_xdr = envelope.to_xdr(Limits::none())?;
-    let envelope_size = envelope_xdr.len();
-
-    // Add a 15% leeway
-    let envelope_size = envelope_size * 115 / 100;
-    Ok(u32::try_from(envelope_size)?)
-}
-
-#[allow(clippy::cast_possible_truncation)]
-fn calculate_host_function_soroban_resources(
-    ledger_changes: &[LedgerEntryChange],
-    footprint: &Footprint,
-    budget: &Budget,
-    resource_config: &ResourceConfig,
-) -> Result<SorobanResources> {
-    let ledger_footprint = storage_footprint_to_ledger_footprint(footprint)
-        .context("cannot convert storage footprint to ledger footprint")?;
-    let read_bytes: u32 = ledger_changes.iter().map(|c| c.old_entry_size_bytes).sum();
-
-    let write_bytes: u32 = ledger_changes
-        .iter()
-        .map(|c| c.encoded_new_value.as_ref().map_or(0, Vec::len) as u32)
-        .sum();
-
-    // Add a 20% leeway with a minimum of 1 million instructions
-    let budget_instructions = budget
-        .get_cpu_insns_consumed()
-        .context("cannot get instructions consumed")?;
-    let instructions = max(
-        budget_instructions + resource_config.instruction_leeway,
-        budget_instructions * 120 / 100,
-    );
-    Ok(SorobanResources {
-        footprint: ledger_footprint,
-        instructions: u32::try_from(instructions)?,
-        read_bytes,
-        write_bytes,
-    })
-}
-
-#[allow(clippy::cast_possible_wrap)]
-fn get_fee_configurations(
-    ledger_storage: &LedgerStorage,
-    bucket_list_size: u64,
-) -> Result<(FeeConfiguration, RentFeeConfiguration)> {
-    let ConfigSettingEntry::ContractComputeV0(compute) =
-        ledger_storage.get_configuration_setting(ConfigSettingId::ContractComputeV0)?
-    else {
-        bail!("unexpected config setting entry for ComputeV0 key");
-    };
-
-    let ConfigSettingEntry::ContractLedgerCostV0(ledger_cost) =
-        ledger_storage.get_configuration_setting(ConfigSettingId::ContractLedgerCostV0)?
-    else {
-        bail!("unexpected config setting entry for LedgerCostV0 key");
-    };
-
-    let ConfigSettingEntry::ContractHistoricalDataV0(historical_data) =
-        ledger_storage.get_configuration_setting(ConfigSettingId::ContractHistoricalDataV0)?
-    else {
-        bail!("unexpected config setting entry for HistoricalDataV0 key");
-    };
-
-    let ConfigSettingEntry::ContractEventsV0(events) =
-        ledger_storage.get_configuration_setting(ConfigSettingId::ContractEventsV0)?
-    else {
-        bail!("unexpected config setting entry for EventsV0 key");
-    };
-
-    let ConfigSettingEntry::ContractBandwidthV0(bandwidth) =
-        ledger_storage.get_configuration_setting(ConfigSettingId::ContractBandwidthV0)?
-    else {
-        bail!("unexpected config setting entry for BandwidthV0 key");
-    };
-
-    let ConfigSettingEntry::StateArchival(state_archival) =
-        ledger_storage.get_configuration_setting(ConfigSettingId::StateArchival)?
-    else {
-        bail!("unexpected config setting entry for StateArchival key");
-    };
-
-    let write_fee_configuration = WriteFeeConfiguration {
-        bucket_list_target_size_bytes: ledger_cost.bucket_list_target_size_bytes,
-        write_fee_1kb_bucket_list_low: ledger_cost.write_fee1_kb_bucket_list_low,
-        write_fee_1kb_bucket_list_high: ledger_cost.write_fee1_kb_bucket_list_high,
-        bucket_list_write_fee_growth_factor: ledger_cost.bucket_list_write_fee_growth_factor,
-    };
-
-    let write_fee_per_1kb =
-        compute_write_fee_per_1kb(bucket_list_size as i64, &write_fee_configuration);
-
-    let fee_configuration = FeeConfiguration {
-        fee_per_instruction_increment: compute.fee_rate_per_instructions_increment,
-        fee_per_read_entry: ledger_cost.fee_read_ledger_entry,
-        fee_per_write_entry: ledger_cost.fee_write_ledger_entry,
-        fee_per_read_1kb: ledger_cost.fee_read1_kb,
-        fee_per_write_1kb: write_fee_per_1kb,
-        fee_per_historical_1kb: historical_data.fee_historical1_kb,
-        fee_per_contract_event_1kb: events.fee_contract_events1_kb,
-        fee_per_transaction_size_1kb: bandwidth.fee_tx_size1_kb,
-    };
-    let rent_fee_configuration = RentFeeConfiguration {
-        fee_per_write_1kb: write_fee_per_1kb,
-        fee_per_write_entry: ledger_cost.fee_write_ledger_entry,
-        persistent_rent_rate_denominator: state_archival.persistent_rent_rate_denominator,
-        temporary_rent_rate_denominator: state_archival.temp_rent_rate_denominator,
-    };
-    Ok((fee_configuration, rent_fee_configuration))
-}
-
-#[allow(clippy::cast_possible_truncation)]
-fn calculate_unmodified_ledger_entry_bytes(
-    ledger_entries: &[LedgerKey],
-    pre_storage: &LedgerStorage,
-    include_not_live: bool,
-) -> Result<u32> {
-    let mut res: usize = 0;
-    for lk in ledger_entries {
-        let entry_xdr = pre_storage
-            .get_xdr(lk, include_not_live)
-            .with_context(|| format!("cannot get xdr of ledger entry with key {lk:?}"))?;
-        let entry_size = entry_xdr.len();
-        res += entry_size;
-    }
-    Ok(res as u32)
-}
-
-fn calculate_contract_events_size_bytes(events: &[DiagnosticEvent]) -> Result<u32> {
-    let mut res: u32 = 0;
-    for e in events {
-        if e.event.type_ != ContractEventType::Contract
-            && e.event.type_ != ContractEventType::System
-        {
-            continue;
-        }
-        let event_xdr = e
-            .to_xdr(Limits::none())
-            .with_context(|| format!("cannot marshal event {e:?}"))?;
-        res += u32::try_from(event_xdr.len())?;
-    }
-    Ok(res)
-}
-
-fn storage_footprint_to_ledger_footprint(foot: &Footprint) -> Result<LedgerFootprint, xdr::Error> {
-    let mut read_only: Vec<LedgerKey> = Vec::with_capacity(foot.0.len());
-    let mut read_write: Vec<LedgerKey> = Vec::with_capacity(foot.0.len());
-    for (k, v) in &foot.0 {
-        match v {
-            AccessType::ReadOnly => read_only.push((**k).clone()),
-            AccessType::ReadWrite => read_write.push((**k).clone()),
-        }
-    }
-    Ok(LedgerFootprint {
-        read_only: read_only.try_into()?,
-        read_write: read_write.try_into()?,
-    })
-}
-
-fn finalize_transaction_data_and_min_fee(
-    pre_storage: &LedgerStorage,
-    transaction_resources: &TransactionResources,
-    soroban_resources: SorobanResources,
-    rent_changes: &Vec<LedgerEntryRentChange>,
-    current_ledger_seq: u32,
-    bucket_list_size: u64,
-) -> Result<(SorobanTransactionData, i64)> {
-    let (fee_configuration, rent_fee_configuration) =
-        get_fee_configurations(pre_storage, bucket_list_size)
-            .context("failed to obtain configuration settings from the network")?;
-    let (non_refundable_fee, refundable_fee) =
-        compute_transaction_resource_fee(transaction_resources, &fee_configuration);
-    let rent_fee = compute_rent_fee(rent_changes, &rent_fee_configuration, current_ledger_seq);
-    let resource_fee = refundable_fee + non_refundable_fee + rent_fee;
-    let transaction_data = SorobanTransactionData {
-        resources: soroban_resources,
-        resource_fee,
-        ext: ExtensionPoint::V0,
-    };
-    let res = (transaction_data, resource_fee);
-    Ok(res)
-}
-
-pub(crate) fn compute_extend_footprint_ttl_transaction_data_and_min_fee(
-    footprint: LedgerFootprint,
-    extend_to: u32,
-    ledger_storage: &LedgerStorage,
-    bucket_list_size: u64,
-    current_ledger_seq: u32,
-) -> Result<(SorobanTransactionData, i64)> {
-    let rent_changes = compute_extend_footprint_rent_changes(
-        &footprint,
-        ledger_storage,
-        extend_to,
-        current_ledger_seq,
-    )
-    .context("cannot compute extend rent changes")?;
-
-    let unmodified_entry_bytes = calculate_unmodified_ledger_entry_bytes(
-        footprint.read_only.as_slice(),
-        ledger_storage,
-        false,
-    )
-    .context("cannot calculate read_bytes resource")?;
-
-    let soroban_resources = SorobanResources {
-        footprint,
-        instructions: 0,
-        read_bytes: unmodified_entry_bytes,
-        write_bytes: 0,
-    };
-    let transaction_size_bytes = estimate_max_transaction_size_for_operation(
-        &OperationBody::ExtendFootprintTtl(ExtendFootprintTtlOp {
-            ext: ExtensionPoint::V0,
-            extend_to,
-        }),
-        &soroban_resources.footprint,
-    )
-    .context("cannot estimate maximum transaction size")?;
-    let transaction_resources = TransactionResources {
-        instructions: 0,
-        read_entries: u32::try_from(soroban_resources.footprint.read_only.as_vec().len())?,
-        write_entries: 0,
-        read_bytes: soroban_resources.read_bytes,
-        write_bytes: 0,
-        transaction_size_bytes,
-        contract_events_size_bytes: 0,
-    };
-    finalize_transaction_data_and_min_fee(
-        ledger_storage,
-        &transaction_resources,
-        soroban_resources,
-        &rent_changes,
-        current_ledger_seq,
-        bucket_list_size,
-    )
-}
-
-#[allow(clippy::cast_possible_truncation)]
-fn compute_extend_footprint_rent_changes(
-    footprint: &LedgerFootprint,
-    ledger_storage: &LedgerStorage,
-    extend_to: u32,
-    current_ledger_seq: u32,
-) -> Result<Vec<LedgerEntryRentChange>> {
-    let mut rent_changes: Vec<LedgerEntryRentChange> =
-        Vec::with_capacity(footprint.read_only.len());
-    for key in footprint.read_only.as_slice() {
-        let unmodified_entry_and_ttl = ledger_storage.get(key, false).with_context(|| {
-            format!("cannot find extend footprint ledger entry with key {key:?}")
-        })?;
-        let size = (key.to_xdr(Limits::none())?.len()
-            + unmodified_entry_and_ttl.0.to_xdr(Limits::none())?.len()) as u32;
-        let ttl_entry: Box<dyn TTLLedgerEntry> =
-            (&unmodified_entry_and_ttl)
-                .try_into()
-                .map_err(|e: String| {
-                    Error::msg(e.clone()).context("incorrect ledger entry type in footprint")
-                })?;
-        let new_live_until_ledger = current_ledger_seq + extend_to;
-        if new_live_until_ledger <= ttl_entry.live_until_ledger_seq() {
-            // The extend would be ineffective
-            continue;
-        }
-        let rent_change = LedgerEntryRentChange {
-            is_persistent: ttl_entry.durability() == Persistent,
-            old_size_bytes: size,
-            new_size_bytes: size,
-            old_live_until_ledger: ttl_entry.live_until_ledger_seq(),
-            new_live_until_ledger,
-        };
-        rent_changes.push(rent_change);
-    }
-    Ok(rent_changes)
-}
-
-pub(crate) fn compute_restore_footprint_transaction_data_and_min_fee(
-    footprint: LedgerFootprint,
-    ledger_storage: &LedgerStorage,
-    bucket_list_size: u64,
-    current_ledger_seq: u32,
-) -> Result<(SorobanTransactionData, i64)> {
-    let ConfigSettingEntry::StateArchival(state_archival) =
-        ledger_storage.get_configuration_setting(ConfigSettingId::StateArchival)?
-    else {
-        bail!("unexpected config setting entry for StateArchival key");
-    };
-    let rent_changes = compute_restore_footprint_rent_changes(
-        &footprint,
-        ledger_storage,
-        state_archival.min_persistent_ttl,
-        current_ledger_seq,
-    )
-    .context("cannot compute restore rent changes")?;
-
-    let write_bytes = calculate_unmodified_ledger_entry_bytes(
-        footprint.read_write.as_vec(),
-        ledger_storage,
-        true,
-    )
-    .context("cannot calculate write_bytes resource")?;
-    let soroban_resources = SorobanResources {
-        footprint,
-        instructions: 0,
-        read_bytes: write_bytes,
-        write_bytes,
-    };
-    let transaction_size_bytes = estimate_max_transaction_size_for_operation(
-        &OperationBody::RestoreFootprint(RestoreFootprintOp {
-            ext: ExtensionPoint::V0,
-        }),
-        &soroban_resources.footprint,
-    )
-    .context("cannot estimate maximum transaction size")?;
-    let transaction_resources = TransactionResources {
-        instructions: 0,
-        read_entries: 0,
-        write_entries: u32::try_from(soroban_resources.footprint.read_write.as_vec().len())?,
-        read_bytes: soroban_resources.read_bytes,
-        write_bytes: soroban_resources.write_bytes,
-        transaction_size_bytes,
-        contract_events_size_bytes: 0,
-    };
-    finalize_transaction_data_and_min_fee(
-        ledger_storage,
-        &transaction_resources,
-        soroban_resources,
-        &rent_changes,
-        current_ledger_seq,
-        bucket_list_size,
-    )
-}
-
-#[allow(clippy::cast_possible_truncation)]
-fn compute_restore_footprint_rent_changes(
-    footprint: &LedgerFootprint,
-    ledger_storage: &LedgerStorage,
-    min_persistent_ttl: u32,
-    current_ledger_seq: u32,
-) -> Result<Vec<LedgerEntryRentChange>> {
-    let mut rent_changes: Vec<LedgerEntryRentChange> =
-        Vec::with_capacity(footprint.read_write.len());
-    for key in footprint.read_write.as_vec() {
-        let unmodified_entry_and_ttl = ledger_storage.get(key, true).with_context(|| {
-            format!("cannot find restore footprint ledger entry with key {key:?}")
-        })?;
-        let size = (key.to_xdr(Limits::none())?.len()
-            + unmodified_entry_and_ttl.0.to_xdr(Limits::none())?.len()) as u32;
-        let ttl_entry: Box<dyn TTLLedgerEntry> =
-            (&unmodified_entry_and_ttl)
-                .try_into()
-                .map_err(|e: String| {
-                    Error::msg(e.clone()).context("incorrect ledger entry type in footprint")
-                })?;
-        ensure!(
-            ttl_entry.durability() == Persistent,
-            "non-persistent entry in footprint: key = {key:?}"
-        );
-        if ttl_entry.is_live(current_ledger_seq) {
-            // noop (the entry is alive)
-            continue;
-        }
-        let new_live_until_ledger =
-            get_restored_ledger_sequence(current_ledger_seq, min_persistent_ttl);
-        let rent_change = LedgerEntryRentChange {
-            is_persistent: true,
-            old_size_bytes: 0,
-            new_size_bytes: size,
-            old_live_until_ledger: 0,
-            new_live_until_ledger,
-        };
-        rent_changes.push(rent_change);
-    }
-    Ok(rent_changes)
-}
diff --git a/soroban-simulation/src/ledger_storage.rs b/soroban-simulation/src/ledger_storage.rs
deleted file mode 100644
index 9efe5301..00000000
--- a/soroban-simulation/src/ledger_storage.rs
+++ /dev/null
@@ -1,212 +0,0 @@
-use super::state_ttl::{get_restored_ledger_sequence, TTLLedgerEntry};
-use soroban_env_host::storage::SnapshotSource;
-use soroban_env_host::xdr::ContractDataDurability::{Persistent, Temporary};
-use soroban_env_host::xdr::{
-    ConfigSettingEntry, ConfigSettingId, Error as XdrError, LedgerEntry, LedgerEntryData,
-    LedgerKey, LedgerKeyConfigSetting, Limits, ScError, ScErrorCode, WriteXdr,
-};
-use soroban_env_host::HostError;
-use std::cell::RefCell;
-use std::collections::HashSet;
-use std::convert::TryInto;
-use std::ffi::NulError;
-use std::rc::Rc;
-use std::str::Utf8Error;
-
-#[derive(thiserror::Error, Debug)]
-pub enum Error {
-    #[error("not found")]
-    NotFound,
-    #[error("entry is not live")]
-    NotLive,
-    #[error("xdr processing error: {0}")]
-    Xdr(#[from] XdrError),
-    #[error("nul error: {0}")]
-    NulError(#[from] NulError),
-    #[error("utf8 error: {0}")]
-    Utf8Error(#[from] Utf8Error),
-    #[error("unexpected config ledger entry for setting_id {setting_id}")]
-    UnexpectedConfigLedgerEntry { setting_id: String },
-    #[error("unexpected ledger entry type ({ledger_entry_type}) for ttl ledger key")]
-    UnexpectedLedgerEntryTypeForTtlKey { ledger_entry_type: String },
-}
-
-impl From<Error> for HostError {
-    fn from(value: Error) -> Self {
-        match value {
-            Error::NotFound | Error::NotLive => ScError::Storage(ScErrorCode::MissingValue).into(),
-            Error::Xdr(_) => ScError::Value(ScErrorCode::InvalidInput).into(),
-            _ => ScError::Context(ScErrorCode::InternalError).into(),
-        }
-    }
-}
-
-struct EntryRestoreTracker {
-    min_persistent_ttl: u32,
-    // RefCell is needed to mutate the hashset inside SnapshotSource::get(), which is an immutable method
-    ledger_keys_requiring_restore: RefCell<HashSet<LedgerKey>>,
-}
-
-impl EntryRestoreTracker {
-    // Tracks ledger entries which need to be restored and returns its ttl as it was restored
-    pub(crate) fn track_and_restore(
-        &self,
-        current_ledger_sequence: u32,
-        key: &LedgerKey,
-        entry_and_ttl: &(LedgerEntry, Option<u32>),
-    ) -> Option<u32> {
-        let ttl_entry: Box<dyn TTLLedgerEntry> = match entry_and_ttl.try_into() {
-            Ok(e) => e,
-            Err(_) => {
-                // Nothing to track, the entry does not have a ttl
-                return None;
-            }
-        };
-        if ttl_entry.durability() != Persistent || ttl_entry.is_live(current_ledger_sequence) {
-            // Nothing to track, the entry isn't persistent (and thus not restorable) or
-            // it is alive
-            return Some(ttl_entry.live_until_ledger_seq());
-        }
-        self.ledger_keys_requiring_restore
-            .borrow_mut()
-            .insert(key.clone());
-        Some(get_restored_ledger_sequence(
-            current_ledger_sequence,
-            self.min_persistent_ttl,
-        ))
-    }
-}
-
-pub trait LedgerGetter {
-    fn get(
-        &self,
-        key: &LedgerKey,
-        include_not_live: bool,
-    ) -> Result<(LedgerEntry, Option<u32>), Error>;
-}
-
-pub struct LedgerStorage {
-    ledger_getter: Box<dyn LedgerGetter>,
-    current_ledger_sequence: u32,
-    restore_tracker: Option<EntryRestoreTracker>,
-}
-
-impl LedgerStorage {
-    pub fn new(ledger_getter: Box<dyn LedgerGetter>, current_ledger_sequence: u32) -> Self {
-        LedgerStorage {
-            ledger_getter,
-            current_ledger_sequence,
-            restore_tracker: None,
-        }
-    }
-
-    pub fn with_restore_tracking(
-        ledger_getter: Box<dyn LedgerGetter>,
-        current_ledger_sequence: u32,
-    ) -> Result<Self, Error> {
-        // First, we initialize it without the tracker, to get the minimum restore ledger from the network
-        let mut ledger_storage = LedgerStorage {
-            ledger_getter,
-            current_ledger_sequence,
-            restore_tracker: None,
-        };
-        let setting_id = ConfigSettingId::StateArchival;
-        let ConfigSettingEntry::StateArchival(state_archival) =
-            ledger_storage.get_configuration_setting(setting_id)?
-        else {
-            return Err(Error::UnexpectedConfigLedgerEntry {
-                setting_id: setting_id.name().to_string(),
-            });
-        };
-        // Now that we have the state archival config, we can build the tracker
-        ledger_storage.restore_tracker = Some(EntryRestoreTracker {
-            ledger_keys_requiring_restore: RefCell::new(HashSet::new()),
-            min_persistent_ttl: state_archival.min_persistent_ttl,
-        });
-        Ok(ledger_storage)
-    }
-
-    pub(crate) fn get_xdr(
-        &self,
-        key: &LedgerKey,
-        include_not_live: bool,
-    ) -> Result<Vec<u8>, Error> {
-        // TODO: this can be optimized since for entry types other than ContractCode/ContractData,
-        //       they don't need to be deserialized and serialized again
-        let (entry, _) = self.ledger_getter.get(key, include_not_live)?;
-        Ok(entry.to_xdr(Limits::none())?)
-    }
-
-    pub(crate) fn get_configuration_setting(
-        &self,
-        setting_id: ConfigSettingId,
-    ) -> Result<ConfigSettingEntry, Error> {
-        let key = LedgerKey::ConfigSetting(LedgerKeyConfigSetting {
-            config_setting_id: setting_id,
-        });
-        match self.ledger_getter.get(&key, false)? {
-            (
-                LedgerEntry {
-                    data: LedgerEntryData::ConfigSetting(cs),
-                    ..
-                },
-                _,
-            ) => Ok(cs),
-            _ => Err(Error::UnexpectedConfigLedgerEntry {
-                setting_id: setting_id.name().to_string(),
-            }),
-        }
-    }
-
-    pub(crate) fn get_ledger_keys_requiring_restore(&self) -> HashSet<LedgerKey> {
-        match self.restore_tracker {
-            Some(ref t) => t.ledger_keys_requiring_restore.borrow().clone(),
-            None => HashSet::new(),
-        }
-    }
-}
-
-impl LedgerGetter for LedgerStorage {
-    fn get(
-        &self,
-        key: &LedgerKey,
-        include_not_live: bool,
-    ) -> anyhow::Result<(LedgerEntry, Option<u32>), Error> {
-        self.ledger_getter.get(key, include_not_live)
-    }
-}
-
-impl SnapshotSource for LedgerStorage {
-    fn get(&self, key: &Rc<LedgerKey>) -> Result<(Rc<LedgerEntry>, Option<u32>), HostError> {
-        if let Some(ref tracker) = self.restore_tracker {
-            let mut entry_and_ttl = self.ledger_getter.get(key, true)?;
-            // Explicitly discard temporary ttl'ed entries
-            if let Ok(ttl_entry) = TryInto::<Box<dyn TTLLedgerEntry>>::try_into(&entry_and_ttl) {
-                if ttl_entry.durability() == Temporary
-                    && !ttl_entry.is_live(self.current_ledger_sequence)
-                {
-                    return Err(HostError::from(Error::NotLive));
-                }
-            }
-            // If the entry is not live, we modify the ttl to make it seem like it was restored
-            entry_and_ttl.1 =
-                tracker.track_and_restore(self.current_ledger_sequence, key, &entry_and_ttl);
-            return Ok((entry_and_ttl.0.into(), entry_and_ttl.1));
-        }
-        let entry_and_ttl = self
-            .ledger_getter
-            .get(key, false)
-            .map_err(HostError::from)?;
-        Ok((entry_and_ttl.0.into(), entry_and_ttl.1))
-    }
-
-    fn has(&self, key: &Rc<LedgerKey>) -> Result<bool, HostError> {
-        let result = <dyn SnapshotSource>::get(self, key);
-        if let Err(ref host_error) = result {
-            if host_error.error.is_code(ScErrorCode::MissingValue) {
-                return Ok(false);
-            }
-        }
-        result.map(|_| true)
-    }
-}
diff --git a/soroban-simulation/src/lib.rs b/soroban-simulation/src/lib.rs
index a3e587a7..860e75d6 100644
--- a/soroban-simulation/src/lib.rs
+++ b/soroban-simulation/src/lib.rs
@@ -1,321 +1,12 @@
-mod fees;
-pub mod ledger_storage;
-mod state_ttl;
-
-use anyhow::{anyhow, bail, Context, Result};
-pub use ledger_storage::LedgerStorage;
-use soroban_env_host::auth::RecordedAuthPayload;
-use soroban_env_host::budget::Budget;
-use soroban_env_host::events::Events;
-use soroban_env_host::storage::Storage;
-use soroban_env_host::xdr::{
-    AccountId, ConfigSettingEntry, ConfigSettingId, DiagnosticEvent, InvokeHostFunctionOp,
-    LedgerFootprint, LedgerKey, OperationBody, ScVal, SorobanAddressCredentials,
-    SorobanAuthorizationEntry, SorobanCredentials, SorobanTransactionData, VecM,
-};
-use soroban_env_host::{DiagnosticLevel, Host, LedgerInfo};
-use std::collections::HashSet;
-use std::convert::{TryFrom, TryInto};
-use std::iter::FromIterator;
-use std::rc::Rc;
-
-pub struct RestorePreamble {
-    pub transaction_data: SorobanTransactionData,
-    pub min_fee: i64,
-}
-
-#[derive(Default)]
-pub struct SimulationResult {
-    pub error: String,
-    pub auth: Vec<SorobanAuthorizationEntry>,
-    pub result: Option<ScVal>,
-    pub transaction_data: Option<SorobanTransactionData>,
-    pub min_fee: i64,
-    pub events: Vec<DiagnosticEvent>,
-    pub cpu_instructions: u64,
-    pub memory_bytes: u64,
-    pub restore_preamble: Option<RestorePreamble>,
-}
-
-pub struct ResourceConfig {
-    pub instruction_leeway: u64,
-}
-
-pub fn simulate_invoke_hf_op(
-    ledger_storage: LedgerStorage,
-    bucket_list_size: u64,
-    invoke_hf_op: InvokeHostFunctionOp,
-    source_account: AccountId,
-    ledger_info: LedgerInfo,
-    resource_config: ResourceConfig,
-    enable_debug: bool,
-) -> std::result::Result<SimulationResult, Box<dyn std::error::Error>> {
-    let ledger_storage_rc = Rc::new(ledger_storage);
-    let budget = get_budget_from_network_config_params(&ledger_storage_rc)
-        .context("cannot create budget")?;
-    let storage = Storage::with_recording_footprint(ledger_storage_rc.clone());
-    let host = Host::with_storage_and_budget(storage, budget);
-    host.set_source_account(source_account.clone())
-        .context("cannot set source account")?;
-    if enable_debug {
-        host.set_diagnostic_level(DiagnosticLevel::Debug)
-            .context("cannot set debug diagnostic level")?;
-    }
-    host.set_ledger_info(ledger_info.clone())
-        .context("cannot set ledger info")?;
-    host.set_base_prng_seed(rand::Rng::gen(&mut rand::thread_rng()))
-        .context("cannot set base prng seed")?;
-
-    // We make an assumption here:
-    // - if a transaction doesn't include any soroban authorization entries the client either
-    // doesn't know the authorization entries, or there are none. In either case it is best to
-    // record the authorization entries and return them to the client.
-    // - if a transaction *does* include soroban authorization entries, then the client *already*
-    // knows the needed entries, so we should try them in enforcing mode so that we can validate
-    // them, and return the correct fees and footprint.
-    let needs_auth_recording = invoke_hf_op.auth.is_empty();
-    if needs_auth_recording {
-        host.switch_to_recording_auth(true)
-            .context("cannot switch auth to recording mode")?;
-    } else {
-        host.set_authorization_entries(invoke_hf_op.auth.to_vec())
-            .context("cannot set authorization entries")?;
-    }
-
-    // Run the simulation.
-    let maybe_result = host
-        .invoke_function(invoke_hf_op.host_function.clone())
-        .context("host invocation failed");
-    let auths: VecM<SorobanAuthorizationEntry> = if needs_auth_recording {
-        let payloads = host.get_recorded_auth_payloads()?;
-        VecM::try_from(
-            payloads
-                .iter()
-                .map(recorded_auth_payload_to_xdr)
-                .collect::<Result<Vec<_>>>()?,
-        )?
-    } else {
-        invoke_hf_op.auth
-    };
-
-    let budget = host.budget_cloned();
-    // Recover, convert and return the storage footprint and other values to C.
-    let (storage, events) = host.try_finish().context("cannot finish host invocation")?;
-
-    let diagnostic_events = host_events_to_diagnostic_events(&events);
-    let result = match maybe_result {
-        Ok(r) => r,
-        // If the invocation failed, try to at least add the diagnostic events
-        Err(e) => {
-            return Ok(SimulationResult {
-                // See https://docs.rs/anyhow/latest/anyhow/struct.Error.html#display-representations
-                error: format!("{e:?}"),
-                events: diagnostic_events,
-                ..Default::default()
-            });
-        }
-    };
-
-    let invoke_host_function_with_auth = InvokeHostFunctionOp {
-        host_function: invoke_hf_op.host_function,
-        auth: auths.clone(),
-    };
-    let (transaction_data, min_fee) = fees::compute_host_function_transaction_data_and_min_fee(
-        &invoke_host_function_with_auth,
-        &ledger_storage_rc,
-        &storage,
-        &budget,
-        &resource_config,
-        &diagnostic_events,
-        &result,
-        bucket_list_size,
-        ledger_info.sequence_number,
-    )
-    .context("cannot compute resources and fees")?;
-
-    let restore_preamble = compute_restore_preamble(
-        ledger_storage_rc.get_ledger_keys_requiring_restore(),
-        &ledger_storage_rc,
-        bucket_list_size,
-        ledger_info.sequence_number,
-    )
-    .context("cannot compute restore preamble")?;
-
-    Ok(SimulationResult {
-        auth: auths.to_vec(),
-        result: Some(result),
-        transaction_data: Some(transaction_data),
-        min_fee,
-        events: diagnostic_events,
-        cpu_instructions: budget
-            .get_cpu_insns_consumed()
-            .context("cannot get cpu instructions")?,
-        memory_bytes: budget
-            .get_mem_bytes_consumed()
-            .context("cannot get consumed memory")?,
-        restore_preamble,
-        ..Default::default()
-    })
-}
-
-fn recorded_auth_payload_to_xdr(
-    payload: &RecordedAuthPayload,
-) -> Result<SorobanAuthorizationEntry> {
-    let result = match (payload.address.clone(), payload.nonce) {
-        (Some(address), Some(nonce)) => SorobanAuthorizationEntry {
-            credentials: SorobanCredentials::Address(SorobanAddressCredentials {
-                address,
-                nonce,
-                // signature is left empty. This is where the client will put their signatures when
-                // submitting the transaction.
-                signature_expiration_ledger: 0,
-                signature: ScVal::Void,
-            }),
-            root_invocation: payload.invocation.clone(),
-        },
-        (None, None) => SorobanAuthorizationEntry {
-            credentials: SorobanCredentials::SourceAccount,
-            root_invocation: payload.invocation.clone(),
-        },
-        // the address and the nonce can't be present independently
-        (a,n) =>
-            bail!("recorded_auth_payload_to_xdr: address and nonce present independently (address: {:?}, nonce: {:?})", a, n),
-    };
-    Ok(result)
-}
-
-fn compute_restore_preamble(
-    entries: HashSet<LedgerKey>,
-    ledger_storage: &LedgerStorage,
-    bucket_list_size: u64,
-    current_ledger_seq: u32,
-) -> Result<Option<RestorePreamble>> {
-    if entries.is_empty() {
-        return Ok(None);
-    }
-    let read_write_vec: Vec<LedgerKey> = Vec::from_iter(entries);
-    let restore_footprint = LedgerFootprint {
-        read_only: VecM::default(),
-        read_write: read_write_vec.try_into()?,
-    };
-    let (transaction_data, min_fee) = fees::compute_restore_footprint_transaction_data_and_min_fee(
-        restore_footprint,
-        ledger_storage,
-        bucket_list_size,
-        current_ledger_seq,
-    )?;
-    Ok(Some(RestorePreamble {
-        transaction_data,
-        min_fee,
-    }))
-}
-
-fn host_events_to_diagnostic_events(events: &Events) -> Vec<DiagnosticEvent> {
-    let mut res: Vec<DiagnosticEvent> = Vec::with_capacity(events.0.len());
-    for e in &events.0 {
-        let diagnostic_event = DiagnosticEvent {
-            in_successful_contract_call: !e.failed_call,
-            event: e.event.clone(),
-        };
-        res.push(diagnostic_event);
-    }
-    res
-}
-#[allow(clippy::cast_sign_loss)]
-fn get_budget_from_network_config_params(ledger_storage: &LedgerStorage) -> Result<Budget> {
-    let ConfigSettingEntry::ContractComputeV0(compute) =
-        ledger_storage.get_configuration_setting(ConfigSettingId::ContractComputeV0)?
-    else {
-        bail!("unexpected config setting entry for ComputeV0 key");
-    };
-
-    let ConfigSettingEntry::ContractCostParamsCpuInstructions(cost_params_cpu) = ledger_storage
-        .get_configuration_setting(ConfigSettingId::ContractCostParamsCpuInstructions)?
-    else {
-        bail!("unexpected config setting entry for CostParamsCpuInstructions key");
-    };
-
-    let ConfigSettingEntry::ContractCostParamsMemoryBytes(cost_params_memory) =
-        ledger_storage.get_configuration_setting(ConfigSettingId::ContractCostParamsMemoryBytes)?
-    else {
-        bail!("unexpected config setting entry for CostParamsMemoryBytes key");
-    };
-    let budget = Budget::try_from_configs(
-        compute.tx_max_instructions as u64,
-        u64::from(compute.tx_memory_limit),
-        cost_params_cpu,
-        cost_params_memory,
-    )
-    .context("cannot create budget from network configuration")?;
-    Ok(budget)
-}
-
-pub fn simulate_footprint_ttl_op(
-    ledger_storage: &LedgerStorage,
-    bucket_list_size: u64,
-    op_body: OperationBody,
-    footprint: LedgerFootprint,
-    current_ledger_seq: u32,
-) -> std::result::Result<SimulationResult, Box<dyn std::error::Error>> {
-    let a = match op_body {
-        OperationBody::ExtendFootprintTtl(op) => simulate_extend_footprint_ttl(
-            footprint,
-            op.extend_to,
-            ledger_storage,
-            bucket_list_size,
-            current_ledger_seq,
-        ),
-        OperationBody::RestoreFootprint(_) => simulate_restore_footprint(
-            footprint,
-            ledger_storage,
-            bucket_list_size,
-            current_ledger_seq,
-        ),
-        op => Err(anyhow!(
-            "simulate_footprint_ttl_op(): unsupported operation type {}",
-            op.name()
-        )),
-    };
-    Ok(a?)
-}
-
-fn simulate_extend_footprint_ttl(
-    footprint: LedgerFootprint,
-    extend_to: u32,
-    ledger_storage: &LedgerStorage,
-    bucket_list_size: u64,
-    current_ledger_seq: u32,
-) -> Result<SimulationResult> {
-    let (transaction_data, min_fee) =
-        fees::compute_extend_footprint_ttl_transaction_data_and_min_fee(
-            footprint,
-            extend_to,
-            ledger_storage,
-            bucket_list_size,
-            current_ledger_seq,
-        )?;
-    Ok(SimulationResult {
-        transaction_data: Some(transaction_data),
-        min_fee,
-        ..Default::default()
-    })
-}
-
-fn simulate_restore_footprint(
-    footprint: LedgerFootprint,
-    ledger_storage: &LedgerStorage,
-    bucket_list_size: u64,
-    current_ledger_seq: u32,
-) -> Result<SimulationResult> {
-    let (transaction_data, min_fee) = fees::compute_restore_footprint_transaction_data_and_min_fee(
-        footprint,
-        ledger_storage,
-        bucket_list_size,
-        current_ledger_seq,
-    )?;
-    Ok(SimulationResult {
-        transaction_data: Some(transaction_data),
-        min_fee,
-        ..Default::default()
-    })
-}
+pub mod simulation;
+pub use network_config::NetworkConfig;
+pub use snapshot_source::AutoRestoringSnapshotSource;
+pub use snapshot_source::SnapshotSourceWithArchive;
+mod network_config;
+mod snapshot_source;
+
+mod resources;
+#[cfg(test)]
+mod test;
+#[cfg(any(test, feature = "testutils"))]
+pub mod testutils;
diff --git a/soroban-simulation/src/network_config.rs b/soroban-simulation/src/network_config.rs
new file mode 100644
index 00000000..4d570990
--- /dev/null
+++ b/soroban-simulation/src/network_config.rs
@@ -0,0 +1,152 @@
+use anyhow::{anyhow, bail, Context, Result};
+use soroban_env_host::budget::Budget;
+use soroban_env_host::fees::{
+    compute_write_fee_per_1kb, FeeConfiguration, RentFeeConfiguration, WriteFeeConfiguration,
+};
+use soroban_env_host::storage::SnapshotSource;
+use soroban_env_host::xdr::{
+    ConfigSettingEntry, ConfigSettingId, ContractCostParams, LedgerEntry, LedgerEntryData,
+    LedgerKey, LedgerKeyConfigSetting,
+};
+use soroban_env_host::LedgerInfo;
+use std::rc::Rc;
+
+/// Network configuration necessary for Soroban operation simulations.
+///
+/// This should normally be loaded from the ledger.
+#[derive(Debug, Default, PartialEq, Eq)]
+pub struct NetworkConfig {
+    pub fee_configuration: FeeConfiguration,
+    pub rent_fee_configuration: RentFeeConfiguration,
+    pub tx_max_instructions: i64,
+    pub tx_memory_limit: u32,
+    pub cpu_cost_params: ContractCostParams,
+    pub memory_cost_params: ContractCostParams,
+    // Configuration to use in `LedgerInfo`.
+    pub min_temp_entry_ttl: u32,
+    pub min_persistent_entry_ttl: u32,
+    pub max_entry_ttl: u32,
+}
+
+fn load_configuration_setting(
+    snapshot: &impl SnapshotSource,
+    setting_id: ConfigSettingId,
+) -> Result<ConfigSettingEntry> {
+    let key = Rc::new(LedgerKey::ConfigSetting(LedgerKeyConfigSetting {
+        config_setting_id: setting_id,
+    }));
+    let (entry, _) = snapshot
+        .get(&key)?
+        .ok_or_else(|| anyhow!("setting {setting_id:?} is not present in the snapshot"))?;
+    if let LedgerEntry {
+        data: LedgerEntryData::ConfigSetting(cs),
+        ..
+    } = &*entry
+    {
+        Ok(cs.clone())
+    } else {
+        bail!("encountered unexpected config setting ledger entry {entry:#?}");
+    }
+}
+
+macro_rules! load_setting {
+    ($snapshot:ident, $enum_variant:ident) => {
+        match load_configuration_setting($snapshot, ConfigSettingId::$enum_variant)? {
+            ConfigSettingEntry::$enum_variant(setting) => setting,
+            _ => bail!(
+                "loaded unexpected config setting entry for {:?} key",
+                stringify!($enum_variant)
+            ),
+        }
+    };
+}
+
+impl NetworkConfig {
+    /// Loads configuration from the ledger snapshot.
+    ///
+    /// This may only fail in case when provided snapshot doesn't contain
+    /// all the necessary entries or when these entries are mis-configured.
+    pub fn load_from_snapshot(snapshot: &impl SnapshotSource) -> Result<Self> {
+        let compute = load_setting!(snapshot, ContractComputeV0);
+        let ledger_cost = load_setting!(snapshot, ContractLedgerCostV0);
+        let historical_data = load_setting!(snapshot, ContractHistoricalDataV0);
+        let events = load_setting!(snapshot, ContractEventsV0);
+        let bandwidth = load_setting!(snapshot, ContractBandwidthV0);
+        let state_archival = load_setting!(snapshot, StateArchival);
+        let cpu_cost_params = load_setting!(snapshot, ContractCostParamsCpuInstructions);
+        let memory_cost_params = load_setting!(snapshot, ContractCostParamsMemoryBytes);
+
+        let bucket_list_size_window = load_setting!(snapshot, BucketlistSizeWindow);
+        if bucket_list_size_window.is_empty() {
+            bail!("bucket list size window can not be empty");
+        }
+        let mut total_bucket_list_size: u128 = 0;
+        for s in bucket_list_size_window.iter() {
+            total_bucket_list_size = total_bucket_list_size.saturating_add(*s as u128);
+        }
+        let bucket_list_size: i64 = (total_bucket_list_size
+            / bucket_list_size_window.len() as u128)
+            .try_into()
+            .context("bucket list size is too large, the ledger is mis-configured")?;
+
+        let write_fee_configuration = WriteFeeConfiguration {
+            bucket_list_target_size_bytes: ledger_cost.bucket_list_target_size_bytes,
+            write_fee_1kb_bucket_list_low: ledger_cost.write_fee1_kb_bucket_list_low,
+            write_fee_1kb_bucket_list_high: ledger_cost.write_fee1_kb_bucket_list_high,
+            bucket_list_write_fee_growth_factor: ledger_cost.bucket_list_write_fee_growth_factor,
+        };
+
+        let write_fee_per_1kb =
+            compute_write_fee_per_1kb(bucket_list_size, &write_fee_configuration);
+
+        let fee_configuration = FeeConfiguration {
+            fee_per_instruction_increment: compute.fee_rate_per_instructions_increment,
+            fee_per_read_entry: ledger_cost.fee_read_ledger_entry,
+            fee_per_write_entry: ledger_cost.fee_write_ledger_entry,
+            fee_per_read_1kb: ledger_cost.fee_read1_kb,
+            fee_per_write_1kb: write_fee_per_1kb,
+            fee_per_historical_1kb: historical_data.fee_historical1_kb,
+            fee_per_contract_event_1kb: events.fee_contract_events1_kb,
+            fee_per_transaction_size_1kb: bandwidth.fee_tx_size1_kb,
+        };
+        let rent_fee_configuration = RentFeeConfiguration {
+            fee_per_write_1kb: write_fee_per_1kb,
+            fee_per_write_entry: ledger_cost.fee_write_ledger_entry,
+            persistent_rent_rate_denominator: state_archival.persistent_rent_rate_denominator,
+            temporary_rent_rate_denominator: state_archival.temp_rent_rate_denominator,
+        };
+
+        Ok(Self {
+            fee_configuration,
+            rent_fee_configuration,
+            cpu_cost_params,
+            memory_cost_params,
+            min_temp_entry_ttl: state_archival.min_temporary_ttl,
+            min_persistent_entry_ttl: state_archival.min_persistent_ttl,
+            tx_max_instructions: compute.tx_max_instructions,
+            tx_memory_limit: compute.tx_memory_limit,
+            max_entry_ttl: state_archival.max_entry_ttl,
+        })
+    }
+
+    /// Fills the `ledger_info` fields that are loaded from the config.
+    ///
+    /// This should normally be used to populate TTL-related fields in
+    /// `LedgerInfo`, so that config loading logic can be encapsulated
+    /// just in `NetworkConfig`.
+    pub fn fill_config_fields_in_ledger_info(self, ledger_info: &mut LedgerInfo) {
+        ledger_info.min_persistent_entry_ttl = self.min_persistent_entry_ttl;
+        ledger_info.min_temp_entry_ttl = self.min_temp_entry_ttl;
+        ledger_info.max_entry_ttl = self.max_entry_ttl;
+    }
+
+    pub(crate) fn create_budget(&self) -> Result<Budget> {
+        Budget::try_from_configs(
+            self.tx_max_instructions as u64,
+            self.tx_memory_limit as u64,
+            self.cpu_cost_params.clone(),
+            self.memory_cost_params.clone(),
+        )
+        .context("cannot create budget from network configuration")
+    }
+}
diff --git a/soroban-simulation/src/resources.rs b/soroban-simulation/src/resources.rs
new file mode 100644
index 00000000..77bd3b07
--- /dev/null
+++ b/soroban-simulation/src/resources.rs
@@ -0,0 +1,311 @@
+use super::snapshot_source::SnapshotSourceWithArchive;
+use crate::network_config::NetworkConfig;
+use crate::simulation::{SimulationAdjustmentConfig, SimulationAdjustmentFactor};
+use anyhow::{anyhow, ensure, Context, Result};
+
+use soroban_env_host::{
+    e2e_invoke::{extract_rent_changes, LedgerEntryChange},
+    fees::{
+        compute_rent_fee, compute_transaction_resource_fee, LedgerEntryRentChange,
+        TransactionResources,
+    },
+    ledger_info::get_key_durability,
+    storage::SnapshotSource,
+    xdr::{
+        BytesM, ContractDataDurability, DecoratedSignature, Duration, ExtensionPoint, Hash,
+        LedgerBounds, LedgerFootprint, LedgerKey, Limits, Memo, MuxedAccount, MuxedAccountMed25519,
+        Operation, OperationBody, Preconditions, PreconditionsV2, ReadXdr, SequenceNumber,
+        Signature, SignatureHint, SignerKey, SignerKeyEd25519SignedPayload, SorobanResources,
+        SorobanTransactionData, TimeBounds, TimePoint, Transaction, TransactionExt,
+        TransactionV1Envelope, Uint256, WriteXdr,
+    },
+    LedgerInfo, DEFAULT_XDR_RW_LIMITS,
+};
+use std::convert::TryInto;
+use std::rc::Rc;
+
+impl SimulationAdjustmentConfig {
+    fn adjust_resources(&self, resources: &mut SorobanResources) {
+        resources.instructions = self.instructions.adjust_u32(resources.instructions);
+        resources.read_bytes = self.read_bytes.adjust_u32(resources.read_bytes);
+        resources.write_bytes = self.write_bytes.adjust_u32(resources.write_bytes);
+    }
+}
+
+impl SimulationAdjustmentFactor {
+    fn adjust_u32(&self, value: u32) -> u32 {
+        // `0` typically means that resource hasn't been used at all,
+        // so adjusting it with an additive factor would likely waste
+        // resources unnecessarily.
+        if value == 0 {
+            return 0;
+        }
+        value.saturating_add(self.additive_factor).max(
+            ((value as f64) * self.multiplicative_factor)
+                .clamp(0.0, u32::MAX as f64)
+                .floor() as u32,
+        )
+    }
+
+    fn adjust_i64(&self, value: i64) -> i64 {
+        // `0` typically means that resource hasn't been used at all,
+        // so adjusting it with an additive factor would likely waste
+        // resources unnecessarily.
+        if value == 0 {
+            return 0;
+        }
+        value.saturating_add(self.additive_factor as i64).max(
+            ((value as f64) * self.multiplicative_factor)
+                .clamp(0.0, i64::MAX as f64)
+                .floor() as i64,
+        )
+    }
+}
+
+pub(crate) fn compute_resource_fee(
+    network_config: &NetworkConfig,
+    ledger_info: &LedgerInfo,
+    resources: &TransactionResources,
+    rent_changes: &[LedgerEntryRentChange],
+    adjustment_config: &SimulationAdjustmentConfig,
+) -> i64 {
+    let (non_refundable_fee, refundable_fee) =
+        compute_transaction_resource_fee(resources, &network_config.fee_configuration);
+    let rent_fee = compute_rent_fee(
+        rent_changes,
+        &network_config.rent_fee_configuration,
+        ledger_info.sequence_number,
+    );
+    let refundable_fee = adjustment_config
+        .refundable_fee
+        .adjust_i64(refundable_fee.saturating_add(rent_fee));
+    non_refundable_fee.saturating_add(refundable_fee)
+}
+
+pub(crate) fn compute_adjusted_transaction_resources(
+    operation: OperationBody,
+    simulated_operation_resources: &mut SorobanResources,
+    adjustment_config: &SimulationAdjustmentConfig,
+    contract_events_and_return_value_size: u32,
+) -> Result<TransactionResources> {
+    adjustment_config.adjust_resources(simulated_operation_resources);
+    Ok(TransactionResources {
+        instructions: simulated_operation_resources.instructions,
+        read_entries: (simulated_operation_resources.footprint.read_only.len()
+            + simulated_operation_resources.footprint.read_write.len())
+            as u32,
+        write_entries: simulated_operation_resources.footprint.read_write.len() as u32,
+        read_bytes: simulated_operation_resources.read_bytes,
+        write_bytes: simulated_operation_resources.write_bytes,
+        transaction_size_bytes: adjustment_config.tx_size.adjust_u32(
+            estimate_max_transaction_size_for_operation(operation, &simulated_operation_resources)
+                .context("could not compute the maximum transaction size for operation")?,
+        ),
+        contract_events_size_bytes: contract_events_and_return_value_size,
+    })
+}
+
+pub(crate) fn simulate_invoke_host_function_op_resources(
+    ledger_changes: &[LedgerEntryChange],
+    simulated_instructions: u32,
+) -> Result<(SorobanResources, Vec<LedgerEntryRentChange>)> {
+    let mut read_bytes = 0;
+    let mut write_bytes = 0;
+    let mut read_only_footprint = vec![];
+    let mut read_write_footprint = vec![];
+    for ledger_change in ledger_changes {
+        read_bytes += ledger_change.old_entry_size_bytes;
+        // It should be safe to decode values returned encoded by host without limits.
+        let key = LedgerKey::from_xdr(&ledger_change.encoded_key, Limits::none())?;
+        if ledger_change.read_only {
+            read_only_footprint.push(key);
+        } else {
+            read_write_footprint.push(key);
+            if let Some(new_value) = &ledger_change.encoded_new_value {
+                write_bytes += new_value.len() as u32;
+            }
+        }
+    }
+    read_only_footprint.sort();
+    read_write_footprint.sort();
+    let footprint = LedgerFootprint {
+        read_only: read_only_footprint.try_into()?,
+        read_write: read_write_footprint.try_into()?,
+    };
+    let resources = SorobanResources {
+        footprint,
+        instructions: simulated_instructions,
+        read_bytes,
+        write_bytes,
+    };
+    let rent_changes = extract_rent_changes(ledger_changes);
+    Ok((resources, rent_changes))
+}
+
+pub(crate) fn simulate_extend_ttl_op_resources(
+    keys_to_extend: &[LedgerKey],
+    snapshot: &impl SnapshotSource,
+    current_ledger_seq: u32,
+    extend_to: u32,
+) -> Result<(SorobanResources, Vec<LedgerEntryRentChange>)> {
+    let mut read_bytes = 0;
+    let mut rent_changes = Vec::<LedgerEntryRentChange>::with_capacity(keys_to_extend.len());
+    let mut extended_keys = Vec::<LedgerKey>::with_capacity(keys_to_extend.len());
+
+    let new_live_until_ledger = current_ledger_seq + extend_to;
+    for key in keys_to_extend {
+        let durability = get_key_durability(key).ok_or_else(|| anyhow!("Can't extend TTL for ledger entry with key `{:?}`. Only entries with TTL (contract data or code entries) can have it extended", key))?;
+        let entry_with_live_until = snapshot.get(&Rc::new(key.clone()))?;
+        let Some((entry, live_until)) = entry_with_live_until else {
+            continue;
+        };
+        let entry_size: u32 = entry.to_xdr(DEFAULT_XDR_RW_LIMITS)?.len().try_into()?;
+        let current_live_until_ledger = live_until.ok_or_else(|| {
+            anyhow!("Internal error: missing TTL for ledger key that must have TTL: `{key:?}`")
+        })?;
+        if new_live_until_ledger <= current_live_until_ledger {
+            continue;
+        }
+        extended_keys.push(key.clone());
+        read_bytes += entry_size;
+        rent_changes.push(LedgerEntryRentChange {
+            is_persistent: durability == ContractDataDurability::Persistent,
+            old_size_bytes: entry_size,
+            new_size_bytes: entry_size,
+            old_live_until_ledger: current_live_until_ledger,
+            new_live_until_ledger,
+        });
+    }
+    extended_keys.sort();
+    let resources = SorobanResources {
+        footprint: LedgerFootprint {
+            read_only: extended_keys.try_into()?,
+            read_write: Default::default(),
+        },
+        instructions: 0,
+        read_bytes,
+        write_bytes: 0,
+    };
+    Ok((resources, rent_changes))
+}
+
+pub(crate) fn simulate_restore_op_resources(
+    keys_to_restore: &[LedgerKey],
+    snapshot_source: &impl SnapshotSourceWithArchive,
+    ledger_info: &LedgerInfo,
+) -> Result<(SorobanResources, Vec<LedgerEntryRentChange>)> {
+    let restored_live_until_ledger = ledger_info
+        .min_live_until_ledger_checked(ContractDataDurability::Persistent)
+        .ok_or_else(|| {
+            anyhow!("minimum persistent live until ledger overflows - ledger info is misconfigured")
+        })?;
+    let mut restored_bytes = 0_u32;
+    let mut rent_changes: Vec<LedgerEntryRentChange> = Vec::with_capacity(keys_to_restore.len());
+    let mut restored_keys = Vec::<LedgerKey>::with_capacity(keys_to_restore.len());
+
+    for key in keys_to_restore {
+        let durability = get_key_durability(key);
+        ensure!(
+            durability == Some(ContractDataDurability::Persistent),
+            "Can't restore a ledger entry with key: {key:?}. Only persistent ledger entries with TTL can be restored."
+        );
+        let entry_with_live_until = snapshot_source
+            .get_including_archived(&Rc::new(key.clone()))?
+            .ok_or_else(|| anyhow!("Missing entry to restore for key {key:?}"))?;
+        let (entry, live_until) = entry_with_live_until;
+
+        let current_live_until_ledger = live_until.ok_or_else(|| {
+            anyhow!("Internal error: missing TTL for ledger key that must have TTL: `{key:?}`")
+        })?;
+
+        if current_live_until_ledger >= ledger_info.sequence_number {
+            continue;
+        }
+        restored_keys.push(key.clone());
+
+        let entry_size: u32 = entry.to_xdr(DEFAULT_XDR_RW_LIMITS)?.len().try_into()?;
+        restored_bytes = restored_bytes.saturating_add(entry_size);
+        rent_changes.push(LedgerEntryRentChange {
+            is_persistent: true,
+            old_size_bytes: 0,
+            new_size_bytes: entry_size,
+            old_live_until_ledger: 0,
+            new_live_until_ledger: restored_live_until_ledger,
+        });
+    }
+    restored_keys.sort();
+    let resources = SorobanResources {
+        footprint: LedgerFootprint {
+            read_only: Default::default(),
+            read_write: restored_keys.try_into()?,
+        },
+        instructions: 0,
+        read_bytes: restored_bytes,
+        write_bytes: restored_bytes,
+    };
+    Ok((resources, rent_changes))
+}
+
+fn estimate_max_transaction_size_for_operation(
+    operation: OperationBody,
+    resources: &SorobanResources,
+) -> Result<u32> {
+    let source = MuxedAccount::MuxedEd25519(MuxedAccountMed25519 {
+        id: 0,
+        ed25519: Uint256([0; 32]),
+    });
+    let bytes64: BytesM<64> = vec![0; 64].try_into()?;
+    let signatures: Vec<DecoratedSignature> = vec![
+        DecoratedSignature {
+            hint: SignatureHint([0; 4]),
+            signature: Signature(bytes64.clone()),
+        };
+        20
+    ];
+    let signer_key = SignerKey::Ed25519SignedPayload(SignerKeyEd25519SignedPayload {
+        ed25519: Uint256([0; 32]),
+        payload: bytes64.clone(),
+    });
+    let envelope = TransactionV1Envelope {
+        tx: Transaction {
+            source_account: source.clone(),
+            fee: 0,
+            seq_num: SequenceNumber(0),
+            cond: Preconditions::V2(PreconditionsV2 {
+                time_bounds: Some(TimeBounds {
+                    min_time: TimePoint(0),
+                    max_time: TimePoint(0),
+                }),
+                ledger_bounds: Some(LedgerBounds {
+                    min_ledger: 0,
+                    max_ledger: 0,
+                }),
+                min_seq_num: Some(SequenceNumber(0)),
+                min_seq_age: Duration(0),
+                min_seq_ledger_gap: 0,
+                extra_signers: vec![signer_key.clone(), signer_key].try_into()?,
+            }),
+            memo: Memo::Hash(Hash([0; 32])),
+            operations: vec![Operation {
+                source_account: Some(source),
+                body: operation,
+            }]
+            .try_into()?,
+            ext: TransactionExt::V1(SorobanTransactionData {
+                resources: SorobanResources {
+                    footprint: resources.footprint.clone(),
+                    instructions: 0,
+                    read_bytes: 0,
+                    write_bytes: 0,
+                },
+                resource_fee: 0,
+                ext: ExtensionPoint::V0,
+            }),
+        },
+        signatures: signatures.try_into()?,
+    };
+
+    let envelope_xdr = envelope.to_xdr(DEFAULT_XDR_RW_LIMITS)?;
+    let envelope_size = envelope_xdr.len();
+    Ok(envelope_size.try_into()?)
+}
diff --git a/soroban-simulation/src/simulation.rs b/soroban-simulation/src/simulation.rs
new file mode 100644
index 00000000..3298a8df
--- /dev/null
+++ b/soroban-simulation/src/simulation.rs
@@ -0,0 +1,360 @@
+use crate::network_config::NetworkConfig;
+use crate::resources::{
+    compute_adjusted_transaction_resources, compute_resource_fee, simulate_extend_ttl_op_resources,
+    simulate_invoke_host_function_op_resources, simulate_restore_op_resources,
+};
+use crate::snapshot_source::SnapshotSourceWithArchive;
+use anyhow::Result;
+use soroban_env_host::{
+    e2e_invoke::invoke_host_function_in_recording_mode,
+    e2e_invoke::LedgerEntryChange,
+    storage::SnapshotSource,
+    xdr::{
+        AccountId, ContractEvent, DiagnosticEvent, HostFunction, InvokeHostFunctionOp, LedgerKey,
+        OperationBody, ScVal, SorobanAuthorizationEntry, SorobanResources, SorobanTransactionData,
+    },
+    xdr::{ExtendFootprintTtlOp, ExtensionPoint, LedgerEntry, ReadXdr, RestoreFootprintOp},
+    HostError, LedgerInfo, DEFAULT_XDR_RW_LIMITS,
+};
+use std::rc::Rc;
+
+/// Configures the adjustment of a simulated value (e.g. resource or fee).
+/// The value is adjusted to be
+/// `max(value * multiplicative_factor, value + additive_factor)`
+pub struct SimulationAdjustmentFactor {
+    pub multiplicative_factor: f64,
+    pub additive_factor: u32,
+}
+
+/// Configuration for adjusting the resources and fees for a simulated
+/// transaction.
+pub struct SimulationAdjustmentConfig {
+    pub instructions: SimulationAdjustmentFactor,
+    pub read_bytes: SimulationAdjustmentFactor,
+    pub write_bytes: SimulationAdjustmentFactor,
+    pub tx_size: SimulationAdjustmentFactor,
+    pub refundable_fee: SimulationAdjustmentFactor,
+}
+
+/// Represents the state of a `LedgerEntry` before and after the
+/// transaction execution.
+/// `None` represents that entry was not present or removed.
+#[derive(Eq, PartialEq, Debug)]
+pub struct LedgerEntryDiff {
+    pub state_before: Option<LedgerEntry>,
+    pub state_after: Option<LedgerEntry>,
+}
+
+/// Result of simulating `InvokeHostFunctionOp` operation.
+#[derive(Debug)]
+pub struct InvokeHostFunctionSimulationResult {
+    /// Result value of the invoked function or error returned for invocation.
+    pub invoke_result: std::result::Result<ScVal, HostError>,
+    /// Authorization data, either passed through from the call (when provided),
+    /// or recorded during the invocation.
+    pub auth: Vec<SorobanAuthorizationEntry>,
+    /// All the events that contracts emitted during invocation.
+    /// Empty for failed invocations.
+    pub contract_events: Vec<ContractEvent>,
+    /// Diagnostic events recorded during simulation.
+    /// This is populated when diagnostics is enabled and even when the
+    /// invocation fails.
+    pub diagnostic_events: Vec<DiagnosticEvent>,
+    /// Soroban transaction extension containing simulated resources and
+    /// the estimated resource fee.
+    /// `None` for failed invocations.
+    pub transaction_data: Option<SorobanTransactionData>,
+    /// The number of CPU instructions metered during the simulation,
+    /// without any adjustments applied.
+    /// This is expected to not match `transaction_data` in case if
+    /// instructions are adjusted via `SimulationAdjustmentConfig`.
+    pub simulated_instructions: u32,
+    /// The number of memory bytes metered during the simulation,
+    /// without any adjustments applied.
+    pub simulated_memory: u32,
+    /// Differences for any RW entries that have been modified during
+    /// the transaction execution.
+    /// Empty for failed invocations.
+    pub modified_entries: Vec<LedgerEntryDiff>,
+}
+
+/// Result of simulating `ExtendFootprintTtlOp` operation.
+#[derive(Eq, PartialEq, Debug)]
+pub struct ExtendTtlOpSimulationResult {
+    /// Soroban transaction extension containing simulated resources and
+    /// the estimated resource fee.
+    pub transaction_data: SorobanTransactionData,
+}
+
+/// Result of simulating `RestoreFootprintOp` operation.
+#[derive(Eq, PartialEq, Debug)]
+pub struct RestoreOpSimulationResult {
+    /// Soroban transaction extension containing simulated resources and
+    /// the estimated resource fee.
+    pub transaction_data: SorobanTransactionData,
+}
+
+/// Simulates `InvokeHostFunctionOp` operation specified via its
+/// relevant payload parts.
+///
+/// The operation is defined by the host function itself (`host_fn`)
+/// and optionally signed `auth_entries`. In case if `auth_entries` are
+/// omitted, the simulation will use recording authorization mode and
+/// return non-signed recorded authorization entries.
+///
+/// The rest of parameters define the ledger state (`snapshot_source`,
+/// `network_config`, `ledger_info`), simulation adjustment
+/// configuration (`adjustment_config`), and transaction execution
+/// parameters (`source_account`, `base_prng_seed`).
+///
+/// `enable_diagnostics` enables recording of `diagnostic_events` in the
+/// response.
+///
+/// This function makes the best effort at returning non-Err result even
+/// for failed invocations. It should only fail if ledger is
+/// mis-configured (e.g. when computed fees cause overflows).
+#[allow(clippy::too_many_arguments)]
+pub fn simulate_invoke_host_function_op(
+    snapshot_source: Rc<dyn SnapshotSource>,
+    network_config: &NetworkConfig,
+    adjustment_config: &SimulationAdjustmentConfig,
+    ledger_info: &LedgerInfo,
+    host_fn: HostFunction,
+    auth_entries: Option<Vec<SorobanAuthorizationEntry>>,
+    source_account: &AccountId,
+    base_prng_seed: [u8; 32],
+    enable_diagnostics: bool,
+) -> Result<InvokeHostFunctionSimulationResult> {
+    let budget = network_config.create_budget()?;
+    let mut diagnostic_events = vec![];
+    let recording_result = invoke_host_function_in_recording_mode(
+        &budget,
+        enable_diagnostics,
+        &host_fn,
+        source_account,
+        auth_entries,
+        ledger_info.clone(),
+        snapshot_source.clone(),
+        base_prng_seed,
+        &mut diagnostic_events,
+    );
+    let invoke_result = match &recording_result {
+        Ok(r) => r.invoke_result.clone(),
+        Err(e) => Err(e.clone()),
+    };
+    // We try to fill the simulation result as much as possible:
+    // diagnostics can be populated unconditionally, and we can always
+    // store the budget measurements.
+    let mut simulation_result = InvokeHostFunctionSimulationResult {
+        // Don't distinguish between the errors that happen during invocation vs
+        // during setup as that seems too granular.
+        invoke_result,
+        simulated_instructions: budget.get_cpu_insns_consumed()?.try_into()?,
+        simulated_memory: budget.get_mem_bytes_consumed()?.try_into()?,
+        diagnostic_events,
+        // Fields that should only be populated for successful invocations.
+        auth: vec![],
+        contract_events: vec![],
+        transaction_data: None,
+        modified_entries: vec![],
+    };
+    let Ok(recording_result) = recording_result else {
+        return Ok(simulation_result);
+    };
+    if recording_result.invoke_result.is_err() {
+        return Ok(simulation_result);
+    }
+    // Fill the remaining fields only for successful invocations.
+    simulation_result.auth = recording_result.auth;
+    simulation_result.contract_events = recording_result.contract_events;
+    simulation_result.modified_entries =
+        extract_modified_entries(&*snapshot_source, &recording_result.ledger_changes)?;
+
+    let (mut resources, rent_changes) = simulate_invoke_host_function_op_resources(
+        &recording_result.ledger_changes,
+        simulation_result.simulated_instructions,
+    )?;
+    let operation = OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
+        host_function: host_fn,
+        auth: simulation_result.auth.clone().try_into()?,
+    });
+    let transaction_resources = compute_adjusted_transaction_resources(
+        operation,
+        &mut resources,
+        adjustment_config,
+        recording_result.contract_events_and_return_value_size,
+    )?;
+    let resource_fee = compute_resource_fee(
+        network_config,
+        &ledger_info,
+        &transaction_resources,
+        &rent_changes,
+        adjustment_config,
+    );
+    simulation_result.transaction_data = Some(create_transaction_data(resources, resource_fee));
+
+    Ok(simulation_result)
+}
+
+/// Simulates `ExtendFootprintTtlOp` operation specified via its
+/// relevant payload parts.
+///
+/// The operation is defined by the `keys_to_extend` and the
+/// `extend_to`. The TTL for the provided keys will be extended to
+/// become `ledger_info.sequence_number + extend_to`. Entries that
+/// don't exist in the `snapshot_source` and entries that already
+/// have TTL bigger than the requested extension will be ignored
+/// and excluded from the simulation results.
+///
+/// The rest of parameters define the ledger state (`snapshot_source`,
+/// `network_config`, `ledger_info`) and simulation adjustment
+/// configuration (`adjustment_config`).
+///
+/// This may only return error in case if ledger is mis-configured.
+pub fn simulate_extend_ttl_op(
+    snapshot_source: &impl SnapshotSource,
+    network_config: &NetworkConfig,
+    adjustment_config: &SimulationAdjustmentConfig,
+    ledger_info: &LedgerInfo,
+    keys_to_extend: &[LedgerKey],
+    extend_to: u32,
+) -> Result<ExtendTtlOpSimulationResult> {
+    let (mut resources, rent_changes) = simulate_extend_ttl_op_resources(
+        keys_to_extend,
+        snapshot_source,
+        ledger_info.sequence_number,
+        extend_to,
+    )?;
+    let operation = OperationBody::ExtendFootprintTtl(ExtendFootprintTtlOp {
+        ext: ExtensionPoint::V0,
+        extend_to,
+    });
+    let transaction_resources =
+        compute_adjusted_transaction_resources(operation, &mut resources, adjustment_config, 0)?;
+    let resource_fee = compute_resource_fee(
+        network_config,
+        &ledger_info,
+        &transaction_resources,
+        &rent_changes,
+        adjustment_config,
+    );
+    Ok(ExtendTtlOpSimulationResult {
+        transaction_data: create_transaction_data(resources, resource_fee),
+    })
+}
+
+/// Simulates `RestoreFootprintTtlOp` operation specified via its
+/// relevant payload parts.
+///
+/// The operation is defined by the specified `keys_to_restore`. The
+/// keys will be restored with TTL set to
+/// `ledger_info.sequence_number + ledger_info.min_persistent_entry_ttl - 1`.
+/// Live entries will be ignored and excluded from the simulation results.
+///
+/// The rest of parameters define the ledger state (`snapshot_source`,
+/// `network_config`, `ledger_info`) and simulation adjustment
+/// configuration (`adjustment_config`). Note, that the `snapshot_source`
+/// has to be able to provide the access to the archived entries.
+///
+/// This will return error if a key can't be restored due to either having
+/// incorrect type/durability (e.g. a temp entry), if a key is missing from
+/// `snapshot_source`, or in case of ledger mis-configuration.
+pub fn simulate_restore_op(
+    snapshot_source: &impl SnapshotSourceWithArchive,
+    network_config: &NetworkConfig,
+    adjustment_config: &SimulationAdjustmentConfig,
+    ledger_info: &LedgerInfo,
+    keys_to_restore: &[LedgerKey],
+) -> Result<RestoreOpSimulationResult> {
+    let (mut resources, rent_changes) =
+        simulate_restore_op_resources(keys_to_restore, snapshot_source, ledger_info)?;
+    let operation = OperationBody::RestoreFootprint(RestoreFootprintOp {
+        ext: ExtensionPoint::V0,
+    });
+    let transaction_resources =
+        compute_adjusted_transaction_resources(operation, &mut resources, adjustment_config, 0)?;
+    let resource_fee = compute_resource_fee(
+        network_config,
+        &ledger_info,
+        &transaction_resources,
+        &rent_changes,
+        adjustment_config,
+    );
+    Ok(RestoreOpSimulationResult {
+        transaction_data: create_transaction_data(resources, resource_fee),
+    })
+}
+
+impl SimulationAdjustmentFactor {
+    pub fn new(multiplicative_factor: f64, additive_factor: u32) -> Self {
+        Self {
+            multiplicative_factor,
+            additive_factor,
+        }
+    }
+
+    pub fn no_adjustment() -> Self {
+        Self {
+            multiplicative_factor: 1.0,
+            additive_factor: 0,
+        }
+    }
+}
+
+impl SimulationAdjustmentConfig {
+    pub fn no_adjustments() -> Self {
+        Self {
+            instructions: SimulationAdjustmentFactor::no_adjustment(),
+            read_bytes: SimulationAdjustmentFactor::no_adjustment(),
+            write_bytes: SimulationAdjustmentFactor::no_adjustment(),
+            tx_size: SimulationAdjustmentFactor::no_adjustment(),
+            refundable_fee: SimulationAdjustmentFactor::no_adjustment(),
+        }
+    }
+
+    pub fn default_adjustment() -> Self {
+        Self {
+            instructions: SimulationAdjustmentFactor::new(1.02, 50_000),
+            read_bytes: SimulationAdjustmentFactor::no_adjustment(),
+            write_bytes: SimulationAdjustmentFactor::no_adjustment(),
+            // It's safe to have pretty significant adjustment for tx size, as
+            // unused fee will be refunded.
+            tx_size: SimulationAdjustmentFactor::new(1.1, 500),
+            refundable_fee: SimulationAdjustmentFactor::new(1.15, 0),
+        }
+    }
+}
+
+fn create_transaction_data(
+    resources: SorobanResources,
+    resource_fee: i64,
+) -> SorobanTransactionData {
+    SorobanTransactionData {
+        resources,
+        resource_fee,
+        ext: ExtensionPoint::V0,
+    }
+}
+
+fn extract_modified_entries(
+    snapshot: &(impl SnapshotSource + ?Sized),
+    ledger_changes: &[LedgerEntryChange],
+) -> Result<Vec<LedgerEntryDiff>> {
+    let mut diffs = vec![];
+    for c in ledger_changes {
+        if c.read_only {
+            continue;
+        }
+        let key = LedgerKey::from_xdr(c.encoded_key.clone(), DEFAULT_XDR_RW_LIMITS)?;
+        let state_before = snapshot.get(&Rc::new(key))?.map(|v| v.0.as_ref().clone());
+        let state_after = match &c.encoded_new_value {
+            Some(v) => Some(LedgerEntry::from_xdr(v.clone(), DEFAULT_XDR_RW_LIMITS)?),
+            None => None,
+        };
+        diffs.push(LedgerEntryDiff {
+            state_before,
+            state_after,
+        });
+    }
+    Ok(diffs)
+}
diff --git a/soroban-simulation/src/snapshot_source.rs b/soroban-simulation/src/snapshot_source.rs
new file mode 100644
index 00000000..1a480211
--- /dev/null
+++ b/soroban-simulation/src/snapshot_source.rs
@@ -0,0 +1,117 @@
+use crate::network_config::NetworkConfig;
+use crate::simulation::{
+    simulate_restore_op, RestoreOpSimulationResult, SimulationAdjustmentConfig,
+};
+use anyhow::{anyhow, Result};
+use soroban_env_host::{
+    ledger_info::get_key_durability,
+    storage::{EntryWithLiveUntil, SnapshotSource},
+    xdr::{ContractDataDurability, LedgerKey, ScErrorCode, ScErrorType},
+    HostError, LedgerInfo,
+};
+use std::cell::RefCell;
+use std::collections::BTreeSet;
+use std::rc::Rc;
+
+/// Read-only ledger snapshot accessor that also has access
+/// to archived entries.
+///
+/// Unlike `SnapshotSource` trait that must only return live entries,
+/// this should return both live and archived ledger entries.
+pub trait SnapshotSourceWithArchive {
+    fn get_including_archived(
+        &self,
+        key: &Rc<LedgerKey>,
+    ) -> std::result::Result<Option<EntryWithLiveUntil>, HostError>;
+}
+
+/// The `SnapshotSource` implementation that automatically restores
+/// the archived ledger entries accessed during `get` calls.
+///
+/// This does not define a concrete implementation for ledger lookups
+/// and just wraps a `SnapshotSourceWithArchive`.
+///
+/// The restored entries will have the rent automatically bumped to
+/// `min_persistent_entry_ttl`, which is consistent with the behavior
+/// of `RestoreFootprintOp` if it was called in the same ledger.
+pub struct AutoRestoringSnapshotSource<T: SnapshotSourceWithArchive> {
+    snapshot_source: Rc<T>,
+    min_persistent_live_until_ledger: u32,
+    current_ledger_sequence: u32,
+    restored_ledger_keys: RefCell<BTreeSet<Rc<LedgerKey>>>,
+}
+
+impl<T: SnapshotSourceWithArchive> AutoRestoringSnapshotSource<T> {
+    pub fn new(snapshot_source: Rc<T>, ledger_info: &LedgerInfo) -> Result<Self, anyhow::Error> {
+        Ok(Self {
+            snapshot_source,
+            min_persistent_live_until_ledger: ledger_info.min_live_until_ledger_checked(ContractDataDurability::Persistent).ok_or_else(|| anyhow!("minimum persistent live until ledger overflows - ledger info is misconfigured"))?,
+            current_ledger_sequence: ledger_info.sequence_number,
+            restored_ledger_keys: RefCell::new(Default::default()),
+        })
+    }
+
+    /// Resets all the restored keys recorded so far.
+    pub fn reset_restored_keys(&self) {
+        self.restored_ledger_keys.borrow_mut().clear();
+    }
+
+    /// Simulates a `RestoreFootprintOp` for all the keys that have been
+    /// restored so far.
+    pub fn simulate_restore_keys_op(
+        &self,
+        network_config: &NetworkConfig,
+        adjustment_config: &SimulationAdjustmentConfig,
+        ledger_info: &LedgerInfo,
+    ) -> Result<Option<RestoreOpSimulationResult>> {
+        let restored_keys = self.restored_ledger_keys.borrow();
+        if restored_keys.is_empty() {
+            return Ok(None);
+        }
+        simulate_restore_op(
+            self.snapshot_source.as_ref(),
+            network_config,
+            adjustment_config,
+            ledger_info,
+            restored_keys
+                .iter()
+                .map(|k| k.as_ref().clone())
+                .collect::<Vec<LedgerKey>>()
+                .as_ref(),
+        )
+        .map(|res| Some(res))
+    }
+}
+
+impl<T: SnapshotSourceWithArchive> SnapshotSource for AutoRestoringSnapshotSource<T> {
+    fn get(&self, key: &Rc<LedgerKey>) -> Result<Option<EntryWithLiveUntil>, HostError> {
+        let entry_with_live_until = self.snapshot_source.get_including_archived(key)?;
+        if let Some((entry, live_until)) = entry_with_live_until {
+            if let Some(durability) = get_key_durability(key.as_ref()) {
+                let live_until = live_until.ok_or_else(|| {
+                    // Entries with durability must have TTL.
+                    HostError::from((ScErrorType::Storage, ScErrorCode::InternalError))
+                })?;
+                if live_until < self.current_ledger_sequence {
+                    return match durability {
+                        ContractDataDurability::Temporary => Ok(None),
+                        ContractDataDurability::Persistent => {
+                            let mut restored_ledger_keys =
+                                self.restored_ledger_keys.try_borrow_mut().map_err(|_| {
+                                    HostError::from((
+                                        ScErrorType::Context,
+                                        ScErrorCode::InternalError,
+                                    ))
+                                })?;
+                            restored_ledger_keys.insert(key.clone());
+                            Ok(Some((entry, Some(self.min_persistent_live_until_ledger))))
+                        }
+                    };
+                }
+            }
+            Ok(Some((entry, live_until)))
+        } else {
+            Ok(None)
+        }
+    }
+}
diff --git a/soroban-simulation/src/state_ttl.rs b/soroban-simulation/src/state_ttl.rs
deleted file mode 100644
index a3ef4e68..00000000
--- a/soroban-simulation/src/state_ttl.rs
+++ /dev/null
@@ -1,63 +0,0 @@
-use soroban_env_host::xdr::ContractDataDurability::Persistent;
-use soroban_env_host::xdr::{
-    ContractCodeEntry, ContractDataDurability, ContractDataEntry, LedgerEntry, LedgerEntryData,
-};
-use std::convert::TryInto;
-
-pub(crate) trait TTLLedgerEntry {
-    fn durability(&self) -> ContractDataDurability;
-    fn live_until_ledger_seq(&self) -> u32;
-    fn is_live(&self, current_ledger_seq: u32) -> bool {
-        self.live_until_ledger_seq() >= current_ledger_seq
-    }
-}
-
-impl TTLLedgerEntry for (&ContractCodeEntry, u32) {
-    fn durability(&self) -> ContractDataDurability {
-        Persistent
-    }
-
-    fn live_until_ledger_seq(&self) -> u32 {
-        self.1
-    }
-}
-
-impl TTLLedgerEntry for (&ContractDataEntry, u32) {
-    fn durability(&self) -> ContractDataDurability {
-        self.0.durability
-    }
-
-    fn live_until_ledger_seq(&self) -> u32 {
-        self.1
-    }
-}
-
-// Convert a ledger entry and its Time to live (i.e. live_until_seq) into a TTLLedgerEntry
-impl<'a> TryInto<Box<dyn TTLLedgerEntry + 'a>> for &'a (LedgerEntry, Option<u32>) {
-    type Error = String;
-
-    fn try_into(self) -> Result<Box<dyn TTLLedgerEntry + 'a>, Self::Error> {
-        match (&self.0.data, self.1) {
-            (LedgerEntryData::ContractData(d), Some(live_until_seq)) => {
-                Ok(Box::new((d, live_until_seq)))
-            }
-            (LedgerEntryData::ContractCode(c), Some(live_until_seq)) => {
-                Ok(Box::new((c, live_until_seq)))
-            }
-            (LedgerEntryData::ContractData(_) | LedgerEntryData::ContractCode(_), _) => Err(
-                format!("missing ttl for ledger entry ({})", self.0.data.name()),
-            ),
-            _ => Err(format!(
-                "ledger entry type ({}) cannot have a TTL",
-                self.0.data.name()
-            )),
-        }
-    }
-}
-
-pub(crate) fn get_restored_ledger_sequence(
-    current_ledger_seq: u32,
-    min_persistent_ttl: u32,
-) -> u32 {
-    current_ledger_seq + min_persistent_ttl - 1
-}


Test patch:
diff --git a/soroban-env-host/src/e2e_invoke.rs b/soroban-env-host/src/e2e_invoke.rs
index b5006cb0..81a141d9 100644
--- a/soroban-env-host/src/e2e_invoke.rs
+++ b/soroban-env-host/src/e2e_invoke.rs
@@ -4,6 +4,9 @@
 /// host functions.
 use std::{cmp::max, rc::Rc};
 
+use crate::ledger_info::get_key_durability;
+#[cfg(any(test, feature = "unstable-next-api"))]
+use crate::storage::EntryWithLiveUntil;
 #[cfg(any(test, feature = "recording_mode"))]
 use crate::{
     auth::RecordedAuthPayload,
@@ -16,7 +19,6 @@ use crate::{
     fees::LedgerEntryRentChange,
     host::{
         crypto::sha256_hash_from_bytes,
-        ledger_info_helper::get_key_durability,
         metered_clone::{MeteredAlloc, MeteredClone, MeteredContainer, MeteredIterator},
         metered_xdr::{metered_from_xdr_with_budget, metered_write_xdr},
         TraceHook,
@@ -60,7 +62,10 @@ pub struct InvokeHostFunctionResult {
 pub struct InvokeHostFunctionRecordingModeResult {
     /// Result value of the invoked function or error returned for invocation.
     pub invoke_result: Result<ScVal, HostError>,
+    /// Resources recorded during the invocation, including the footprint.
     pub resources: SorobanResources,
+    /// Authorization data, either passed through from the call (when provided),
+    /// or recorded during the invocation.
     pub auth: Vec<SorobanAuthorizationEntry>,
     /// All the ledger changes caused by this invocation, including no-ops.
     ///
@@ -73,6 +78,10 @@ pub struct InvokeHostFunctionRecordingModeResult {
     ///
     /// Empty when invocation fails.
     pub contract_events: Vec<ContractEvent>,
+    /// Size of the encoded contract events and the return value.
+    /// Non-zero only when invocation has succeeded.
+    #[cfg(any(test, feature = "unstable-next-api"))]
+    pub contract_events_and_return_value_size: u32,
 }
 
 /// Represents a change of the ledger entry from 'old' value to the 'new' one.
@@ -151,8 +160,15 @@ pub fn get_ledger_changes(
                 new_live_until_ledger: 0,
             });
         }
-        if init_storage_snapshot.has(key)? {
-            let (old_entry, old_live_until_ledger) = init_storage_snapshot.get(key)?;
+        #[cfg(any(test, feature = "unstable-next-api"))]
+        let entry_with_live_until = init_storage_snapshot.get(key)?;
+        #[cfg(not(any(test, feature = "unstable-next-api")))]
+        let entry_with_live_until = if init_storage_snapshot.has(key)? {
+            Some(init_storage_snapshot.get(key)?)
+        } else {
+            None
+        };
+        if let Some((old_entry, old_live_until_ledger)) = entry_with_live_until {
             let mut buf = vec![];
             metered_write_xdr(budget, old_entry.as_ref(), &mut buf)?;
             entry_change.old_entry_size_bytes = buf.len() as u32;
@@ -198,7 +214,7 @@ pub fn get_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<LedgerEntryChange>) -> Vec<LedgerEntryRentChange> {
+pub fn extract_rent_changes(ledger_changes: &[LedgerEntryChange]) -> Vec<LedgerEntryRentChange> {
     ledger_changes
         .iter()
         .filter_map(|entry_change| {
@@ -515,10 +531,12 @@ pub fn invoke_host_function_in_recording_mode(
         host.set_diagnostic_level(DiagnosticLevel::Debug)?;
     }
     let invoke_result = host.invoke_function(host_function);
-
+    let mut contract_events_and_return_value_size = 0_u32;
     if let Ok(res) = &invoke_result {
         let mut encoded_result_sc_val = vec![];
         metered_write_xdr(&budget, res, &mut encoded_result_sc_val)?;
+        contract_events_and_return_value_size = contract_events_and_return_value_size
+            .saturating_add(encoded_result_sc_val.len() as u32);
     }
     let mut output_auth = if let Some(auth_entries) = auth_entries {
         auth_entries
@@ -549,8 +567,15 @@ pub fn invoke_host_function_in_recording_mode(
         let mut encoded_ttl_entries = Vec::with_capacity(storage.footprint.0.len());
         let mut read_bytes = 0_u32;
         for (lk, _) in &storage.footprint.0 {
-            if ledger_snapshot.has(lk)? {
-                let (le, live_until) = ledger_snapshot.get(lk)?;
+            #[cfg(any(test, feature = "unstable-next-api"))]
+            let entry_with_live_until = ledger_snapshot.get(lk)?;
+            #[cfg(not(any(test, feature = "unstable-next-api")))]
+            let entry_with_live_until = if ledger_snapshot.has(lk)? {
+                Some(ledger_snapshot.get(lk)?)
+            } else {
+                None
+            };
+            if let Some((le, live_until)) = entry_with_live_until {
                 let encoded_le = host.to_xdr_non_metered(&*le)?;
                 read_bytes = read_bytes.saturating_add(encoded_le.len() as u32);
                 encoded_ledger_entries.push(encoded_le);
@@ -595,7 +620,11 @@ pub fn invoke_host_function_in_recording_mode(
     let (ledger_changes, contract_events) = if invoke_result.is_ok() {
         let ledger_changes =
             get_ledger_changes(&budget, &storage, &*ledger_snapshot, init_ttl_map)?;
-        let _encoded_contract_events = encode_contract_events(budget, &events)?;
+        let encoded_contract_events = encode_contract_events(budget, &events)?;
+        for e in &encoded_contract_events {
+            contract_events_and_return_value_size =
+                contract_events_and_return_value_size.saturating_add(e.len() as u32);
+        }
         let contract_events: Vec<ContractEvent> = events
             .0
             .into_iter()
@@ -623,6 +652,8 @@ pub fn invoke_host_function_in_recording_mode(
         auth: output_auth,
         ledger_changes,
         contract_events,
+        #[cfg(any(test, feature = "unstable-next-api"))]
+        contract_events_and_return_value_size,
     })
 }
 
@@ -800,6 +831,20 @@ struct StorageMapSnapshotSource<'a> {
     map: &'a StorageMap,
 }
 
+#[cfg(any(test, feature = "unstable-next-api"))]
+impl<'a> SnapshotSource for StorageMapSnapshotSource<'a> {
+    fn get(&self, key: &Rc<LedgerKey>) -> Result<Option<EntryWithLiveUntil>, HostError> {
+        if let Some(Some((entry, live_until_ledger))) =
+            self.map.get::<Rc<LedgerKey>>(key, self.budget)?
+        {
+            Ok(Some((Rc::clone(entry), *live_until_ledger)))
+        } else {
+            Ok(None)
+        }
+    }
+}
+
+#[cfg(not(any(test, feature = "unstable-next-api")))]
 impl<'a> SnapshotSource for StorageMapSnapshotSource<'a> {
     fn get(&self, key: &Rc<LedgerKey>) -> Result<(Rc<LedgerEntry>, Option<u32>), HostError> {
         if let Some(Some((entry, live_until_ledger))) =
diff --git a/soroban-env-host/src/e2e_testutils.rs b/soroban-env-host/src/e2e_testutils.rs
new file mode 100644
index 00000000..284262a7
--- /dev/null
+++ b/soroban-env-host/src/e2e_testutils.rs
@@ -0,0 +1,234 @@
+use crate::xdr::{
+    AccountEntry, AccountEntryExt, AccountId, ContractCodeEntry, ContractDataDurability,
+    ContractDataEntry, ContractExecutable, ContractIdPreimage, ContractIdPreimageFromAddress,
+    CreateContractArgs, ExtensionPoint, HashIdPreimage, HashIdPreimageContractId, HostFunction,
+    InvokeContractArgs, LedgerEntry, LedgerEntryData, LedgerEntryExt, LedgerKey,
+    LedgerKeyContractCode, LedgerKeyContractData, Limits, PublicKey, ScAddress, ScBytes,
+    ScContractInstance, ScMapEntry, ScSymbol, ScVal, SequenceNumber, SorobanAuthorizationEntry,
+    SorobanAuthorizedFunction, SorobanAuthorizedInvocation, SorobanCredentials, Thresholds,
+    Uint256, WriteXdr,
+};
+use crate::LedgerInfo;
+use sha2::{Digest, Sha256};
+
+pub const DEFAULT_LEDGER_SEQ: u32 = 1_000_000;
+pub const DEFAULT_NETWORK_ID: [u8; 32] = [5; 32];
+
+pub fn ledger_entry(le_data: LedgerEntryData) -> LedgerEntry {
+    LedgerEntry {
+        last_modified_ledger_seq: 0,
+        data: le_data,
+        ext: LedgerEntryExt::V0,
+    }
+}
+
+pub fn account_entry(account_id: &AccountId) -> LedgerEntry {
+    ledger_entry(LedgerEntryData::Account(AccountEntry {
+        account_id: account_id.clone(),
+        balance: 10_000_000,
+        seq_num: SequenceNumber(0),
+        num_sub_entries: 0,
+        inflation_dest: None,
+        flags: 0,
+        home_domain: Default::default(),
+        thresholds: Thresholds([1, 0, 0, 0]),
+        signers: Default::default(),
+        ext: AccountEntryExt::V0,
+    }))
+}
+
+pub fn get_wasm_hash(wasm: &[u8]) -> [u8; 32] {
+    Sha256::digest(wasm).into()
+}
+
+pub fn bytes_sc_val(bytes: &[u8]) -> ScVal {
+    ScVal::Bytes(ScBytes(bytes.try_into().unwrap()))
+}
+
+pub fn wasm_entry(wasm: &[u8]) -> LedgerEntry {
+    ledger_entry(LedgerEntryData::ContractCode(ContractCodeEntry {
+        ext: ExtensionPoint::V0,
+        hash: get_wasm_hash(wasm).try_into().unwrap(),
+        code: wasm.try_into().unwrap(),
+    }))
+}
+
+pub fn default_ledger_info() -> LedgerInfo {
+    LedgerInfo {
+        protocol_version: 20,
+        sequence_number: DEFAULT_LEDGER_SEQ,
+        timestamp: 12345678,
+        network_id: DEFAULT_NETWORK_ID,
+        base_reserve: 5_000_000,
+        min_temp_entry_ttl: 16,
+        min_persistent_entry_ttl: 100_000,
+        max_entry_ttl: 10_000_000,
+    }
+}
+
+pub struct CreateContractData {
+    pub deployer: AccountId,
+    pub wasm_key: LedgerKey,
+    pub wasm_entry: LedgerEntry,
+    pub contract_key: LedgerKey,
+    pub contract_entry: LedgerEntry,
+    pub contract_address: ScAddress,
+    pub auth_entry: SorobanAuthorizationEntry,
+    pub host_fn: HostFunction,
+}
+
+impl CreateContractData {
+    pub fn new(salt: [u8; 32], wasm: &[u8]) -> Self {
+        let deployer = get_account_id([123; 32]);
+        let contract_id_preimage = get_contract_id_preimage(&deployer, &salt);
+
+        let host_fn = HostFunction::CreateContract(CreateContractArgs {
+            contract_id_preimage: contract_id_preimage.clone(),
+            executable: ContractExecutable::Wasm(get_wasm_hash(wasm).try_into().unwrap()),
+        });
+        let contract_address = ScAddress::Contract(
+            get_contract_id_hash(&contract_id_preimage)
+                .try_into()
+                .unwrap(),
+        );
+        let contract_key = LedgerKey::ContractData(LedgerKeyContractData {
+            contract: contract_address.clone(),
+            key: ScVal::LedgerKeyContractInstance,
+            durability: ContractDataDurability::Persistent,
+        });
+        let auth_entry = create_contract_auth(&contract_id_preimage, wasm);
+
+        let contract_entry = ledger_entry(LedgerEntryData::ContractData(ContractDataEntry {
+            ext: ExtensionPoint::V0,
+            contract: contract_address.clone(),
+            key: ScVal::LedgerKeyContractInstance,
+            durability: ContractDataDurability::Persistent,
+            val: ScVal::ContractInstance(ScContractInstance {
+                executable: ContractExecutable::Wasm(get_wasm_hash(wasm).try_into().unwrap()),
+                storage: None,
+            }),
+        }));
+
+        Self {
+            deployer,
+            wasm_key: get_wasm_key(wasm),
+            wasm_entry: wasm_entry(wasm),
+            contract_key,
+            contract_entry,
+            contract_address,
+            auth_entry,
+            host_fn,
+        }
+    }
+}
+
+pub fn upload_wasm_host_fn(wasm: &[u8]) -> HostFunction {
+    HostFunction::UploadContractWasm(wasm.try_into().unwrap())
+}
+
+pub fn get_wasm_key(wasm: &[u8]) -> LedgerKey {
+    LedgerKey::ContractCode(LedgerKeyContractCode {
+        hash: get_wasm_hash(wasm).try_into().unwrap(),
+    })
+}
+pub fn get_account_id(pub_key: [u8; 32]) -> AccountId {
+    AccountId(PublicKey::PublicKeyTypeEd25519(pub_key.try_into().unwrap()))
+}
+
+pub fn get_contract_id_preimage(account_id: &AccountId, salt: &[u8; 32]) -> ContractIdPreimage {
+    ContractIdPreimage::Address(ContractIdPreimageFromAddress {
+        address: ScAddress::Account(account_id.clone()),
+        salt: Uint256(*salt),
+    })
+}
+
+pub fn get_contract_id_hash(id_preimage: &ContractIdPreimage) -> [u8; 32] {
+    let preimage = HashIdPreimage::ContractId(HashIdPreimageContractId {
+        network_id: DEFAULT_NETWORK_ID.try_into().unwrap(),
+        contract_id_preimage: id_preimage.clone(),
+    });
+    Sha256::digest(&preimage.to_xdr(Limits::none()).unwrap()).into()
+}
+
+pub fn create_contract_auth(
+    contract_id_preimage: &ContractIdPreimage,
+    wasm: &[u8],
+) -> SorobanAuthorizationEntry {
+    SorobanAuthorizationEntry {
+        credentials: SorobanCredentials::SourceAccount,
+        root_invocation: SorobanAuthorizedInvocation {
+            function: SorobanAuthorizedFunction::CreateContractHostFn(CreateContractArgs {
+                contract_id_preimage: contract_id_preimage.clone(),
+                executable: ContractExecutable::Wasm(get_wasm_hash(wasm).try_into().unwrap()),
+            }),
+            sub_invocations: Default::default(),
+        },
+    }
+}
+
+#[derive(Clone)]
+pub struct AuthContractInvocationNode {
+    pub address: ScAddress,
+    pub children: Vec<AuthContractInvocationNode>,
+}
+
+pub fn sc_struct_field(key: &str, val: ScVal) -> ScMapEntry {
+    ScMapEntry {
+        key: ScVal::Symbol(key.try_into().unwrap()),
+        val,
+    }
+}
+
+impl AuthContractInvocationNode {
+    pub fn into_scval(self, address_count: usize) -> ScVal {
+        let children: Vec<ScVal> = self
+            .children
+            .into_iter()
+            .map(|c| c.into_scval(address_count))
+            .collect();
+        let fields = vec![
+            sc_struct_field("children", ScVal::Vec(Some(children.try_into().unwrap()))),
+            sc_struct_field("contract", ScVal::Address(self.address)),
+            sc_struct_field(
+                "need_auth",
+                ScVal::Vec(Some(
+                    vec![ScVal::Bool(true); address_count].try_into().unwrap(),
+                )),
+            ),
+            sc_struct_field("try_call", ScVal::Bool(false)),
+        ];
+        ScVal::Map(Some(fields.try_into().unwrap()))
+    }
+
+    pub fn into_authorized_invocation(self) -> SorobanAuthorizedInvocation {
+        let children: Vec<SorobanAuthorizedInvocation> = self
+            .children
+            .into_iter()
+            .map(|c| c.into_authorized_invocation())
+            .collect();
+        SorobanAuthorizedInvocation {
+            function: SorobanAuthorizedFunction::ContractFn(InvokeContractArgs {
+                contract_address: self.address,
+                function_name: ScSymbol("tree_fn".try_into().unwrap()),
+                args: Default::default(),
+            }),
+            sub_invocations: children.try_into().unwrap(),
+        }
+    }
+}
+
+pub fn auth_contract_invocation(
+    addresses: Vec<ScAddress>,
+    tree: AuthContractInvocationNode,
+) -> HostFunction {
+    let address_count = addresses.len();
+    let address_vals: Vec<ScVal> = addresses.into_iter().map(|a| ScVal::Address(a)).collect();
+    let addresses_val = ScVal::Vec(Some(address_vals.try_into().unwrap()));
+    HostFunction::InvokeContract(InvokeContractArgs {
+        contract_address: tree.address.clone(),
+        function_name: "tree_fn".try_into().unwrap(),
+        args: vec![addresses_val, tree.into_scval(address_count)]
+            .try_into()
+            .unwrap(),
+    })
+}
diff --git a/soroban-env-host/src/test/e2e_tests.rs b/soroban-env-host/src/test/e2e_tests.rs
index b198898c..ffd45e32 100644
--- a/soroban-env-host/src/test/e2e_tests.rs
+++ b/soroban-env-host/src/test/e2e_tests.rs
@@ -1,22 +1,24 @@
-use crate::builtin_contracts::testutils::TestSigner;
-use crate::e2e_invoke::{
-    invoke_host_function_in_recording_mode, ledger_entry_to_ledger_key, LedgerEntryLiveUntilChange,
-};
-use crate::testutils::MockSnapshotSource;
+use crate::e2e_testutils::{account_entry, bytes_sc_val, upload_wasm_host_fn};
 use crate::{
     budget::Budget,
-    e2e_invoke::{invoke_host_function, LedgerEntryChange},
+    builtin_contracts::testutils::TestSigner,
+    e2e_invoke::{
+        invoke_host_function, invoke_host_function_in_recording_mode, ledger_entry_to_ledger_key,
+        LedgerEntryChange, LedgerEntryLiveUntilChange,
+    },
+    e2e_testutils::{
+        auth_contract_invocation, create_contract_auth, default_ledger_info, get_account_id,
+        get_contract_id_preimage, get_wasm_hash, get_wasm_key, ledger_entry, wasm_entry,
+        AuthContractInvocationNode, CreateContractData,
+    },
+    testutils::MockSnapshotSource,
     xdr::{
-        AccountEntry, AccountEntryExt, AccountId, ContractCodeEntry, ContractDataDurability,
-        ContractDataEntry, ContractEvent, ContractExecutable, ContractIdPreimage,
-        ContractIdPreimageFromAddress, CreateContractArgs, DiagnosticEvent, ExtensionPoint,
-        HashIdPreimage, HashIdPreimageContractId, HashIdPreimageSorobanAuthorization, HostFunction,
-        InvokeContractArgs, LedgerEntry, LedgerEntryData, LedgerEntryExt, LedgerFootprint,
-        LedgerKey, LedgerKeyContractCode, LedgerKeyContractData, Limits, PublicKey, ReadXdr,
-        ScAddress, ScBytes, ScContractInstance, ScErrorCode, ScErrorType, ScMapEntry, ScVal,
-        SequenceNumber, SorobanAuthorizationEntry, SorobanAuthorizedFunction,
-        SorobanAuthorizedInvocation, SorobanCredentials, SorobanResources, Thresholds, TtlEntry,
-        Uint256, WriteXdr,
+        AccountId, ContractDataDurability, ContractDataEntry, ContractEvent, DiagnosticEvent,
+        ExtensionPoint, HashIdPreimage, HashIdPreimageSorobanAuthorization, HostFunction,
+        InvokeContractArgs, LedgerEntry, LedgerEntryData, LedgerFootprint, LedgerKey,
+        LedgerKeyContractCode, LedgerKeyContractData, Limits, ReadXdr, ScAddress, ScErrorCode,
+        ScErrorType, ScVal, SorobanAuthorizationEntry, SorobanCredentials, SorobanResources,
+        TtlEntry, WriteXdr,
     },
     Host, HostError, LedgerInfo,
 };
@@ -28,9 +30,6 @@ use sha2::{Digest, Sha256};
 use soroban_test_wasms::{ADD_F32, ADD_I32, AUTH_TEST_CONTRACT, CONTRACT_STORAGE};
 use std::rc::Rc;
 
-const DEFAULT_LEDGER_SEQ: u32 = 1_000_000;
-const DEFAULT_NETWORK_ID: [u8; 32] = [5; 32];
-
 // It's tricky to get exactly the same instruction consumption
 // in the recording storage/auth mode vs the enforcing mode. For
 // example, frame snapshots in enforcing mode contain all the auths and
@@ -40,95 +39,14 @@ const DEFAULT_NETWORK_ID: [u8; 32] = [5; 32];
 // is why this coefficient should be low.
 const RECORDING_MODE_INSTRUCTIONS_COEFFICIENT: f64 = 1.01;
 
-fn default_ledger_info() -> LedgerInfo {
-    LedgerInfo {
-        protocol_version: 20,
-        sequence_number: DEFAULT_LEDGER_SEQ,
-        timestamp: 12345678,
-        network_id: DEFAULT_NETWORK_ID,
-        base_reserve: 5_000_000,
-        min_temp_entry_ttl: 16,
-        min_persistent_entry_ttl: 100_000,
-        max_entry_ttl: 10_000_000,
-    }
-}
-
-fn upload_wasm_host_fn(wasm: &[u8]) -> HostFunction {
-    HostFunction::UploadContractWasm(wasm.try_into().unwrap())
-}
-
-fn get_wasm_hash(wasm: &[u8]) -> [u8; 32] {
-    Sha256::digest(wasm).into()
-}
-
-fn get_wasm_key(wasm: &[u8]) -> LedgerKey {
-    LedgerKey::ContractCode(LedgerKeyContractCode {
-        hash: get_wasm_hash(wasm).try_into().unwrap(),
-    })
-}
-
-fn get_contract_id_preimage(account_id: &AccountId, salt: &[u8; 32]) -> ContractIdPreimage {
-    ContractIdPreimage::Address(ContractIdPreimageFromAddress {
-        address: ScAddress::Account(account_id.clone()),
-        salt: Uint256(salt.clone().try_into().unwrap()),
-    })
-}
-
-fn get_contract_id_hash(id_preimage: &ContractIdPreimage) -> [u8; 32] {
-    let preimage = HashIdPreimage::ContractId(HashIdPreimageContractId {
-        network_id: DEFAULT_NETWORK_ID.clone().try_into().unwrap(),
-        contract_id_preimage: id_preimage.clone(),
-    });
-    Sha256::digest(&preimage.to_xdr(Limits::none()).unwrap()).into()
-}
-
-fn ledger_entry(le_data: LedgerEntryData) -> LedgerEntry {
-    LedgerEntry {
-        last_modified_ledger_seq: 0,
-        data: le_data,
-        ext: LedgerEntryExt::V0,
-    }
-}
-
-fn account_entry(account_id: &AccountId) -> LedgerEntry {
-    ledger_entry(LedgerEntryData::Account(AccountEntry {
-        account_id: account_id.clone(),
-        balance: 10_000_000,
-        seq_num: SequenceNumber(0),
-        num_sub_entries: 0,
-        inflation_dest: None,
-        flags: 0,
-        home_domain: Default::default(),
-        thresholds: Thresholds([1, 0, 0, 0]),
-        signers: Default::default(),
-        ext: AccountEntryExt::V0,
-    }))
-}
-
-fn wasm_entry(wasm: &[u8]) -> LedgerEntry {
-    ledger_entry(LedgerEntryData::ContractCode(ContractCodeEntry {
-        ext: ExtensionPoint::V0,
-        hash: get_wasm_hash(wasm).try_into().unwrap(),
-        code: wasm.try_into().unwrap(),
-    }))
-}
-
 fn wasm_entry_size(wasm: &[u8]) -> u32 {
     wasm_entry(wasm).to_xdr(Limits::none()).unwrap().len() as u32
 }
 
-fn get_account_id(pub_key: [u8; 32]) -> AccountId {
-    AccountId(PublicKey::PublicKeyTypeEd25519(pub_key.try_into().unwrap()))
-}
-
 fn prng_seed() -> [u8; 32] {
     [0; 32]
 }
 
-fn bytes_sc_val(bytes: &[u8]) -> ScVal {
-    ScVal::Bytes(ScBytes(bytes.try_into().unwrap()))
-}
-
 fn resources(
     instructions: u32,
     ro_footprint: Vec<LedgerKey>,
@@ -171,72 +89,6 @@ fn u32_sc_val(v: u32) -> ScVal {
     ScVal::U32(v)
 }
 
-fn create_contract_auth(
-    contract_id_preimage: &ContractIdPreimage,
-    wasm: &[u8],
-) -> SorobanAuthorizationEntry {
-    SorobanAuthorizationEntry {
-        credentials: SorobanCredentials::SourceAccount,
-        root_invocation: SorobanAuthorizedInvocation {
-            function: SorobanAuthorizedFunction::CreateContractHostFn(CreateContractArgs {
-                contract_id_preimage: contract_id_preimage.clone(),
-                executable: ContractExecutable::Wasm(get_wasm_hash(wasm).try_into().unwrap()),
-            }),
-            sub_invocations: Default::default(),
-        },
-    }
-}
-
-struct AuthContractInvocationNode {
-    address: ScAddress,
-    children: Vec<AuthContractInvocationNode>,
-}
-
-fn sc_struct_field(key: &str, val: ScVal) -> ScMapEntry {
-    ScMapEntry {
-        key: ScVal::Symbol(key.try_into().unwrap()),
-        val,
-    }
-}
-
-impl AuthContractInvocationNode {
-    fn into_scval(self, address_count: usize) -> ScVal {
-        let children: Vec<ScVal> = self
-            .children
-            .into_iter()
-            .map(|c| c.into_scval(address_count))
-            .collect();
-        let fields = vec![
-            sc_struct_field("children", ScVal::Vec(Some(children.try_into().unwrap()))),
-            sc_struct_field("contract", ScVal::Address(self.address)),
-            sc_struct_field(
-                "need_auth",
-                ScVal::Vec(Some(
-                    vec![ScVal::Bool(true); address_count].try_into().unwrap(),
-                )),
-            ),
-            sc_struct_field("try_call", ScVal::Bool(false)),
-        ];
-        ScVal::Map(Some(fields.try_into().unwrap()))
-    }
-}
-
-fn auth_contract_invocation(
-    addresses: Vec<ScAddress>,
-    tree: AuthContractInvocationNode,
-) -> HostFunction {
-    let address_count = addresses.len();
-    let address_vals: Vec<ScVal> = addresses.into_iter().map(|a| ScVal::Address(a)).collect();
-    let addresses_val = ScVal::Vec(Some(address_vals.try_into().unwrap()));
-    HostFunction::InvokeContract(InvokeContractArgs {
-        contract_address: tree.address.clone(),
-        function_name: "tree_fn".try_into().unwrap(),
-        args: vec![addresses_val, tree.into_scval(address_count)]
-            .try_into()
-            .unwrap(),
-    })
-}
-
 fn sign_auth_entry(
     ledger_info: &LedgerInfo,
     signers: &Vec<TestSigner>,
@@ -344,6 +196,7 @@ struct InvokeHostFunctionRecordingHelperResult {
     ledger_changes: Vec<LedgerEntryChangeHelper>,
     contract_events: Vec<ContractEvent>,
     diagnostic_events: Vec<DiagnosticEvent>,
+    contract_events_and_return_value_size: u32,
 }
 
 fn invoke_host_function_helper(
@@ -461,6 +314,7 @@ fn invoke_host_function_recording_helper(
         ledger_changes: res.ledger_changes.into_iter().map(|c| c.into()).collect(),
         contract_events: res.contract_events,
         diagnostic_events,
+        contract_events_and_return_value_size: res.contract_events_and_return_value_size,
     })
 }
 
@@ -530,6 +384,10 @@ fn invoke_host_function_using_simulation_with_signers(
         recording_result.diagnostic_events,
         recording_result_with_enforcing_auth.diagnostic_events
     );
+    assert_eq!(
+        recording_result.contract_events_and_return_value_size,
+        recording_result_with_enforcing_auth.contract_events_and_return_value_size
+    );
 
     // Instructions are expected to be slightly different between recording and
     // enforcing modes, so just make sure that the estimation is within the small
@@ -568,6 +426,19 @@ fn invoke_host_function_using_simulation_with_signers(
         recording_result.diagnostic_events,
         enforcing_result.diagnostic_events
     );
+    if let Ok(res) = &enforcing_result.invoke_result {
+        let mut enforcing_events_size = res.to_xdr(Limits::none()).unwrap().len();
+        for e in &enforcing_result.contract_events {
+            enforcing_events_size += e.to_xdr(Limits::none()).unwrap().len();
+        }
+        assert_eq!(
+            recording_result.contract_events_and_return_value_size,
+            enforcing_events_size as u32
+        );
+    } else {
+        assert_eq!(recording_result.contract_events_and_return_value_size, 0);
+    }
+
     Ok(enforcing_result)
 }
 
@@ -590,62 +461,6 @@ fn invoke_host_function_using_simulation(
     )
 }
 
-struct CreateContractData {
-    deployer: AccountId,
-    wasm_key: LedgerKey,
-    wasm_entry: LedgerEntry,
-    contract_key: LedgerKey,
-    contract_entry: LedgerEntry,
-    contract_address: ScAddress,
-    auth_entry: SorobanAuthorizationEntry,
-    host_fn: HostFunction,
-}
-
-impl CreateContractData {
-    fn new(salt: [u8; 32], wasm: &[u8]) -> Self {
-        let deployer = get_account_id([123; 32]);
-        let contract_id_preimage = get_contract_id_preimage(&deployer, &salt);
-
-        let host_fn = HostFunction::CreateContract(CreateContractArgs {
-            contract_id_preimage: contract_id_preimage.clone(),
-            executable: ContractExecutable::Wasm(get_wasm_hash(wasm).try_into().unwrap()),
-        });
-        let contract_address = ScAddress::Contract(
-            get_contract_id_hash(&contract_id_preimage)
-                .try_into()
-                .unwrap(),
-        );
-        let contract_key = LedgerKey::ContractData(LedgerKeyContractData {
-            contract: contract_address.clone(),
-            key: ScVal::LedgerKeyContractInstance,
-            durability: ContractDataDurability::Persistent,
-        });
-        let auth_entry = create_contract_auth(&contract_id_preimage, wasm);
-
-        let contract_entry = ledger_entry(LedgerEntryData::ContractData(ContractDataEntry {
-            ext: ExtensionPoint::V0,
-            contract: contract_address.clone(),
-            key: ScVal::LedgerKeyContractInstance,
-            durability: ContractDataDurability::Persistent,
-            val: ScVal::ContractInstance(ScContractInstance {
-                executable: ContractExecutable::Wasm(get_wasm_hash(wasm).try_into().unwrap()),
-                storage: None,
-            }),
-        }));
-
-        Self {
-            deployer,
-            wasm_key: get_wasm_key(wasm),
-            wasm_entry: wasm_entry(wasm),
-            contract_key,
-            contract_entry,
-            contract_address,
-            auth_entry,
-            host_fn,
-        }
-    }
-}
-
 fn invoke_contract_host_fn(contract: &ScAddress, fn_name: &str, args: Vec<ScVal>) -> HostFunction {
     HostFunction::InvokeContract(InvokeContractArgs {
         contract_address: contract.clone(),
diff --git a/soroban-env-host/src/testutils.rs b/soroban-env-host/src/testutils.rs
index ec35f4e7..5e914e19 100644
--- a/soroban-env-host/src/testutils.rs
+++ b/soroban-env-host/src/testutils.rs
@@ -1,13 +1,15 @@
 use crate::e2e_invoke::ledger_entry_to_ledger_key;
+#[cfg(any(test, feature = "unstable-next-api"))]
+use crate::storage::EntryWithLiveUntil;
 use crate::{
     budget::Budget,
     builtin_contracts::testutils::create_account,
     storage::{SnapshotSource, Storage},
     xdr::{
-        AccountId, ContractCostType, LedgerEntry, LedgerKey, PublicKey, ScAddress, ScErrorCode,
-        ScErrorType, ScVal, ScVec, Uint256,
+        AccountId, ContractCostType, LedgerEntry, LedgerKey, PublicKey, ScAddress, ScVal, ScVec,
+        Uint256,
     },
-    AddressObject, BytesObject, Env, EnvBase, Error, Host, HostError, LedgerInfo, MeteredOrdMap,
+    AddressObject, BytesObject, Env, EnvBase, Host, HostError, LedgerInfo, MeteredOrdMap,
     StorageType, SymbolSmall, Val, VecObject,
 };
 use rand::RngCore;
@@ -131,12 +133,29 @@ impl MockSnapshotSource {
         Self(map)
     }
 }
+
+#[cfg(any(test, feature = "unstable-next-api"))]
+impl SnapshotSource for MockSnapshotSource {
+    fn get(&self, key: &Rc<LedgerKey>) -> Result<Option<EntryWithLiveUntil>, HostError> {
+        if let Some((entry, live_until)) = self.0.get(key) {
+            Ok(Some((Rc::clone(entry), *live_until)))
+        } else {
+            Ok(None)
+        }
+    }
+}
+
+#[cfg(not(any(test, feature = "unstable-next-api")))]
 impl SnapshotSource for MockSnapshotSource {
     fn get(&self, key: &Rc<LedgerKey>) -> Result<(Rc<LedgerEntry>, Option<u32>), HostError> {
         if let Some(val) = self.0.get(key) {
             Ok((Rc::clone(&val.0), val.1))
         } else {
-            Err(Error::from_type_and_code(ScErrorType::Storage, ScErrorCode::MissingValue).into())
+            Err((
+                crate::xdr::ScErrorType::Storage,
+                crate::xdr::ScErrorCode::MissingValue,
+            )
+                .into())
         }
     }
 
diff --git a/soroban-simulation/src/test.rs b/soroban-simulation/src/test.rs
new file mode 100644
index 00000000..81b8fd82
--- /dev/null
+++ b/soroban-simulation/src/test.rs
@@ -0,0 +1,3 @@
+mod network_config;
+mod simulation;
+mod snapshot_source;
diff --git a/soroban-simulation/src/test/network_config.rs b/soroban-simulation/src/test/network_config.rs
new file mode 100644
index 00000000..ba6ccd3f
--- /dev/null
+++ b/soroban-simulation/src/test/network_config.rs
@@ -0,0 +1,231 @@
+use crate::network_config::NetworkConfig;
+use soroban_env_host::{
+    e2e_testutils::ledger_entry,
+    fees::{FeeConfiguration, RentFeeConfiguration},
+    xdr::{
+        ConfigSettingContractBandwidthV0, ConfigSettingContractComputeV0,
+        ConfigSettingContractEventsV0, ConfigSettingContractHistoricalDataV0,
+        ConfigSettingContractLedgerCostV0, ConfigSettingEntry, ContractCostParamEntry,
+        ContractCostParams, ExtensionPoint, LedgerEntry, LedgerEntryData, StateArchivalSettings,
+    },
+    LedgerInfo,
+};
+
+use crate::testutils::MockSnapshotSource;
+use pretty_assertions::assert_eq;
+use soroban_env_host::xdr::ContractCostType;
+
+fn config_entry(entry: ConfigSettingEntry) -> (LedgerEntry, Option<u32>) {
+    (ledger_entry(LedgerEntryData::ConfigSetting(entry)), None)
+}
+
+#[test]
+fn test_load_config_from_snapshot() {
+    let cpu_cost_params = ContractCostParams(
+        vec![
+            ContractCostParamEntry {
+                ext: ExtensionPoint::V0,
+                const_term: 35,
+                linear_term: 36,
+            },
+            ContractCostParamEntry {
+                ext: ExtensionPoint::V0,
+                const_term: 37,
+                linear_term: 38,
+            },
+        ]
+        .try_into()
+        .unwrap(),
+    );
+    let memory_cost_params = ContractCostParams(
+        vec![
+            ContractCostParamEntry {
+                ext: ExtensionPoint::V0,
+                const_term: 39,
+                linear_term: 40,
+            },
+            ContractCostParamEntry {
+                ext: ExtensionPoint::V0,
+                const_term: 41,
+                linear_term: 42,
+            },
+            ContractCostParamEntry {
+                ext: ExtensionPoint::V0,
+                const_term: 43,
+                linear_term: 44,
+            },
+        ]
+        .try_into()
+        .unwrap(),
+    );
+    let snapshot_source = MockSnapshotSource::from_entries(
+        vec![
+            config_entry(ConfigSettingEntry::ContractComputeV0(
+                ConfigSettingContractComputeV0 {
+                    ledger_max_instructions: 1,
+                    tx_max_instructions: 2,
+                    fee_rate_per_instructions_increment: 3,
+                    tx_memory_limit: 4,
+                },
+            )),
+            config_entry(ConfigSettingEntry::ContractLedgerCostV0(
+                ConfigSettingContractLedgerCostV0 {
+                    ledger_max_read_ledger_entries: 5,
+                    ledger_max_read_bytes: 6,
+                    ledger_max_write_ledger_entries: 7,
+                    ledger_max_write_bytes: 8,
+                    tx_max_read_ledger_entries: 9,
+                    tx_max_read_bytes: 10,
+                    tx_max_write_ledger_entries: 11,
+                    tx_max_write_bytes: 12,
+                    fee_read_ledger_entry: 13,
+                    fee_write_ledger_entry: 14,
+                    fee_read1_kb: 15,
+                    // From tests/resources `test_compute_write_fee`
+                    bucket_list_target_size_bytes: 100_000_000_000_000,
+                    write_fee1_kb_bucket_list_low: 1_000_000,
+                    write_fee1_kb_bucket_list_high: 1_000_000_000,
+                    bucket_list_write_fee_growth_factor: 50,
+                },
+            )),
+            config_entry(ConfigSettingEntry::ContractHistoricalDataV0(
+                ConfigSettingContractHistoricalDataV0 {
+                    fee_historical1_kb: 20,
+                },
+            )),
+            config_entry(ConfigSettingEntry::ContractEventsV0(
+                ConfigSettingContractEventsV0 {
+                    tx_max_contract_events_size_bytes: 21,
+                    fee_contract_events1_kb: 22,
+                },
+            )),
+            config_entry(ConfigSettingEntry::ContractBandwidthV0(
+                ConfigSettingContractBandwidthV0 {
+                    ledger_max_txs_size_bytes: 23,
+                    tx_max_size_bytes: 24,
+                    fee_tx_size1_kb: 25,
+                },
+            )),
+            config_entry(ConfigSettingEntry::StateArchival(StateArchivalSettings {
+                max_entry_ttl: 26,
+                min_temporary_ttl: 27,
+                min_persistent_ttl: 28,
+                persistent_rent_rate_denominator: 29,
+                temp_rent_rate_denominator: 30,
+                max_entries_to_archive: 31,
+                bucket_list_size_window_sample_size: 32,
+                bucket_list_window_sample_period: 33,
+                eviction_scan_size: 34,
+                starting_eviction_scan_level: 35,
+            })),
+            config_entry(ConfigSettingEntry::ContractCostParamsCpuInstructions(
+                cpu_cost_params.clone(),
+            )),
+            config_entry(ConfigSettingEntry::ContractCostParamsMemoryBytes(
+                memory_cost_params.clone(),
+            )),
+            config_entry(ConfigSettingEntry::BucketlistSizeWindow(
+                vec![
+                    150_000_000_000_000 * 2,
+                    150_000_000_000_000,
+                    150_000_000_000_000 / 2,
+                    150_000_000_000_000 / 2,
+                ]
+                .try_into()
+                .unwrap(),
+            )),
+        ],
+        0,
+    )
+    .unwrap();
+
+    let network_config = NetworkConfig::load_from_snapshot(&snapshot_source).unwrap();
+    // From tests/resources `test_compute_write_fee`
+    let write_fee = 1_000_000_000 + 50 * (1_000_000_000_i64 - 1_000_000) / 2;
+    assert_eq!(
+        network_config,
+        NetworkConfig {
+            fee_configuration: FeeConfiguration {
+                fee_per_instruction_increment: 3,
+                fee_per_read_entry: 13,
+                fee_per_write_entry: 14,
+                fee_per_read_1kb: 15,
+                fee_per_write_1kb: write_fee,
+                fee_per_historical_1kb: 20,
+                fee_per_contract_event_1kb: 22,
+                fee_per_transaction_size_1kb: 25,
+            },
+            rent_fee_configuration: RentFeeConfiguration {
+                fee_per_write_1kb: write_fee,
+                fee_per_write_entry: 14,
+                persistent_rent_rate_denominator: 29,
+                temporary_rent_rate_denominator: 30,
+            },
+            tx_max_instructions: 2,
+            tx_memory_limit: 4,
+            cpu_cost_params,
+            memory_cost_params,
+            min_temp_entry_ttl: 27,
+            min_persistent_entry_ttl: 28,
+            max_entry_ttl: 26,
+        }
+    );
+}
+
+#[test]
+fn test_create_budget() {
+    let default_entry = ContractCostParamEntry {
+        ext: ExtensionPoint::V0,
+        const_term: 0,
+        linear_term: 0,
+    };
+    let mut cpu_cost_params = vec![default_entry.clone(); ContractCostType::variants().len()];
+    let mut mem_cost_params = vec![default_entry; ContractCostType::variants().len()];
+    cpu_cost_params[ContractCostType::ComputeKeccak256Hash as usize] = ContractCostParamEntry {
+        ext: ExtensionPoint::V0,
+        const_term: 50,
+        linear_term: 100 << 7,
+    };
+    mem_cost_params[ContractCostType::ComputeKeccak256Hash as usize] = ContractCostParamEntry {
+        ext: ExtensionPoint::V0,
+        const_term: 20,
+        linear_term: 5 << 7,
+    };
+    let config = NetworkConfig {
+        tx_max_instructions: 1000,
+        tx_memory_limit: 500,
+        cpu_cost_params: ContractCostParams(cpu_cost_params.try_into().unwrap()),
+        memory_cost_params: ContractCostParams(mem_cost_params.try_into().unwrap()),
+        ..Default::default()
+    };
+    let budget = config.create_budget().unwrap();
+    assert_eq!(budget.get_cpu_insns_remaining().unwrap(), 1000);
+    assert_eq!(budget.get_mem_bytes_remaining().unwrap(), 500);
+
+    budget
+        .charge(ContractCostType::ComputeKeccak256Hash, Some(5))
+        .unwrap();
+    assert_eq!(budget.get_cpu_insns_consumed().unwrap(), 50 + 5 * 100);
+    assert_eq!(budget.get_mem_bytes_consumed().unwrap(), 20 + 5 * 5);
+}
+
+#[test]
+fn test_fill_ledger_info() {
+    let config = NetworkConfig {
+        min_temp_entry_ttl: 111,
+        min_persistent_entry_ttl: 222,
+        max_entry_ttl: 333,
+        ..Default::default()
+    };
+    let mut ledger_info = LedgerInfo::default();
+    config.fill_config_fields_in_ledger_info(&mut ledger_info);
+    assert_eq!(
+        ledger_info,
+        LedgerInfo {
+            min_temp_entry_ttl: 111,
+            min_persistent_entry_ttl: 222,
+            max_entry_ttl: 333,
+            ..Default::default()
+        }
+    )
+}
diff --git a/soroban-simulation/src/test/simulation.rs b/soroban-simulation/src/test/simulation.rs
new file mode 100644
index 00000000..e1f98afe
--- /dev/null
+++ b/soroban-simulation/src/test/simulation.rs
@@ -0,0 +1,854 @@
+use crate::simulation::{
+    simulate_extend_ttl_op, simulate_invoke_host_function_op, simulate_restore_op,
+    ExtendTtlOpSimulationResult, LedgerEntryDiff, RestoreOpSimulationResult,
+    SimulationAdjustmentConfig, SimulationAdjustmentFactor,
+};
+use crate::testutils::{ledger_entry_to_ledger_key, temp_entry, MockSnapshotSource};
+use crate::NetworkConfig;
+use pretty_assertions::assert_eq;
+use soroban_env_host::e2e_testutils::{
+    account_entry, auth_contract_invocation, bytes_sc_val, create_contract_auth,
+    default_ledger_info, get_account_id, get_contract_id_preimage, get_wasm_hash, get_wasm_key,
+    ledger_entry, upload_wasm_host_fn, wasm_entry, AuthContractInvocationNode, CreateContractData,
+};
+use soroban_env_host::fees::{FeeConfiguration, RentFeeConfiguration};
+use soroban_env_host::xdr::{
+    ContractCostParamEntry, ContractCostParams, ContractCostType, ContractDataDurability,
+    ContractDataEntry, ExtensionPoint, LedgerEntry, LedgerEntryData, LedgerFootprint, LedgerKey,
+    LedgerKeyContractData, ScAddress, ScErrorCode, ScErrorType, ScNonceKey, ScVal,
+    SorobanAddressCredentials, SorobanAuthorizationEntry, SorobanCredentials, SorobanResources,
+    SorobanTransactionData,
+};
+use soroban_env_host::HostError;
+use soroban_test_wasms::{ADD_I32, AUTH_TEST_CONTRACT};
+use std::rc::Rc;
+use tap::prelude::*;
+
+fn default_network_config() -> NetworkConfig {
+    let default_entry = ContractCostParamEntry {
+        ext: ExtensionPoint::V0,
+        const_term: 0,
+        linear_term: 0,
+    };
+    let mut cpu_cost_params = vec![default_entry.clone(); ContractCostType::variants().len()];
+    let mut mem_cost_params = vec![default_entry; ContractCostType::variants().len()];
+    for i in 0..ContractCostType::variants().len() {
+        let v = i as i64;
+        cpu_cost_params[i].const_term = (v + 1) * 1000;
+        cpu_cost_params[i].linear_term = v << 7;
+        mem_cost_params[i].const_term = (v + 1) * 500;
+        mem_cost_params[i].linear_term = v << 6;
+    }
+    let ledger_info = default_ledger_info();
+
+    NetworkConfig {
+        fee_configuration: FeeConfiguration {
+            fee_per_instruction_increment: 10,
+            fee_per_read_entry: 20,
+            fee_per_write_entry: 30,
+            fee_per_read_1kb: 40,
+            fee_per_write_1kb: 50,
+            fee_per_historical_1kb: 60,
+            fee_per_contract_event_1kb: 70,
+            fee_per_transaction_size_1kb: 80,
+        },
+        rent_fee_configuration: RentFeeConfiguration {
+            fee_per_write_1kb: 50,
+            fee_per_write_entry: 30,
+            persistent_rent_rate_denominator: 100,
+            temporary_rent_rate_denominator: 1000,
+        },
+        tx_max_instructions: 100_000_000,
+        tx_memory_limit: 40_000_000,
+        cpu_cost_params: ContractCostParams(cpu_cost_params.try_into().unwrap()),
+        memory_cost_params: ContractCostParams(mem_cost_params.try_into().unwrap()),
+        min_temp_entry_ttl: ledger_info.min_temp_entry_ttl,
+        min_persistent_entry_ttl: ledger_info.min_persistent_entry_ttl,
+        max_entry_ttl: ledger_info.max_entry_ttl,
+    }
+}
+
+fn test_adjustment_config() -> SimulationAdjustmentConfig {
+    SimulationAdjustmentConfig {
+        instructions: SimulationAdjustmentFactor::new(1.1, 100_000),
+        read_bytes: SimulationAdjustmentFactor::new(1.2, 500),
+        write_bytes: SimulationAdjustmentFactor::new(1.3, 300),
+        tx_size: SimulationAdjustmentFactor::new(1.4, 1000),
+        refundable_fee: SimulationAdjustmentFactor::new(1.5, 100_000),
+    }
+}
+
+fn nonce_key(address: ScAddress, nonce: i64) -> LedgerKey {
+    LedgerKey::ContractData(LedgerKeyContractData {
+        contract: address,
+        key: ScVal::LedgerKeyNonce(ScNonceKey { nonce }),
+        durability: ContractDataDurability::Temporary,
+    })
+}
+
+fn nonce_entry(address: ScAddress, nonce: i64) -> LedgerEntry {
+    ledger_entry(LedgerEntryData::ContractData(ContractDataEntry {
+        ext: ExtensionPoint::V0,
+        contract: address,
+        key: ScVal::LedgerKeyNonce(ScNonceKey { nonce }),
+        durability: ContractDataDurability::Temporary,
+        val: ScVal::Void,
+    }))
+}
+
+#[test]
+fn test_simulate_upload_wasm() {
+    let source_account = get_account_id([123; 32]);
+    let ledger_info = default_ledger_info();
+    let network_config = default_network_config();
+    let snapshot_source =
+        Rc::new(MockSnapshotSource::from_entries(vec![], ledger_info.sequence_number).unwrap());
+
+    let res = simulate_invoke_host_function_op(
+        snapshot_source.clone(),
+        &network_config,
+        &SimulationAdjustmentConfig::no_adjustments(),
+        &ledger_info,
+        upload_wasm_host_fn(ADD_I32),
+        None,
+        &source_account,
+        [1; 32],
+        true,
+    )
+    .unwrap();
+    assert_eq!(
+        res.invoke_result.unwrap(),
+        bytes_sc_val(&get_wasm_hash(ADD_I32))
+    );
+
+    assert_eq!(res.auth, vec![]);
+    assert!(res.contract_events.is_empty());
+    assert!(res.diagnostic_events.is_empty());
+
+    assert_eq!(
+        res.transaction_data,
+        Some(SorobanTransactionData {
+            ext: ExtensionPoint::V0,
+            resources: SorobanResources {
+                footprint: LedgerFootprint {
+                    read_only: Default::default(),
+                    read_write: vec![get_wasm_key(ADD_I32)].try_into().unwrap()
+                },
+                instructions: 1505493,
+                read_bytes: 0,
+                write_bytes: 636,
+            },
+            resource_fee: 33063,
+        })
+    );
+    assert_eq!(res.simulated_instructions, 1505493);
+    assert_eq!(res.simulated_memory, 752745);
+    assert_eq!(
+        res.modified_entries,
+        vec![LedgerEntryDiff {
+            state_before: None,
+            state_after: Some(wasm_entry(ADD_I32))
+        }]
+    );
+
+    let res_with_adjustments = simulate_invoke_host_function_op(
+        snapshot_source,
+        &network_config,
+        &test_adjustment_config(),
+        &ledger_info,
+        upload_wasm_host_fn(ADD_I32),
+        None,
+        &source_account,
+        [1; 32],
+        true,
+    )
+    .unwrap();
+    assert_eq!(
+        res_with_adjustments.invoke_result.unwrap(),
+        bytes_sc_val(&get_wasm_hash(ADD_I32))
+    );
+
+    assert_eq!(res_with_adjustments.auth, res.auth);
+    assert_eq!(res_with_adjustments.contract_events, res.contract_events);
+    assert_eq!(
+        res_with_adjustments.diagnostic_events,
+        res.diagnostic_events
+    );
+    assert_eq!(
+        res_with_adjustments.simulated_instructions,
+        res.simulated_instructions
+    );
+    assert_eq!(res_with_adjustments.simulated_memory, res.simulated_memory);
+    assert_eq!(
+        res_with_adjustments.transaction_data,
+        Some(SorobanTransactionData {
+            ext: ExtensionPoint::V0,
+            resources: SorobanResources {
+                footprint: LedgerFootprint {
+                    read_only: Default::default(),
+                    read_write: vec![get_wasm_key(ADD_I32)].try_into().unwrap()
+                },
+                instructions: (1505493.0 * 1.1) as u32,
+                read_bytes: 0,
+                write_bytes: 636 + 300,
+            },
+            resource_fee: 133367,
+        })
+    );
+}
+
+#[test]
+fn test_simulation_returns_insufficient_budget_error() {
+    let source_account = get_account_id([123; 32]);
+    let ledger_info = default_ledger_info();
+    let mut network_config = default_network_config();
+    network_config.tx_max_instructions = 100_000;
+    let snapshot_source =
+        Rc::new(MockSnapshotSource::from_entries(vec![], ledger_info.sequence_number).unwrap());
+
+    let res = simulate_invoke_host_function_op(
+        snapshot_source.clone(),
+        &network_config,
+        &SimulationAdjustmentConfig::no_adjustments(),
+        &ledger_info,
+        upload_wasm_host_fn(ADD_I32),
+        None,
+        &source_account,
+        [1; 32],
+        true,
+    )
+    .unwrap();
+    assert!(HostError::result_matches_err(
+        res.invoke_result,
+        (ScErrorType::Budget, ScErrorCode::ExceededLimit)
+    ));
+    assert_eq!(res.auth, vec![]);
+    assert!(res.contract_events.is_empty());
+    assert!(res.diagnostic_events.is_empty());
+
+    assert_eq!(res.transaction_data, None);
+    assert_eq!(res.simulated_instructions, 111516);
+    assert_eq!(res.simulated_memory, 45006);
+    assert_eq!(res.modified_entries, vec![]);
+}
+
+#[test]
+fn test_simulation_returns_logic_error() {
+    let source_account = get_account_id([123; 32]);
+    let ledger_info = default_ledger_info();
+    let network_config = default_network_config();
+    let snapshot_source =
+        Rc::new(MockSnapshotSource::from_entries(vec![], ledger_info.sequence_number).unwrap());
+    let bad_wasm = [0; 1000];
+
+    let res = simulate_invoke_host_function_op(
+        snapshot_source.clone(),
+        &network_config,
+        &SimulationAdjustmentConfig::no_adjustments(),
+        &ledger_info,
+        upload_wasm_host_fn(&bad_wasm),
+        None,
+        &source_account,
+        [1; 32],
+        true,
+    )
+    .unwrap();
+    assert!(HostError::result_matches_err(
+        res.invoke_result,
+        (ScErrorType::WasmVm, ScErrorCode::InvalidAction)
+    ));
+    assert_eq!(res.auth, vec![]);
+    assert!(res.contract_events.is_empty());
+    assert!(!res.diagnostic_events.is_empty());
+
+    assert_eq!(res.transaction_data, None);
+    assert_eq!(res.simulated_instructions, 154568);
+    assert_eq!(res.simulated_memory, 77284);
+    assert_eq!(res.modified_entries, vec![]);
+}
+
+#[test]
+fn test_simulate_create_contract() {
+    let source_account = get_account_id([123; 32]);
+    let ledger_info = default_ledger_info();
+    let network_config = default_network_config();
+    let contract = CreateContractData::new([1; 32], ADD_I32);
+
+    let snapshot_source = Rc::new(
+        MockSnapshotSource::from_entries(
+            vec![(
+                contract.wasm_entry,
+                Some(ledger_info.sequence_number + 1000),
+            )],
+            ledger_info.sequence_number,
+        )
+        .unwrap(),
+    );
+
+    let res = simulate_invoke_host_function_op(
+        snapshot_source.clone(),
+        &network_config,
+        &SimulationAdjustmentConfig::no_adjustments(),
+        &ledger_info,
+        contract.host_fn.clone(),
+        None,
+        &source_account,
+        [1; 32],
+        true,
+    )
+    .unwrap();
+    assert_eq!(
+        res.invoke_result.unwrap(),
+        ScVal::Address(contract.contract_address)
+    );
+
+    assert_eq!(
+        res.auth,
+        vec![create_contract_auth(
+            &get_contract_id_preimage(&contract.deployer, &[1; 32]),
+            ADD_I32,
+        )]
+    );
+    assert!(res.contract_events.is_empty());
+    assert!(res.diagnostic_events.is_empty());
+
+    assert_eq!(
+        res.transaction_data,
+        Some(SorobanTransactionData {
+            ext: ExtensionPoint::V0,
+            resources: SorobanResources {
+                footprint: LedgerFootprint {
+                    read_only: vec![contract.wasm_key.clone()].try_into().unwrap(),
+                    read_write: vec![contract.contract_key.clone()].try_into().unwrap()
+                },
+                instructions: 929703,
+                read_bytes: 636,
+                write_bytes: 104,
+            },
+            resource_fee: 6490,
+        })
+    );
+    assert_eq!(res.simulated_instructions, 929703);
+    assert_eq!(res.simulated_memory, 464846);
+    assert_eq!(
+        res.modified_entries,
+        vec![LedgerEntryDiff {
+            state_before: None,
+            state_after: Some(contract.contract_entry)
+        }]
+    );
+}
+
+#[test]
+fn test_simulate_invoke_contract_with_auth() {
+    let contracts = vec![
+        CreateContractData::new([1; 32], AUTH_TEST_CONTRACT),
+        CreateContractData::new([2; 32], AUTH_TEST_CONTRACT),
+        CreateContractData::new([3; 32], AUTH_TEST_CONTRACT),
+        CreateContractData::new([4; 32], AUTH_TEST_CONTRACT),
+    ];
+
+    let tree = AuthContractInvocationNode {
+        address: contracts[0].contract_address.clone(),
+        children: vec![
+            AuthContractInvocationNode {
+                address: contracts[1].contract_address.clone(),
+                children: vec![AuthContractInvocationNode {
+                    address: contracts[2].contract_address.clone(),
+                    children: vec![AuthContractInvocationNode {
+                        address: contracts[3].contract_address.clone(),
+                        children: vec![],
+                    }],
+                }],
+            },
+            AuthContractInvocationNode {
+                address: contracts[2].contract_address.clone(),
+                children: vec![
+                    AuthContractInvocationNode {
+                        address: contracts[1].contract_address.clone(),
+                        children: vec![],
+                    },
+                    AuthContractInvocationNode {
+                        address: contracts[3].contract_address.clone(),
+                        children: vec![],
+                    },
+                ],
+            },
+        ],
+    };
+    let source_account = get_account_id([123; 32]);
+    let other_account = get_account_id([124; 32]);
+    let host_fn = auth_contract_invocation(
+        vec![
+            ScAddress::Account(source_account.clone()),
+            ScAddress::Account(other_account.clone()),
+        ],
+        tree.clone(),
+    );
+    let ledger_info = default_ledger_info();
+    let network_config = default_network_config();
+    let snapshot_source = Rc::new(
+        MockSnapshotSource::from_entries(
+            vec![
+                (
+                    contracts[0].wasm_entry.clone(),
+                    Some(ledger_info.sequence_number + 100),
+                ),
+                (
+                    contracts[0].contract_entry.clone(),
+                    Some(ledger_info.sequence_number + 1000),
+                ),
+                (
+                    contracts[1].contract_entry.clone(),
+                    Some(ledger_info.sequence_number + 1000),
+                ),
+                (
+                    contracts[2].contract_entry.clone(),
+                    Some(ledger_info.sequence_number + 1000),
+                ),
+                (
+                    contracts[3].contract_entry.clone(),
+                    Some(ledger_info.sequence_number + 1000),
+                ),
+                // Source account doesn't need to be accessed
+                (account_entry(&other_account), None),
+            ],
+            ledger_info.sequence_number,
+        )
+        .unwrap(),
+    );
+
+    let res = simulate_invoke_host_function_op(
+        snapshot_source,
+        &network_config,
+        &SimulationAdjustmentConfig::no_adjustments(),
+        &ledger_info,
+        host_fn,
+        None,
+        &source_account,
+        [1; 32],
+        true,
+    )
+    .unwrap();
+    assert_eq!(res.invoke_result.unwrap(), ScVal::Void);
+
+    let other_account_address = ScAddress::Account(other_account.clone());
+    // This value is stable thanks to hardcoded RNG seed.
+    let other_account_nonce = 1039859045797838027;
+    let expected_auth_tree = tree.into_authorized_invocation();
+    assert_eq!(
+        res.auth,
+        vec![
+            SorobanAuthorizationEntry {
+                credentials: SorobanCredentials::SourceAccount,
+                root_invocation: expected_auth_tree.clone(),
+            },
+            SorobanAuthorizationEntry {
+                credentials: SorobanCredentials::Address(SorobanAddressCredentials {
+                    address: other_account_address.clone(),
+                    nonce: other_account_nonce,
+                    signature_expiration_ledger: 0,
+                    signature: ScVal::Void,
+                }),
+                root_invocation: expected_auth_tree,
+            }
+        ]
+    );
+    assert!(res.contract_events.is_empty());
+    assert!(!res.diagnostic_events.is_empty());
+
+    assert_eq!(
+        res.transaction_data,
+        Some(SorobanTransactionData {
+            ext: ExtensionPoint::V0,
+            resources: SorobanResources {
+                footprint: LedgerFootprint {
+                    read_only: vec![
+                        ledger_entry_to_ledger_key(&account_entry(&other_account)).unwrap(),
+                        contracts[0].contract_key.clone(),
+                        contracts[1].contract_key.clone(),
+                        contracts[2].contract_key.clone(),
+                        contracts[3].contract_key.clone(),
+                        contracts[0].wasm_key.clone(),
+                    ]
+                    .tap_mut(|v| v.sort())
+                    .try_into()
+                    .unwrap(),
+                    read_write: vec![nonce_key(
+                        other_account_address.clone(),
+                        other_account_nonce
+                    )]
+                    .try_into()
+                    .unwrap()
+                },
+                instructions: 38139496,
+                read_bytes: 7440,
+                write_bytes: 76,
+            },
+            resource_fee: 39290,
+        })
+    );
+    assert_eq!(res.simulated_instructions, 38139496);
+    assert_eq!(res.simulated_memory, 19069620);
+    assert_eq!(
+        res.modified_entries,
+        vec![LedgerEntryDiff {
+            state_before: None,
+            state_after: Some(nonce_entry(other_account_address, other_account_nonce))
+        }]
+    );
+}
+
+#[test]
+fn test_simulate_extend_ttl_op() {
+    let ledger_info = default_ledger_info();
+    let network_config = default_network_config();
+    let contract_entry = CreateContractData::new([111; 32], ADD_I32).contract_entry;
+    let entries = vec![
+        (
+            wasm_entry(ADD_I32),
+            Some(ledger_info.sequence_number + 100_000),
+        ),
+        (
+            wasm_entry(AUTH_TEST_CONTRACT),
+            Some(ledger_info.sequence_number + 100),
+        ),
+        (contract_entry, Some(ledger_info.sequence_number + 500_000)),
+        (
+            wasm_entry(b"123"),
+            Some(ledger_info.sequence_number + 1_000_000),
+        ),
+        (temp_entry(b"321"), Some(ledger_info.sequence_number + 100)),
+        (
+            temp_entry(b"123"),
+            Some(ledger_info.sequence_number + 100_000),
+        ),
+        (
+            temp_entry(b"456"),
+            Some(ledger_info.sequence_number + 1_000_000),
+        ),
+    ];
+    let mut keys: Vec<LedgerKey> = entries
+        .iter()
+        .map(|e| ledger_entry_to_ledger_key(&e.0).unwrap())
+        .collect();
+    let snapshot_source =
+        MockSnapshotSource::from_entries(entries, ledger_info.sequence_number).unwrap();
+
+    let no_op_extension = simulate_extend_ttl_op(
+        &snapshot_source,
+        &network_config,
+        &SimulationAdjustmentConfig::no_adjustments(),
+        &ledger_info,
+        &keys,
+        100,
+    )
+    .unwrap();
+    assert_eq!(
+        no_op_extension,
+        ExtendTtlOpSimulationResult {
+            transaction_data: SorobanTransactionData {
+                ext: ExtensionPoint::V0,
+                resources: SorobanResources {
+                    footprint: LedgerFootprint {
+                        read_only: Default::default(),
+                        read_write: Default::default()
+                    },
+                    instructions: 0,
+                    read_bytes: 0,
+                    write_bytes: 0,
+                },
+                resource_fee: 280,
+            }
+        }
+    );
+
+    let extension_for_some_entries = simulate_extend_ttl_op(
+        &snapshot_source,
+        &network_config,
+        &SimulationAdjustmentConfig::no_adjustments(),
+        &ledger_info,
+        &keys,
+        100_001,
+    )
+    .unwrap();
+    assert_eq!(
+        extension_for_some_entries,
+        ExtendTtlOpSimulationResult {
+            transaction_data: SorobanTransactionData {
+                ext: ExtensionPoint::V0,
+                resources: SorobanResources {
+                    footprint: LedgerFootprint {
+                        read_only: vec![
+                            keys[0].clone(),
+                            keys[1].clone(),
+                            keys[4].clone(),
+                            keys[5].clone(),
+                        ]
+                        .tap_mut(|v| v.sort())
+                        .try_into()
+                        .unwrap(),
+                        read_write: Default::default()
+                    },
+                    instructions: 0,
+                    read_bytes: 7712,
+                    write_bytes: 0,
+                },
+                resource_fee: 339313,
+            }
+        }
+    );
+
+    let extension_for_all_entries = simulate_extend_ttl_op(
+        &snapshot_source,
+        &network_config,
+        &SimulationAdjustmentConfig::no_adjustments(),
+        &ledger_info,
+        &keys,
+        1_000_001,
+    )
+    .unwrap();
+    assert_eq!(
+        extension_for_all_entries,
+        ExtendTtlOpSimulationResult {
+            transaction_data: SorobanTransactionData {
+                ext: ExtensionPoint::V0,
+                resources: SorobanResources {
+                    footprint: LedgerFootprint {
+                        read_only: keys.clone().tap_mut(|v| v.sort()).try_into().unwrap(),
+                        read_write: Default::default()
+                    },
+                    instructions: 0,
+                    read_bytes: 7944,
+                    write_bytes: 0,
+                },
+                resource_fee: 3697000,
+            }
+        }
+    );
+
+    // Non-existent entry should be just skipped.
+    keys.push(get_wasm_key(b"abc"));
+    let extension_for_all_entries_with_non_existent = simulate_extend_ttl_op(
+        &snapshot_source,
+        &network_config,
+        &SimulationAdjustmentConfig::no_adjustments(),
+        &ledger_info,
+        &keys,
+        1_000_001,
+    )
+    .unwrap();
+    assert_eq!(
+        extension_for_all_entries,
+        extension_for_all_entries_with_non_existent
+    );
+
+    let extension_for_all_entries_with_adjustment = simulate_extend_ttl_op(
+        &snapshot_source,
+        &network_config,
+        &test_adjustment_config(),
+        &ledger_info,
+        &keys,
+        1_000_001,
+    )
+    .unwrap();
+    assert_eq!(
+        extension_for_all_entries_with_adjustment,
+        ExtendTtlOpSimulationResult {
+            transaction_data: SorobanTransactionData {
+                ext: ExtensionPoint::V0,
+                resources: SorobanResources {
+                    footprint: extension_for_all_entries
+                        .transaction_data
+                        .resources
+                        .footprint,
+                    instructions: 0,
+                    read_bytes: (7944.0 * 1.2) as u32,
+                    write_bytes: 0,
+                },
+                resource_fee: 5545310,
+            }
+        }
+    );
+}
+
+#[test]
+fn test_simulate_restore_op() {
+    let mut ledger_info = default_ledger_info();
+    let network_config = default_network_config();
+    let contract_entry = CreateContractData::new([111; 32], ADD_I32).contract_entry;
+    let entries = vec![
+        (
+            wasm_entry(ADD_I32),
+            Some(ledger_info.sequence_number + 100_000),
+        ),
+        (
+            wasm_entry(AUTH_TEST_CONTRACT),
+            Some(ledger_info.sequence_number + 100),
+        ),
+        (contract_entry, Some(ledger_info.sequence_number + 500_000)),
+        (
+            wasm_entry(b"123"),
+            Some(ledger_info.sequence_number + 1_000_000),
+        ),
+    ];
+    let keys: Vec<LedgerKey> = entries
+        .iter()
+        .map(|e| ledger_entry_to_ledger_key(&e.0).unwrap())
+        .collect();
+    let snapshot_source =
+        MockSnapshotSource::from_entries(entries, ledger_info.sequence_number).unwrap();
+
+    let no_op_restoration = simulate_restore_op(
+        &snapshot_source,
+        &network_config,
+        &SimulationAdjustmentConfig::no_adjustments(),
+        &ledger_info,
+        &keys,
+    )
+    .unwrap();
+    assert_eq!(
+        no_op_restoration,
+        RestoreOpSimulationResult {
+            transaction_data: SorobanTransactionData {
+                ext: ExtensionPoint::V0,
+                resources: SorobanResources {
+                    footprint: LedgerFootprint {
+                        read_only: Default::default(),
+                        read_write: Default::default()
+                    },
+                    instructions: 0,
+                    read_bytes: 0,
+                    write_bytes: 0,
+                },
+                resource_fee: 279,
+            }
+        }
+    );
+
+    let init_seq_num = ledger_info.sequence_number;
+    ledger_info.sequence_number = init_seq_num + 100_001;
+    let restoration_for_some_entries = simulate_restore_op(
+        &snapshot_source,
+        &network_config,
+        &SimulationAdjustmentConfig::no_adjustments(),
+        &ledger_info,
+        &keys,
+    )
+    .unwrap();
+    assert_eq!(
+        restoration_for_some_entries,
+        RestoreOpSimulationResult {
+            transaction_data: SorobanTransactionData {
+                ext: ExtensionPoint::V0,
+                resources: SorobanResources {
+                    footprint: LedgerFootprint {
+                        read_only: Default::default(),
+                        read_write: vec![keys[0].clone(), keys[1].clone(),]
+                            .tap_mut(|v| v.sort())
+                            .try_into()
+                            .unwrap()
+                    },
+                    instructions: 0,
+                    read_bytes: 7568,
+                    write_bytes: 7568,
+                },
+                resource_fee: 370692,
+            }
+        }
+    );
+
+    ledger_info.sequence_number = init_seq_num + 1_000_001;
+    let extension_for_all_entries = simulate_restore_op(
+        &snapshot_source,
+        &network_config,
+        &SimulationAdjustmentConfig::no_adjustments(),
+        &ledger_info,
+        &keys,
+    )
+    .unwrap();
+    assert_eq!(
+        extension_for_all_entries,
+        RestoreOpSimulationResult {
+            transaction_data: SorobanTransactionData {
+                ext: ExtensionPoint::V0,
+                resources: SorobanResources {
+                    footprint: LedgerFootprint {
+                        read_only: Default::default(),
+                        read_write: keys.clone().tap_mut(|v| v.sort()).try_into().unwrap()
+                    },
+                    instructions: 0,
+                    read_bytes: 7728,
+                    write_bytes: 7728,
+                },
+                resource_fee: 378736,
+            }
+        }
+    );
+
+    let extension_for_all_entries_with_adjustment = simulate_restore_op(
+        &snapshot_source,
+        &network_config,
+        &test_adjustment_config(),
+        &ledger_info,
+        &keys,
+    )
+    .unwrap();
+    assert_eq!(
+        extension_for_all_entries_with_adjustment,
+        RestoreOpSimulationResult {
+            transaction_data: SorobanTransactionData {
+                ext: ExtensionPoint::V0,
+                resources: SorobanResources {
+                    footprint: LedgerFootprint {
+                        read_only: Default::default(),
+                        read_write: keys.clone().tap_mut(|v| v.sort()).try_into().unwrap()
+                    },
+                    instructions: 0,
+                    read_bytes: (7728.0 * 1.2) as u32,
+                    write_bytes: (7728.0 * 1.3) as u32,
+                },
+                resource_fee: 567785,
+            }
+        }
+    );
+}
+
+#[test]
+fn test_simulate_restore_op_returns_error_for_temp_entries() {
+    let ledger_info = default_ledger_info();
+    let network_config = default_network_config();
+
+    let snapshot_source = MockSnapshotSource::from_entries(
+        vec![(temp_entry(b"123"), Some(ledger_info.sequence_number - 10))],
+        ledger_info.sequence_number,
+    )
+    .unwrap();
+
+    let res = simulate_restore_op(
+        &snapshot_source,
+        &network_config,
+        &SimulationAdjustmentConfig::no_adjustments(),
+        &ledger_info,
+        &[ledger_entry_to_ledger_key(&temp_entry(b"123")).unwrap()],
+    );
+    assert!(res.is_err());
+}
+
+#[test]
+fn test_simulate_restore_op_returns_error_for_non_existent_entry() {
+    let ledger_info = default_ledger_info();
+    let network_config = default_network_config();
+
+    let snapshot_source =
+        MockSnapshotSource::from_entries(vec![], ledger_info.sequence_number).unwrap();
+
+    let res = simulate_restore_op(
+        &snapshot_source,
+        &network_config,
+        &SimulationAdjustmentConfig::no_adjustments(),
+        &ledger_info,
+        &[get_wasm_key(b"123")],
+    );
+    assert!(res.is_err());
+}
diff --git a/soroban-simulation/src/test/snapshot_source.rs b/soroban-simulation/src/test/snapshot_source.rs
new file mode 100644
index 00000000..6968694d
--- /dev/null
+++ b/soroban-simulation/src/test/snapshot_source.rs
@@ -0,0 +1,178 @@
+use crate::network_config::NetworkConfig;
+use crate::simulation::{RestoreOpSimulationResult, SimulationAdjustmentConfig};
+use crate::snapshot_source::AutoRestoringSnapshotSource;
+use crate::testutils::{ledger_entry_to_ledger_key, temp_entry, MockSnapshotSource};
+use pretty_assertions::assert_eq;
+use soroban_env_host::e2e_testutils::wasm_entry;
+use soroban_env_host::fees::{FeeConfiguration, RentFeeConfiguration};
+use soroban_env_host::storage::SnapshotSource;
+use soroban_env_host::xdr::{
+    ExtensionPoint, LedgerFootprint, SorobanResources, SorobanTransactionData,
+};
+use soroban_env_host::LedgerInfo;
+use std::rc::Rc;
+
+#[test]
+fn test_automatic_restoration() {
+    let ledger_seq = 300;
+    let snapshot = Rc::new(
+        MockSnapshotSource::from_entries(
+            vec![
+                (wasm_entry(b"1"), Some(100)), // persistent, expired
+                (wasm_entry(b"2"), Some(299)), // persistent, expired
+                (wasm_entry(b"3"), Some(300)), // persistent, live
+                (wasm_entry(b"4"), Some(400)), // persistent, live
+                (temp_entry(b"5"), Some(299)), // temp, removed
+                (temp_entry(b"6"), Some(300)), // temp, live
+                (temp_entry(b"7"), Some(400)), // temp, live
+            ],
+            ledger_seq,
+        )
+        .unwrap(),
+    );
+    let ledger_info = LedgerInfo {
+        sequence_number: ledger_seq,
+        min_persistent_entry_ttl: 1000,
+        ..Default::default()
+    };
+    let network_config = NetworkConfig {
+        fee_configuration: FeeConfiguration {
+            fee_per_instruction_increment: 8000,
+            fee_per_read_entry: 500,
+            fee_per_write_entry: 1000,
+            fee_per_read_1kb: 30,
+            fee_per_write_1kb: 60,
+            fee_per_historical_1kb: 200,
+            fee_per_contract_event_1kb: 500,
+            fee_per_transaction_size_1kb: 300,
+        },
+        rent_fee_configuration: RentFeeConfiguration {
+            fee_per_write_1kb: 5000,
+            fee_per_write_entry: 1000,
+            persistent_rent_rate_denominator: 10,
+            temporary_rent_rate_denominator: 100,
+        },
+        ..Default::default()
+    };
+    let auto_restoring_snapshot = AutoRestoringSnapshotSource::new(snapshot, &ledger_info).unwrap();
+    assert_eq!(
+        auto_restoring_snapshot
+            .simulate_restore_keys_op(
+                &network_config,
+                &SimulationAdjustmentConfig::no_adjustments(),
+                &ledger_info
+            )
+            .unwrap(),
+        None
+    );
+
+    let restored_entry_expiration = ledger_seq + 1000 - 1;
+
+    assert_eq!(
+        auto_restoring_snapshot
+            .get(&Rc::new(
+                ledger_entry_to_ledger_key(&wasm_entry(b"1111")).unwrap()
+            ))
+            .unwrap(),
+        None
+    );
+    assert_eq!(
+        auto_restoring_snapshot
+            .get(&Rc::new(
+                ledger_entry_to_ledger_key(&wasm_entry(b"1")).unwrap()
+            ))
+            .unwrap(),
+        Some((Rc::new(wasm_entry(b"1")), Some(restored_entry_expiration)))
+    );
+    assert_eq!(
+        auto_restoring_snapshot
+            .get(&Rc::new(
+                ledger_entry_to_ledger_key(&wasm_entry(b"2")).unwrap()
+            ))
+            .unwrap(),
+        Some((Rc::new(wasm_entry(b"2")), Some(restored_entry_expiration)))
+    );
+    assert_eq!(
+        auto_restoring_snapshot
+            .get(&Rc::new(
+                ledger_entry_to_ledger_key(&wasm_entry(b"3")).unwrap()
+            ))
+            .unwrap(),
+        Some((Rc::new(wasm_entry(b"3")), Some(300)))
+    );
+    assert_eq!(
+        auto_restoring_snapshot
+            .get(&Rc::new(
+                ledger_entry_to_ledger_key(&wasm_entry(b"4")).unwrap()
+            ))
+            .unwrap(),
+        Some((Rc::new(wasm_entry(b"4")), Some(400)))
+    );
+
+    assert_eq!(
+        auto_restoring_snapshot
+            .get(&Rc::new(
+                ledger_entry_to_ledger_key(&temp_entry(b"5")).unwrap()
+            ))
+            .unwrap(),
+        None
+    );
+    assert_eq!(
+        auto_restoring_snapshot
+            .get(&Rc::new(
+                ledger_entry_to_ledger_key(&temp_entry(b"6")).unwrap()
+            ))
+            .unwrap(),
+        Some((Rc::new(temp_entry(b"6")), Some(300)))
+    );
+    assert_eq!(
+        auto_restoring_snapshot
+            .get(&Rc::new(
+                ledger_entry_to_ledger_key(&temp_entry(b"7")).unwrap()
+            ))
+            .unwrap(),
+        Some((Rc::new(temp_entry(b"7")), Some(400)))
+    );
+
+    assert_eq!(
+        auto_restoring_snapshot
+            .simulate_restore_keys_op(
+                &network_config,
+                &SimulationAdjustmentConfig::no_adjustments(),
+                &ledger_info
+            )
+            .unwrap(),
+        Some(RestoreOpSimulationResult {
+            transaction_data: SorobanTransactionData {
+                ext: ExtensionPoint::V0,
+                resources: SorobanResources {
+                    footprint: LedgerFootprint {
+                        read_only: Default::default(),
+                        read_write: vec![
+                            ledger_entry_to_ledger_key(&wasm_entry(b"1")).unwrap(),
+                            ledger_entry_to_ledger_key(&wasm_entry(b"2")).unwrap(),
+                        ]
+                        .try_into()
+                        .unwrap()
+                    },
+                    instructions: 0,
+                    read_bytes: 112,
+                    write_bytes: 112,
+                },
+                resource_fee: 62192,
+            }
+        })
+    );
+
+    auto_restoring_snapshot.reset_restored_keys();
+    assert_eq!(
+        auto_restoring_snapshot
+            .simulate_restore_keys_op(
+                &network_config,
+                &SimulationAdjustmentConfig::no_adjustments(),
+                &ledger_info
+            )
+            .unwrap(),
+        None
+    );
+}
diff --git a/soroban-simulation/src/testutils.rs b/soroban-simulation/src/testutils.rs
new file mode 100644
index 00000000..d189ec75
--- /dev/null
+++ b/soroban-simulation/src/testutils.rs
@@ -0,0 +1,105 @@
+use crate::snapshot_source::SnapshotSourceWithArchive;
+use anyhow::{bail, Result};
+use soroban_env_host::{
+    e2e_testutils::ledger_entry,
+    storage::{EntryWithLiveUntil, SnapshotSource},
+    xdr::{
+        ContractDataDurability, ContractDataEntry, ExtensionPoint, Hash, LedgerEntry,
+        LedgerEntryData, LedgerKey, LedgerKeyAccount, LedgerKeyConfigSetting,
+        LedgerKeyContractCode, LedgerKeyContractData, LedgerKeyTrustLine, ScAddress, ScBytes,
+        ScErrorCode, ScErrorType, ScVal,
+    },
+    HostError,
+};
+use std::collections::BTreeMap;
+use std::rc::Rc;
+
+pub struct MockSnapshotSource {
+    map: BTreeMap<Rc<LedgerKey>, EntryWithLiveUntil>,
+    current_ledger_seq: u32,
+}
+
+impl MockSnapshotSource {
+    pub fn from_entries(
+        entries: Vec<(LedgerEntry, Option<u32>)>,
+        current_ledger_seq: u32,
+    ) -> Result<Self> {
+        let mut map = BTreeMap::<Rc<LedgerKey>, (Rc<LedgerEntry>, Option<u32>)>::new();
+        for (e, maybe_ttl) in entries {
+            let key = Rc::new(ledger_entry_to_ledger_key(&e)?);
+            map.insert(key, (Rc::new(e), maybe_ttl));
+        }
+        Ok(Self {
+            map,
+            current_ledger_seq,
+        })
+    }
+}
+
+pub fn ledger_entry_to_ledger_key(entry: &LedgerEntry) -> Result<LedgerKey> {
+    match &entry.data {
+        LedgerEntryData::Account(a) => Ok(LedgerKey::Account(LedgerKeyAccount {
+            account_id: a.account_id.clone(),
+        })),
+        LedgerEntryData::Trustline(tl) => Ok(LedgerKey::Trustline(LedgerKeyTrustLine {
+            account_id: tl.account_id.clone(),
+            asset: tl.asset.clone(),
+        })),
+        LedgerEntryData::ContractData(cd) => Ok(LedgerKey::ContractData(LedgerKeyContractData {
+            contract: cd.contract.clone(),
+            key: cd.key.clone(),
+            durability: cd.durability,
+        })),
+        LedgerEntryData::ContractCode(code) => Ok(LedgerKey::ContractCode(LedgerKeyContractCode {
+            hash: code.hash.clone(),
+        })),
+        LedgerEntryData::ConfigSetting(cs) => {
+            Ok(LedgerKey::ConfigSetting(LedgerKeyConfigSetting {
+                config_setting_id: cs.discriminant(),
+            }))
+        }
+        _ => bail!("ledger entry type is not supported: {entry:#?}"),
+    }
+}
+
+impl SnapshotSourceWithArchive for MockSnapshotSource {
+    fn get_including_archived(
+        &self,
+        key: &Rc<LedgerKey>,
+    ) -> std::result::Result<Option<EntryWithLiveUntil>, HostError> {
+        if let Some((entry, live_until)) = self.map.get(key) {
+            Ok(Some((entry.clone(), *live_until)))
+        } else {
+            Ok(None)
+        }
+    }
+}
+
+impl SnapshotSource for MockSnapshotSource {
+    fn get(
+        &self,
+        key: &Rc<LedgerKey>,
+    ) -> std::result::Result<Option<EntryWithLiveUntil>, HostError> {
+        if let Some((entry, live_until)) = self.map.get(key) {
+            if let Some(live_until) = live_until {
+                if *live_until < self.current_ledger_seq {
+                    return Err((ScErrorType::Storage, ScErrorCode::InternalError).into());
+                }
+            }
+
+            Ok(Some((entry.clone(), *live_until)))
+        } else {
+            Ok(None)
+        }
+    }
+}
+
+pub fn temp_entry(key: &[u8]) -> LedgerEntry {
+    ledger_entry(LedgerEntryData::ContractData(ContractDataEntry {
+        ext: ExtensionPoint::V0,
+        contract: ScAddress::Contract(Hash([0; 32])),
+        key: ScVal::Bytes(ScBytes(key.try_into().unwrap())),
+        durability: ContractDataDurability::Temporary,
+        val: ScVal::Void,
+    }))
+}