Repository: stellar/rs-soroban-env
Pull request #1192 (2023-11-09 22:23:08)

Add tests that cover fees for each individual resource.
### What

Add tests that cover fees for each individual resource.

Resolves https://github.com/stellar/rs-soroban-env/issues/1165

### Why

Improving test coverage.

### Known limitations

N/A



Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 973edc9a..2e704476 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1484,17 +1484,19 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
 
 [[package]]
 name = "stellar-strkey"
-version = "0.0.7"
-source = "git+https://github.com/stellar/rs-stellar-strkey?rev=e6ba45c60c16de28c7522586b80ed0150157df73#e6ba45c60c16de28c7522586b80ed0150157df73"
+version = "0.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12d2bf45e114117ea91d820a846fd1afbe3ba7d717988fee094ce8227a3bf8bd"
 dependencies = [
  "base32",
+ "crate-git-revision",
  "thiserror",
 ]
 
 [[package]]
 name = "stellar-xdr"
 version = "20.0.0-rc1"
-source = "git+https://github.com/stellar/rs-stellar-xdr?rev=9c97e4fa909a0b6455547a4f4a95800696b2a69a#9c97e4fa909a0b6455547a4f4a95800696b2a69a"
+source = "git+https://github.com/stellar/rs-stellar-xdr?rev=957273d7b8092888849219a3ad2aa054059ca89d#957273d7b8092888849219a3ad2aa054059ca89d"
 dependencies = [
  "arbitrary",
  "base64 0.13.1",
@@ -1502,6 +1504,7 @@ dependencies = [
  "hex",
  "serde",
  "serde_with",
+ "stellar-strkey",
 ]
 
 [[package]]
diff --git a/Cargo.toml b/Cargo.toml
index c418b919..5cf258c1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -28,7 +28,7 @@ soroban-builtin-sdk-macros = { version = "20.0.0-rc2", path = "soroban-builtin-s
 [workspace.dependencies.stellar-xdr]
 version = "20.0.0-rc1"
 git = "https://github.com/stellar/rs-stellar-xdr"
-rev = "9c97e4fa909a0b6455547a4f4a95800696b2a69a"
+rev = "957273d7b8092888849219a3ad2aa054059ca89d"
 default-features = false
 
 [workspace.dependencies.wasmi]
@@ -37,11 +37,6 @@ version = "0.31.0-soroban1"
 git = "https://github.com/stellar/wasmi"
 rev = "7e63b4c9e08c4163f417d118d81f7ea34789d0be"
 
-[workspace.dependencies.stellar-strkey]
-version = "0.0.7"
-git = "https://github.com/stellar/rs-stellar-strkey"
-rev = "e6ba45c60c16de28c7522586b80ed0150157df73"
-
 # [patch."https://github.com/stellar/rs-stellar-xdr"]
 # stellar-xdr = { path = "../rs-stellar-xdr/" }
 # [patch."https://github.com/stellar/wasmi"]
diff --git a/soroban-env-common/src/error.rs b/soroban-env-common/src/error.rs
index 3dcdebb6..a9705c99 100644
--- a/soroban-env-common/src/error.rs
+++ b/soroban-env-common/src/error.rs
@@ -155,7 +155,7 @@ impl From<ConversionError> for Error {
 impl From<crate::xdr::Error> for Error {
     fn from(e: crate::xdr::Error) -> Self {
         match e {
-            crate::xdr::Error::DepthLimitExceeded => {
+            crate::xdr::Error::DepthLimitExceeded | crate::xdr::Error::LengthLimitExceeded => {
                 Error::from_type_and_code(ScErrorType::Context, ScErrorCode::ExceededLimit)
             }
             _ => Error::from_type_and_code(ScErrorType::Value, ScErrorCode::InvalidInput),
diff --git a/soroban-env-host/Cargo.toml b/soroban-env-host/Cargo.toml
index 9f8e54a4..e4fc27d6 100644
--- a/soroban-env-host/Cargo.toml
+++ b/soroban-env-host/Cargo.toml
@@ -14,8 +14,8 @@ build = "build.rs"
 [dependencies]
 soroban-builtin-sdk-macros = { workspace = true }
 soroban-env-common = { workspace = true, features = ["std", "wasmi"] }
-stellar-strkey = { workspace = true }
 wasmi = { workspace = true }
+stellar-strkey = "0.0.8"
 static_assertions = "1.1.0"
 sha2 = "0.10.0"
 ed25519-dalek = {version = "2.0.0", features = ["rand_core"] }
diff --git a/soroban-env-host/benches/common/cost_types/host_mem_cmp.rs b/soroban-env-host/benches/common/cost_types/host_mem_cmp.rs
index 1e853a84..5d5c9126 100644
--- a/soroban-env-host/benches/common/cost_types/host_mem_cmp.rs
+++ b/soroban-env-host/benches/common/cost_types/host_mem_cmp.rs
@@ -1,13 +1,7 @@
-use crate::HostCostMeasurement;
-use rand::{rngs::StdRng, RngCore};
+use crate::{common::util::randvec, HostCostMeasurement};
+use rand::rngs::StdRng;
 use soroban_env_host::{cost_runner::*, Host};
 
-fn randvec(rng: &mut StdRng, len: u64) -> Vec<u8> {
-    let mut res: Vec<u8> = vec![0; len as usize];
-    rng.fill_bytes(res.as_mut_slice());
-    res
-}
-
 pub struct MemCmpMeasure;
 impl HostCostMeasurement for MemCmpMeasure {
     type Runner = MemCmpRun;
diff --git a/soroban-env-host/benches/common/cost_types/val_deser.rs b/soroban-env-host/benches/common/cost_types/val_deser.rs
index 0c775e8d..88e1feb0 100644
--- a/soroban-env-host/benches/common/cost_types/val_deser.rs
+++ b/soroban-env-host/benches/common/cost_types/val_deser.rs
@@ -1,4 +1,6 @@
-use soroban_env_host::{cost_runner::ValDeserRun, xdr::ScVal, xdr::WriteXdr};
+use soroban_env_host::{
+    cost_runner::ValDeserRun, xdr::ScVal, xdr::WriteXdr, DEFAULT_XDR_RW_LIMITS,
+};
 
 use super::{ValSerMeasure, MAX_DEPTH};
 use crate::common::HostCostMeasurement;
@@ -14,7 +16,7 @@ impl HostCostMeasurement for ValDeserMeasure {
         input: u64,
     ) -> Vec<u8> {
         let scval = ValSerMeasure::new_random_case(host, rng, input);
-        scval.0.to_xdr().unwrap()
+        scval.0.to_xdr(DEFAULT_XDR_RW_LIMITS).unwrap()
     }
 
     // The worst case is a deeply nested structure, each level containing minimal
@@ -37,6 +39,6 @@ impl HostCostMeasurement for ValDeserMeasure {
             v = ScVal::Vec(Some(inner.try_into().unwrap()));
             rem -= elem_per_level;
         }
-        v.to_xdr().unwrap()
+        v.to_xdr(DEFAULT_XDR_RW_LIMITS).unwrap()
     }
 }
diff --git a/soroban-env-host/benches/common/cost_types/val_ser.rs b/soroban-env-host/benches/common/cost_types/val_ser.rs
index 43c8c3dd..20fdce7a 100644
--- a/soroban-env-host/benches/common/cost_types/val_ser.rs
+++ b/soroban-env-host/benches/common/cost_types/val_ser.rs
@@ -10,6 +10,7 @@ pub(crate) struct ValSerMeasure;
 // This measures the costs of converting an ScVal into bytes.
 impl HostCostMeasurement for ValSerMeasure {
     type Runner = ValSerRun;
+    const STEP_SIZE: u64 = 512;
 
     fn new_random_case(_host: &Host, rng: &mut StdRng, input: u64) -> (ScVal, Vec<u8>) {
         let len = 1 + input * Self::STEP_SIZE;
diff --git a/soroban-env-host/benches/common/experimental/mod.rs b/soroban-env-host/benches/common/experimental/mod.rs
index 0811ba6e..aa11efb1 100644
--- a/soroban-env-host/benches/common/experimental/mod.rs
+++ b/soroban-env-host/benches/common/experimental/mod.rs
@@ -1,3 +1,5 @@
 mod ed25519_scalar_mul;
+mod read_xdr;
 
 pub(crate) use ed25519_scalar_mul::*;
+pub(crate) use read_xdr::*;
diff --git a/soroban-env-host/benches/common/experimental/read_xdr.rs b/soroban-env-host/benches/common/experimental/read_xdr.rs
new file mode 100644
index 00000000..c6e6be67
--- /dev/null
+++ b/soroban-env-host/benches/common/experimental/read_xdr.rs
@@ -0,0 +1,24 @@
+use soroban_env_host::{
+    cost_runner::ReadXdrByteArrayRun,
+    xdr::{ScBytes, ScVal, WriteXdr},
+    DEFAULT_XDR_RW_LIMITS,
+};
+
+use crate::common::{util::randvec, HostCostMeasurement};
+
+pub(crate) struct ReadXdrByteArrayMeasure;
+
+impl HostCostMeasurement for ReadXdrByteArrayMeasure {
+    type Runner = ReadXdrByteArrayRun;
+
+    fn new_random_case(
+        _host: &soroban_env_host::Host,
+        rng: &mut rand::prelude::StdRng,
+        input: u64,
+    ) -> Vec<u8> {
+        let len = 1 + input * Self::STEP_SIZE;
+        let vec = randvec(rng, len);
+        let scval = ScVal::Bytes(ScBytes(vec.try_into().unwrap()));
+        scval.to_xdr(DEFAULT_XDR_RW_LIMITS).unwrap()
+    }
+}
diff --git a/soroban-env-host/benches/common/mod.rs b/soroban-env-host/benches/common/mod.rs
index fd069488..713f9086 100644
--- a/soroban-env-host/benches/common/mod.rs
+++ b/soroban-env-host/benches/common/mod.rs
@@ -49,9 +49,12 @@ fn call_bench<B: Benchmark, HCM: HostCostMeasurement>(
 }
 
 pub(crate) fn for_each_experimental_cost_measurement<B: Benchmark>(
-) -> std::io::Result<(FPCostModel, FPCostModel)> {
-    B::bench::<Ed25519ScalarMulMeasure>()?;
-    B::bench::<VerifyEd25519SigMeasure>()
+) -> std::io::Result<BTreeMap<CostType, (FPCostModel, FPCostModel)>> {
+    let mut params: BTreeMap<CostType, (FPCostModel, FPCostModel)> = BTreeMap::new();
+    call_bench::<B, Ed25519ScalarMulMeasure>(&mut params)?;
+    call_bench::<B, VerifyEd25519SigMeasure>(&mut params)?;
+    call_bench::<B, ReadXdrByteArrayMeasure>(&mut params)?;
+    Ok(params)
 }
 
 pub(crate) fn for_each_host_cost_measurement<B: Benchmark>(
diff --git a/soroban-env-host/benches/common/util.rs b/soroban-env-host/benches/common/util.rs
index ae463494..a41c222f 100644
--- a/soroban-env-host/benches/common/util.rs
+++ b/soroban-env-host/benches/common/util.rs
@@ -1,3 +1,4 @@
+use rand::{rngs::StdRng, RngCore};
 use soroban_env_host::{budget::AsBudget, meta, Host, LedgerInfo, Val, I256};
 
 pub(crate) fn test_host() -> Host {
@@ -35,3 +36,9 @@ pub(crate) fn repeating_byte_i256(byte: u8, input: u64) -> I256 {
     res.extend_from_slice(buf.as_slice());
     I256::from_be_bytes(res.try_into().unwrap())
 }
+
+pub(crate) fn randvec(rng: &mut StdRng, len: u64) -> Vec<u8> {
+    let mut res: Vec<u8> = vec![0; len as usize];
+    rng.fill_bytes(res.as_mut_slice());
+    res
+}
diff --git a/soroban-env-host/benches/worst_case_linear_models.rs b/soroban-env-host/benches/worst_case_linear_models.rs
index b66d0c60..3de6fea8 100644
--- a/soroban-env-host/benches/worst_case_linear_models.rs
+++ b/soroban-env-host/benches/worst_case_linear_models.rs
@@ -2,6 +2,7 @@
 // $ cargo bench --features testutils --bench worst_case_linear_models -- --nocapture
 // You can optionally pass in args listing the {`ContractCostType`, `WasmInsnType`} combination to run with, e.g.
 // $ cargo bench --features testutils --bench worst_case_linear_models -- MemCpy I64Rotr --nocapture
+// To run the experimental cost types: $ RUN_EXPERIMENT=1 cargo bench ...
 mod common;
 use common::*;
 use soroban_env_host::{
@@ -176,7 +177,11 @@ fn extract_wasmi_fuel_costs(
 
 #[cfg(all(test, any(target_os = "linux", target_os = "macos")))]
 fn main() -> std::io::Result<()> {
-    let params = for_each_host_cost_measurement::<WorstCaseLinearModels>()?;
+    let params = if std::env::var("RUN_EXPERIMENT").is_err() {
+        for_each_host_cost_measurement::<WorstCaseLinearModels>()?
+    } else {
+        for_each_experimental_cost_measurement::<WorstCaseLinearModels>()?
+    };
     let params_wasm = for_each_wasm_insn_measurement::<WorstCaseLinearModels>()?;
 
     let mut tw = TabWriter::new(vec![])
diff --git a/soroban-env-host/src/budget.rs b/soroban-env-host/src/budget.rs
index cf9ba7b4..1091dac9 100644
--- a/soroban-env-host/src/budget.rs
+++ b/soroban-env-host/src/budget.rs
@@ -1,8 +1,11 @@
 mod dimension;
+mod limits;
 mod model;
 mod util;
 mod wasmi_helper;
 
+pub(crate) use limits::DepthLimiter;
+pub use limits::{DEFAULT_HOST_DEPTH_LIMIT, DEFAULT_XDR_RW_LIMITS};
 pub use model::COST_MODEL_LIN_TERM_SCALE_BITS;
 
 use std::{
@@ -13,19 +16,14 @@ use std::{
 
 use crate::{
     host::error::TryBorrowOrErr,
-    xdr::{ContractCostParams, ContractCostType, DepthLimiter, ScErrorCode, ScErrorType},
-    Error, Host, HostError, DEFAULT_HOST_DEPTH_LIMIT,
+    xdr::{ContractCostParams, ContractCostType, ScErrorCode, ScErrorType},
+    Host, HostError,
 };
 
 use dimension::{BudgetDimension, IsCpu, IsShadowMode};
 use model::ScaledU64;
 use wasmi_helper::FuelConfig;
 
-// These are some sane values, however the embedder should typically customize
-// these to match the network config.
-const DEFAULT_CPU_INSN_LIMIT: u64 = 100_000_000;
-const DEFAULT_MEM_BYTES_LIMIT: u64 = 40 * 1024 * 1024; // 40MB
-
 #[derive(Clone, Default)]
 struct MeterTracker {
     // Tracks the `(sum_of_iterations, total_input)` for each `CostType`
@@ -423,8 +421,8 @@ impl Default for BudgetImpl {
         }
 
         // define the limits
-        b.cpu_insns.reset(DEFAULT_CPU_INSN_LIMIT);
-        b.mem_bytes.reset(DEFAULT_MEM_BYTES_LIMIT);
+        b.cpu_insns.reset(limits::DEFAULT_CPU_INSN_LIMIT);
+        b.mem_bytes.reset(limits::DEFAULT_MEM_BYTES_LIMIT);
         b
     }
 }
@@ -527,30 +525,6 @@ impl Display for BudgetImpl {
     }
 }
 
-impl DepthLimiter for BudgetImpl {
-    type DepthLimiterError = HostError;
-
-    fn enter(&mut self) -> Result<(), HostError> {
-        if let Some(depth) = self.depth_limit.checked_sub(1) {
-            self.depth_limit = depth;
-        } else {
-            return Err(Error::from_type_and_code(
-                ScErrorType::Context,
-                ScErrorCode::ExceededLimit,
-            )
-            .into());
-        }
-        Ok(())
-    }
-
-    // `leave` should be called in tandem with `enter` such that the depth
-    // doesn't exceed the initial depth limit.
-    fn leave(&mut self) -> Result<(), HostError> {
-        self.depth_limit = self.depth_limit.saturating_add(1);
-        Ok(())
-    }
-}
-
 #[derive(Clone)]
 pub struct Budget(pub(crate) Rc<RefCell<BudgetImpl>>);
 
@@ -603,18 +577,6 @@ impl AsBudget for &Host {
     }
 }
 
-impl DepthLimiter for Budget {
-    type DepthLimiterError = HostError;
-
-    fn enter(&mut self) -> Result<(), HostError> {
-        self.0.try_borrow_mut_or_err()?.enter()
-    }
-
-    fn leave(&mut self) -> Result<(), HostError> {
-        self.0.try_borrow_mut_or_err()?.leave()
-    }
-}
-
 impl Budget {
     /// Initializes the budget from network configuration settings.
     pub fn try_from_configs(
diff --git a/soroban-env-host/src/budget/limits.rs b/soroban-env-host/src/budget/limits.rs
new file mode 100644
index 00000000..0d27dfa3
--- /dev/null
+++ b/soroban-env-host/src/budget/limits.rs
@@ -0,0 +1,99 @@
+use crate::{
+    budget::{Budget, BudgetImpl},
+    host::error::TryBorrowOrErr,
+    xdr::{Limits, ScErrorCode, ScErrorType},
+    Error, HostError,
+};
+
+/// These constants are used to set limits on recursion and data length in the
+/// context of XDR (de)serialization. They serve as safeguards agaist both
+/// exccessive stack allocation, which could cause an unrecoverable `SIGABRT`,
+/// and exccessive heap memory allocation.
+pub const DEFAULT_XDR_RW_LIMITS: Limits = Limits {
+    /// recursion limit for reading and writing XDR structures.
+    depth: 500,
+    /// maximum byte length for a data structure during serialization and
+    /// deserialization to and from the XDR format. The limit of 16MiB
+    /// corresponds to the overlay maximum message size.
+    len: 0x1000000,
+};
+
+/// - `DEFAULT_HOST_DEPTH_LIMIT`: This limit applies to the host environment. It
+///   guards recursion paths involving the `Env` and `Budget`, particularly
+///   during operations like conversion, comparison, and deep cloning. The limit
+///   is strategically checked at critical recursion points, such as when
+///   encountering a `Val`. As the actual stack usage can be higher,
+///   `DEFAULT_HOST_DEPTH_LIMIT` is conservatively set to a lower value than the
+///   XDR limit.
+pub const DEFAULT_HOST_DEPTH_LIMIT: u32 = 100;
+
+// These are some sane values, however the embedder should typically customize
+// these to match the network config.
+pub(crate) const DEFAULT_CPU_INSN_LIMIT: u64 = 100_000_000;
+pub(crate) const DEFAULT_MEM_BYTES_LIMIT: u64 = 40 * 1024 * 1024; // 40MB
+
+/// `DepthLimiter` is a trait designed for managing the depth of recursive operations.
+/// It provides a mechanism to limit recursion depth, and defines the behavior upon
+/// entering and leaving a recursion level.
+pub(crate) trait DepthLimiter {
+    /// Defines the behavior for entering a new recursion level.
+    /// An `ExceededLimit` is returned if the new level exceeds the depth limit.
+    fn enter(&mut self) -> Result<(), HostError>;
+
+    /// Defines the behavior for leaving a recursion level.
+    fn leave(&mut self) -> Result<(), HostError>;
+
+    /// Wraps a given function `f` with depth limiting guards.
+    /// It triggers an `enter` before, and a `leave` after the execution of `f`.
+    ///
+    /// # Parameters
+    ///
+    /// - `f`: The function to be executed under depth limit constraints.
+    ///
+    /// # Returns
+    ///
+    /// - `Err` if 1. the depth limit has been exceeded upon `enter` 2. `f` executes
+    ///         with an error 3. if error occurs on `leave`.
+    ///   `Ok` otherwise.
+    fn with_limited_depth<T, F>(&mut self, f: F) -> Result<T, HostError>
+    where
+        F: FnOnce(&mut Self) -> Result<T, HostError>,
+    {
+        self.enter()?;
+        let res = f(self);
+        self.leave()?;
+        res
+    }
+}
+
+impl DepthLimiter for BudgetImpl {
+    fn enter(&mut self) -> Result<(), HostError> {
+        if let Some(depth) = self.depth_limit.checked_sub(1) {
+            self.depth_limit = depth;
+        } else {
+            return Err(Error::from_type_and_code(
+                ScErrorType::Context,
+                ScErrorCode::ExceededLimit,
+            )
+            .into());
+        }
+        Ok(())
+    }
+
+    // `leave` should be called in tandem with `enter` such that the depth
+    // doesn't exceed the initial depth limit.
+    fn leave(&mut self) -> Result<(), HostError> {
+        self.depth_limit = self.depth_limit.saturating_add(1);
+        Ok(())
+    }
+}
+
+impl DepthLimiter for Budget {
+    fn enter(&mut self) -> Result<(), HostError> {
+        self.0.try_borrow_mut_or_err()?.enter()
+    }
+
+    fn leave(&mut self) -> Result<(), HostError> {
+        self.0.try_borrow_mut_or_err()?.leave()
+    }
+}
diff --git a/soroban-env-host/src/cost_runner/experimental/mod.rs b/soroban-env-host/src/cost_runner/experimental/mod.rs
index c43a2b42..2f0ef241 100644
--- a/soroban-env-host/src/cost_runner/experimental/mod.rs
+++ b/soroban-env-host/src/cost_runner/experimental/mod.rs
@@ -1,6 +1,8 @@
 mod ed25519_scalar_mut;
+mod read_xdr;
 
 pub use ed25519_scalar_mut::*;
+pub use read_xdr::*;
 
 use crate::xdr::Name;
 use core::fmt;
@@ -8,6 +10,7 @@ use core::fmt;
 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
 pub enum ExperimentalCostType {
     EdwardsPointCurve25519ScalarMul,
+    ReadXdrByteArray,
 }
 
 impl Name for ExperimentalCostType {
@@ -16,6 +19,7 @@ impl Name for ExperimentalCostType {
             ExperimentalCostType::EdwardsPointCurve25519ScalarMul => {
                 "EdwardsPointCurve25519ScalarMul"
             }
+            ExperimentalCostType::ReadXdrByteArray => "ReadXdrByteArray",
         }
     }
 }
diff --git a/soroban-env-host/src/cost_runner/experimental/read_xdr.rs b/soroban-env-host/src/cost_runner/experimental/read_xdr.rs
new file mode 100644
index 00000000..a8bbfc9b
--- /dev/null
+++ b/soroban-env-host/src/cost_runner/experimental/read_xdr.rs
@@ -0,0 +1,39 @@
+use std::hint::black_box;
+
+use crate::{
+    budget::AsBudget,
+    cost_runner::{experimental::ExperimentalCostType::ReadXdrByteArray, CostRunner, CostType},
+    xdr::{ContractCostType::ValDeser, ScVal},
+};
+
+pub struct ReadXdrByteArrayRun;
+
+impl CostRunner for ReadXdrByteArrayRun {
+    // experimental cost type that identifies this component. Used purely for result aggregation
+    // purpose. Internally there is no budget component associated with this type, and you'll
+    // have to override the get_tracker method to return the correct inputs.
+    const COST_TYPE: CostType = CostType::Experimental(ReadXdrByteArray);
+
+    type SampleType = Vec<u8>;
+
+    type RecycledType = (Option<ScVal>, Vec<u8>);
+
+    fn run_iter(host: &crate::Host, _iter: u64, sample: Self::SampleType) -> Self::RecycledType {
+        let sv = black_box(host.metered_from_xdr::<ScVal>(&sample).unwrap());
+        (Some(sv), sample)
+    }
+
+    fn run_baseline_iter(
+        host: &crate::Host,
+        _iter: u64,
+        sample: Self::SampleType,
+    ) -> Self::RecycledType {
+        black_box(host.charge_budget(ValDeser, Some(0)).unwrap());
+        black_box((None, sample))
+    }
+
+    fn get_tracker(host: &crate::Host) -> (u64, Option<u64>) {
+        // internally this is still charged under `ValDeser`
+        host.as_budget().get_tracker(ValDeser).unwrap()
+    }
+}
diff --git a/soroban-env-host/src/host.rs b/soroban-env-host/src/host.rs
index c639166c..05cb3450 100644
--- a/soroban-env-host/src/host.rs
+++ b/soroban-env-host/src/host.rs
@@ -56,20 +56,6 @@ pub(crate) use frame::Frame;
 #[cfg(any(test, feature = "recording_auth"))]
 use rand_chacha::ChaCha20Rng;
 
-/// Defines the maximum depth for recursive calls in the host, i.e. `Val` conversion, comparison,
-/// and deep clone, to prevent stack overflow.
-///
-/// Similar to the `xdr::DEFAULT_XDR_RW_DEPTH_LIMIT`, `DEFAULT_HOST_DEPTH_LIMIT` is also a proxy
-/// to the stack depth limit, and its purpose is to prevent the program from
-/// hitting the maximum stack size allowed by Rust, which would result in an unrecoverable `SIGABRT`.
-///
-/// The difference is the `DEFAULT_HOST_DEPTH_LIMIT`guards the recursion paths via the `Env` and
-/// the `Budget`, i.e., conversion, comparison and deep clone. The limit is checked at specific
-/// points of the recursion path, e.g. when `Val` is encountered, to minimize noise. So the
-/// "actual stack depth"/"host depth" factor will typically be larger, and thus the
-/// `DEFAULT_HOST_DEPTH_LIMIT` here is set to a smaller value.
-pub const DEFAULT_HOST_DEPTH_LIMIT: u32 = 100;
-
 /// Temporary helper for denoting a slice of guest memory, as formed by
 /// various bytes operations.
 pub(crate) struct VmSlice {
diff --git a/soroban-env-host/src/host/comparison.rs b/soroban-env-host/src/host/comparison.rs
index fe22e5b8..4eded490 100644
--- a/soroban-env-host/src/host/comparison.rs
+++ b/soroban-env-host/src/host/comparison.rs
@@ -1,16 +1,15 @@
 use core::cmp::{min, Ordering};
 
 use crate::{
-    budget::{AsBudget, Budget},
+    budget::{AsBudget, Budget, DepthLimiter},
     host_object::HostObject,
     storage::Storage,
     xdr::{
         AccountId, ContractCostType, ContractDataDurability, ContractExecutable,
-        CreateContractArgs, DepthLimiter, Duration, Hash, Int128Parts, Int256Parts, LedgerKey,
-        LedgerKeyAccount, LedgerKeyContractCode, LedgerKeyContractData, LedgerKeyTrustLine,
-        PublicKey, ScAddress, ScContractInstance, ScError, ScErrorCode, ScErrorType, ScMap,
-        ScMapEntry, ScNonceKey, ScVal, ScVec, TimePoint, TrustLineAsset, UInt128Parts,
-        UInt256Parts, Uint256,
+        CreateContractArgs, Duration, Hash, Int128Parts, Int256Parts, LedgerKey, LedgerKeyAccount,
+        LedgerKeyContractCode, LedgerKeyContractData, LedgerKeyTrustLine, PublicKey, ScAddress,
+        ScContractInstance, ScError, ScErrorCode, ScErrorType, ScMap, ScMapEntry, ScNonceKey,
+        ScVal, ScVec, TimePoint, TrustLineAsset, UInt128Parts, UInt256Parts, Uint256,
     },
     Compare, Host, HostError, SymbolStr, I256, U256,
 };
diff --git a/soroban-env-host/src/host/conversion.rs b/soroban-env-host/src/host/conversion.rs
index b39cc301..6dc3dedc 100644
--- a/soroban-env-host/src/host/conversion.rs
+++ b/soroban-env-host/src/host/conversion.rs
@@ -3,7 +3,7 @@ use std::rc::Rc;
 use super::metered_clone::{
     charge_shallow_copy, MeteredAlloc, MeteredClone, MeteredContainer, MeteredIterator,
 };
-use crate::budget::AsBudget;
+use crate::budget::{AsBudget, DepthLimiter};
 use crate::err;
 use crate::host_object::{HostMap, HostObject, HostVec};
 use crate::xdr::{Hash, LedgerKey, LedgerKeyContractData, ScVal, ScVec, Uint256};
@@ -12,9 +12,8 @@ use soroban_env_common::num::{
     i256_from_pieces, i256_into_pieces, u256_from_pieces, u256_into_pieces,
 };
 use soroban_env_common::xdr::{
-    self, int128_helpers, AccountId, ContractDataDurability, DepthLimiter, Int128Parts,
-    Int256Parts, ScAddress, ScBytes, ScErrorCode, ScErrorType, ScMap, ScMapEntry, UInt128Parts,
-    UInt256Parts, VecM,
+    self, int128_helpers, AccountId, ContractDataDurability, Int128Parts, Int256Parts, ScAddress,
+    ScBytes, ScErrorCode, ScErrorType, ScMap, ScMapEntry, UInt128Parts, UInt256Parts, VecM,
 };
 use soroban_env_common::{
     AddressObject, BytesObject, Convert, Object, ScValObjRef, ScValObject, TryFromVal, TryIntoVal,
diff --git a/soroban-env-host/src/host/metered_clone.rs b/soroban-env-host/src/host/metered_clone.rs
index 3cd5981d..6d700915 100644
--- a/soroban-env-host/src/host/metered_clone.rs
+++ b/soroban-env-host/src/host/metered_clone.rs
@@ -19,12 +19,12 @@
 use std::{iter::FromIterator, mem, rc::Rc};
 
 use crate::{
-    budget::AsBudget,
+    budget::{AsBudget, DepthLimiter},
     builtin_contracts::base_types::Address,
     storage::AccessType,
     xdr::{
         AccountEntry, AccountId, Asset, BytesM, ContractCodeEntry, ContractCostType,
-        ContractExecutable, ContractIdPreimage, CreateContractArgs, DepthLimiter, Duration, Hash,
+        ContractExecutable, ContractIdPreimage, CreateContractArgs, Duration, Hash,
         InvokeContractArgs, LedgerEntry, LedgerEntryData, LedgerEntryExt, LedgerKey,
         LedgerKeyAccount, LedgerKeyContractCode, LedgerKeyTrustLine, PublicKey, ScAddress, ScBytes,
         ScContractInstance, ScErrorCode, ScErrorType, ScMap, ScMapEntry, ScNonceKey, ScString,
diff --git a/soroban-env-host/src/host/metered_xdr.rs b/soroban-env-host/src/host/metered_xdr.rs
index eab29fd8..8567498e 100644
--- a/soroban-env-host/src/host/metered_xdr.rs
+++ b/soroban-env-host/src/host/metered_xdr.rs
@@ -1,10 +1,7 @@
 use crate::{
     budget::Budget,
-    xdr::{
-        ContractCostType, DepthLimitedWrite, ReadXdr, ScBytes, ScErrorCode, ScErrorType, WriteXdr,
-        DEFAULT_XDR_RW_DEPTH_LIMIT,
-    },
-    BytesObject, Host, HostError,
+    xdr::{ContractCostType, Limited, ReadXdr, ScBytes, ScErrorCode, ScErrorType, WriteXdr},
+    BytesObject, Host, HostError, DEFAULT_XDR_RW_LIMITS,
 };
 use std::io::Write;
 
@@ -43,7 +40,7 @@ impl Host {
     pub fn metered_from_xdr<T: ReadXdr>(&self, bytes: &[u8]) -> Result<T, HostError> {
         let _span = tracy_span!("read xdr");
         self.charge_budget(ContractCostType::ValDeser, Some(bytes.len() as u64))?;
-        self.map_err(T::from_xdr(bytes))
+        self.map_err(T::from_xdr(bytes, DEFAULT_XDR_RW_LIMITS))
     }
 
     pub(crate) fn metered_from_xdr_obj<T: ReadXdr>(
@@ -60,8 +57,7 @@ pub fn metered_write_xdr(
     w: &mut Vec<u8>,
 ) -> Result<(), HostError> {
     let _span = tracy_span!("write xdr");
-    let mw = MeteredWrite { budget, w };
-    let mut w = DepthLimitedWrite::new(mw, DEFAULT_XDR_RW_DEPTH_LIMIT);
+    let mut w = Limited::new(MeteredWrite { budget, w }, DEFAULT_XDR_RW_LIMITS);
     // MeteredWrite above turned any budget failure into an IO error; we turn it
     // back to a budget failure here, since there's really no "IO error" that can
     // occur when writing to a Vec<u8>.
@@ -78,5 +74,5 @@ pub fn metered_from_xdr_with_budget<T: ReadXdr>(
 ) -> Result<T, HostError> {
     let _span = tracy_span!("read xdr with budget");
     budget.charge(ContractCostType::ValDeser, Some(bytes.len() as u64))?;
-    T::from_xdr(bytes).map_err(|e| e.into())
+    T::from_xdr(bytes, DEFAULT_XDR_RW_LIMITS).map_err(|e| e.into())
 }
diff --git a/soroban-env-host/src/lib.rs b/soroban-env-host/src/lib.rs
index 0ade700d..1441a424 100644
--- a/soroban-env-host/src/lib.rs
+++ b/soroban-env-host/src/lib.rs
@@ -57,6 +57,7 @@ pub mod storage;
 #[cfg(test)]
 mod test;
 
+pub use budget::{DEFAULT_HOST_DEPTH_LIMIT, DEFAULT_XDR_RW_LIMITS};
 #[cfg(any(test, feature = "testutils"))]
 #[doc(hidden)]
 pub use host::testutils::call_with_suppressed_panic_hook;
@@ -64,7 +65,7 @@ pub use host::testutils::call_with_suppressed_panic_hook;
 pub use host::ContractFunctionSet;
 pub use host::{
     metered_map::MeteredOrdMap, metered_vector::MeteredVector, Host, HostError, LedgerInfo, Seed,
-    DEFAULT_HOST_DEPTH_LIMIT, SEED_BYTES,
+    SEED_BYTES,
 };
 pub use soroban_env_common::*;
 
diff --git a/soroban-env-host/src/vm.rs b/soroban-env-host/src/vm.rs
index b6aae5e2..0bb857e7 100644
--- a/soroban-env-host/src/vm.rs
+++ b/soroban-env-host/src/vm.rs
@@ -19,22 +19,15 @@ use crate::{
     budget::AsBudget,
     err,
     host::{error::TryBorrowOrErr, metered_clone::MeteredContainer},
-    xdr::ContractCostType,
-    HostError,
+    meta::{self, get_ledger_protocol_version},
+    xdr::{ContractCostType, Hash, Limited, ReadXdr, ScEnvMetaEntry, ScErrorCode, ScErrorType},
+    ConversionError, Host, HostError, Symbol, SymbolStr, TryIntoVal, Val, WasmiMarshal,
+    DEFAULT_XDR_RW_LIMITS,
 };
 use std::{cell::RefCell, io::Cursor, rc::Rc};
 
-use super::{xdr::Hash, Host, Symbol, Val};
 use fuel_refillable::FuelRefillable;
 use func_info::HOST_FUNCTIONS;
-use soroban_env_common::{
-    meta::{self, get_ledger_protocol_version},
-    xdr::{
-        DepthLimitedRead, ReadXdr, ScEnvMetaEntry, ScErrorCode, ScErrorType,
-        DEFAULT_XDR_RW_DEPTH_LIMIT,
-    },
-    ConversionError, SymbolStr, TryIntoVal, WasmiMarshal,
-};
 
 use wasmi::{Engine, FuelConsumptionMode, Instance, Linker, Memory, Module, Store, Value};
 
@@ -161,8 +154,7 @@ impl Vm {
         // us as well as a protocol that's less than or equal to our protocol.
 
         if let Some(env_meta) = Self::module_custom_section(m, meta::ENV_META_V0_SECTION_NAME) {
-            let mut cursor =
-                DepthLimitedRead::new(Cursor::new(env_meta), DEFAULT_XDR_RW_DEPTH_LIMIT);
+            let mut cursor = Limited::new(Cursor::new(env_meta), DEFAULT_XDR_RW_LIMITS);
             if let Some(env_meta_entry) = ScEnvMetaEntry::read_xdr_iter(&mut cursor).next() {
                 let ScEnvMetaEntry::ScEnvMetaKindInterfaceVersion(v) =
                     host.map_err(env_meta_entry)?;
diff --git a/soroban-env-macros/src/lib.rs b/soroban-env-macros/src/lib.rs
index 2e2b7fb9..e6a0970b 100644
--- a/soroban-env-macros/src/lib.rs
+++ b/soroban-env-macros/src/lib.rs
@@ -15,7 +15,7 @@ use stellar_xdr::curr as xdr;
 #[cfg(feature = "next")]
 use stellar_xdr::next as xdr;
 
-use crate::xdr::{ScEnvMetaEntry, WriteXdr};
+use crate::xdr::{Limits, ScEnvMetaEntry, WriteXdr};
 
 struct MetaInput {
     pub interface_version: u64,
@@ -62,7 +62,8 @@ impl ToTokens for MetaConstsOutput {
         let meta_xdr = self
             .to_meta_entries()
             .into_iter()
-            .map(|entry| entry.to_xdr())
+            // Limits::none here is okay since `MetaConstsOutput` is controled by us
+            .map(|entry| entry.to_xdr(Limits::none()))
             .collect::<Result<Vec<Vec<u8>>, crate::xdr::Error>>()
             .unwrap()
             .concat();


Test patch:
diff --git a/soroban-env-host/src/builtin_contracts/stellar_asset_contract/test_stellar_asset_contract.rs b/soroban-env-host/src/builtin_contracts/stellar_asset_contract/test_stellar_asset_contract.rs
index cde4da1c..3d14f98e 100644
--- a/soroban-env-host/src/builtin_contracts/stellar_asset_contract/test_stellar_asset_contract.rs
+++ b/soroban-env-host/src/builtin_contracts/stellar_asset_contract/test_stellar_asset_contract.rs
@@ -1,17 +1,12 @@
 use crate::{
     builtin_contracts::{
-        base_types::Address,
+        base_types::{Address, Bytes, String},
         testutils::{authorize_single_invocation, ContractTypeVec, TestSigner},
     },
-    host_vec, Host, HostError,
+    host_vec,
+    xdr::{Asset, Limited, WriteXdr},
+    Env, Host, HostError, Symbol, TryFromVal, TryIntoVal, DEFAULT_XDR_RW_LIMITS,
 };
-use soroban_env_common::{
-    xdr::{Asset, DepthLimitedWrite, WriteXdr, DEFAULT_XDR_RW_DEPTH_LIMIT},
-    Env,
-};
-use soroban_env_common::{Symbol, TryFromVal, TryIntoVal};
-
-use crate::builtin_contracts::base_types::{Bytes, String};
 
 pub(crate) struct TestStellarAssetContract<'a> {
     pub(crate) address: Address,
@@ -20,7 +15,7 @@ pub(crate) struct TestStellarAssetContract<'a> {
 
 impl<'a> TestStellarAssetContract<'a> {
     pub(crate) fn new_from_asset(host: &'a Host, asset: Asset) -> Self {
-        let mut asset_bytes_vec = DepthLimitedWrite::new(vec![], DEFAULT_XDR_RW_DEPTH_LIMIT);
+        let mut asset_bytes_vec = Limited::new(vec![], DEFAULT_XDR_RW_LIMITS);
         asset.write_xdr(&mut asset_bytes_vec).unwrap();
         let address_obj = host
             .create_asset_contract(
diff --git a/soroban-env-host/src/test/bytes.rs b/soroban-env-host/src/test/bytes.rs
index 9e4fc9c6..f5bfea4f 100644
--- a/soroban-env-host/src/test/bytes.rs
+++ b/soroban-env-host/src/test/bytes.rs
@@ -1,6 +1,6 @@
 use crate::{
     xdr::{ScError, ScVal},
-    Env, Host, HostError, Val,
+    Env, Host, HostError, Val, DEFAULT_XDR_RW_LIMITS,
 };
 use soroban_env_common::{
     xdr::{ScBytes, ScErrorCode, ScErrorType, ScVec, WriteXdr},
@@ -119,7 +119,7 @@ fn bytes_xdr_roundtrip() -> Result<(), HostError> {
         Ok(())
     };
     let deser_fails_scv = |v: ScVal| -> Result<(), HostError> {
-        let bytes: Vec<u8> = v.to_xdr()?;
+        let bytes: Vec<u8> = v.to_xdr(DEFAULT_XDR_RW_LIMITS)?;
         let bo = host.add_host_object(ScBytes(bytes.try_into()?))?;
         let res = host.deserialize_from_bytes(bo);
         assert!(res.is_err());
diff --git a/soroban-env-host/src/test/depth_limit.rs b/soroban-env-host/src/test/depth_limit.rs
index 90e4652a..6aea6538 100644
--- a/soroban-env-host/src/test/depth_limit.rs
+++ b/soroban-env-host/src/test/depth_limit.rs
@@ -1,10 +1,10 @@
-use soroban_env_common::xdr::{ReadXdr, WriteXdr};
+use soroban_env_common::xdr::{Limits, ReadXdr, WriteXdr};
 
 use crate::{
     budget::AsBudget,
     host::metered_clone::MeteredClone,
     xdr::{ScErrorCode, ScErrorType, ScVal, ScVec},
-    Env, Host, HostError,
+    Env, Host, HostError, DEFAULT_XDR_RW_LIMITS,
 };
 
 #[test]
@@ -91,29 +91,51 @@ fn deep_host_obj_cmp() -> Result<(), HostError> {
 }
 
 #[test]
-fn deep_scval_xdr_serialization() -> Result<(), HostError> {
+fn depth_limited_scval_xdr_serialization() -> Result<(), HostError> {
     let mut v = ScVal::from(ScVec::default());
     for _ in 0..200 {
         let vv = ScVec::try_from(vec![v])?;
         v = ScVal::from(vv);
     }
+    // default depth limit will cause serialization to fail
     let res = v
-        .to_xdr_with_depth_limit(500)
+        .to_xdr(DEFAULT_XDR_RW_LIMITS)
         .map_err(|e| HostError::from(e));
     let code = (ScErrorType::Context, ScErrorCode::ExceededLimit);
     assert!(HostError::result_matches_err(res, code));
+
+    // remove the Limits to make serialization pass
+    let bytes = v.to_xdr(Limits::none())?;
+    // deserialization from the input with limits will fail
+    let res = ScVal::from_xdr(bytes, DEFAULT_XDR_RW_LIMITS).map_err(|e| HostError::from(e));
+    assert!(HostError::result_matches_err(res, code));
     Ok(())
 }
 
 #[test]
-fn deep_scval_xdr_deserialization() -> Result<(), HostError> {
-    let mut v = ScVal::from(ScVec::default());
-    for _ in 0..200 {
-        let vv = ScVec::try_from(vec![v])?;
-        v = ScVal::from(vv);
+fn length_limited_scval_xdr_conversion() -> Result<(), HostError> {
+    let buf = vec![0; 200000];
+    let scv_bytes = ScVal::Bytes(buf.try_into().unwrap());
+    let mut scv_vec = ScVal::Vec(Some(ScVec(vec![scv_bytes.clone(); 1].try_into().unwrap())));
+    // roughly consumes 20MiB (> 16 MiB the limit)
+    for _i in 1..100 {
+        let mut v = vec![scv_vec; 1];
+        v.push(scv_bytes.clone());
+        scv_vec = ScVal::Vec(Some(v.try_into().unwrap()));
     }
-    let bytes = v.to_xdr_with_depth_limit(10000)?;
-    let res = ScVal::from_xdr_with_depth_limit(bytes, 500).map_err(|e| HostError::from(e));
+    // default length limit will cause serialization to fail
+    let res = scv_vec
+        .to_xdr(DEFAULT_XDR_RW_LIMITS)
+        .map_err(|e| HostError::from(e));
+    let code = (ScErrorType::Context, ScErrorCode::ExceededLimit);
+    assert!(HostError::result_matches_err(res, code));
+
+    // remove the Limits to make serialization pass
+    let bytes = scv_vec
+        .to_xdr(Limits::none())
+        .map_err(|e| HostError::from(e))?;
+    // deserialization from the input with limits will fail
+    let res = ScVal::from_xdr(bytes, DEFAULT_XDR_RW_LIMITS).map_err(|e| HostError::from(e));
     let code = (ScErrorType::Context, ScErrorCode::ExceededLimit);
     assert!(HostError::result_matches_err(res, code));
     Ok(())
diff --git a/soroban-env-host/src/test/lifecycle.rs b/soroban-env-host/src/test/lifecycle.rs
index 1368e410..4a46885f 100644
--- a/soroban-env-host/src/test/lifecycle.rs
+++ b/soroban-env-host/src/test/lifecycle.rs
@@ -9,13 +9,13 @@ use crate::{
         ContractExecutable, CreateContractArgs, ExtensionPoint, Hash, HashIdPreimage,
         HashIdPreimageContractId, LedgerEntryData, ScSymbol, ScVal, ScVec, Uint256,
     },
-    Env, Host, LedgerInfo, Symbol,
+    Env, Host, LedgerInfo, Symbol, DEFAULT_XDR_RW_LIMITS,
 };
 use sha2::{Digest, Sha256};
 use soroban_env_common::xdr::{
-    ContractIdPreimage, ContractIdPreimageFromAddress, DepthLimitedWrite, HostFunction, ScAddress,
+    ContractIdPreimage, ContractIdPreimageFromAddress, HostFunction, Limited, ScAddress,
     ScErrorCode, ScErrorType, SorobanAuthorizationEntry, SorobanAuthorizedFunction,
-    SorobanAuthorizedInvocation, SorobanCredentials, VecM, DEFAULT_XDR_RW_DEPTH_LIMIT,
+    SorobanAuthorizedInvocation, SorobanCredentials, VecM,
 };
 use soroban_env_common::{xdr::ScBytes, TryIntoVal, Val};
 use soroban_env_common::{StorageType, VecObject};
@@ -241,7 +241,7 @@ fn create_contract_from_source_account() {
 }
 
 pub(crate) fn sha256_hash_id_preimage<T: xdr::WriteXdr>(pre_image: T) -> xdr::Hash {
-    let mut buf = DepthLimitedWrite::new(Vec::new(), DEFAULT_XDR_RW_DEPTH_LIMIT);
+    let mut buf = Limited::new(Vec::new(), DEFAULT_XDR_RW_LIMITS);
     pre_image
         .write_xdr(&mut buf)
         .expect("preimage write failed");
diff --git a/soroban-env-host/tests/fees.rs b/soroban-env-host/tests/fees.rs
index 92f7730e..10814540 100644
--- a/soroban-env-host/tests/fees.rs
+++ b/soroban-env-host/tests/fees.rs
@@ -1,10 +1,13 @@
 use soroban_env_common::xdr::{Hash, LedgerEntry, LedgerEntryData, LedgerEntryExt, WriteXdr};
-use soroban_env_host::fees::{
-    compute_rent_fee, compute_transaction_resource_fee, compute_write_fee_per_1kb,
-    FeeConfiguration, LedgerEntryRentChange, RentFeeConfiguration, TransactionResources,
-    WriteFeeConfiguration, TTL_ENTRY_SIZE,
+use soroban_env_host::{
+    fees::{
+        compute_rent_fee, compute_transaction_resource_fee, compute_write_fee_per_1kb,
+        FeeConfiguration, LedgerEntryRentChange, RentFeeConfiguration, TransactionResources,
+        WriteFeeConfiguration, TTL_ENTRY_SIZE,
+    },
+    xdr::TtlEntry,
+    DEFAULT_XDR_RW_LIMITS,
 };
-use soroban_env_host::xdr::TtlEntry;
 
 #[test]
 fn ttl_entry_size() {
@@ -18,7 +21,306 @@ fn ttl_entry_size() {
     };
     assert_eq!(
         TTL_ENTRY_SIZE,
-        expiration_entry.to_xdr().unwrap().len() as u32
+        expiration_entry
+            .to_xdr(DEFAULT_XDR_RW_LIMITS)
+            .unwrap()
+            .len() as u32
+    );
+}
+
+fn change_resource<T>(func: T) -> TransactionResources
+where
+    T: FnOnce(&mut TransactionResources) -> (),
+{
+    let mut resources = TransactionResources {
+        instructions: 0,
+        read_entries: 0,
+        write_entries: 0,
+        read_bytes: 0,
+        write_bytes: 0,
+        contract_events_size_bytes: 0,
+        transaction_size_bytes: 0,
+    };
+    func(&mut resources);
+    resources
+}
+
+#[test]
+fn resource_fee_computation_with_single_resource() {
+    // Historical fee is always paid for 300 byte of transaction result.
+    // ceil(6000 * 300 / 1024) == 1758
+    const BASE_HISTORICAL_FEE: i64 = 1758;
+    let fee_config = FeeConfiguration {
+        fee_per_instruction_increment: 1000,
+        fee_per_read_entry: 2000,
+        fee_per_write_entry: 3000,
+        fee_per_read_1kb: 4000,
+        fee_per_write_1kb: 5000,
+        fee_per_historical_1kb: 6000,
+        fee_per_contract_event_1kb: 7000,
+        fee_per_transaction_size_1kb: 8000,
+    };
+
+    // Instructions
+    assert_eq!(
+        compute_transaction_resource_fee(
+            &change_resource(|res: &mut TransactionResources| {
+                res.instructions = 1;
+            }),
+            &fee_config,
+        ),
+        (1 + BASE_HISTORICAL_FEE, 0)
+    );
+    assert_eq!(
+        compute_transaction_resource_fee(
+            &change_resource(|res: &mut TransactionResources| {
+                res.instructions = 10000;
+            }),
+            &fee_config,
+        ),
+        (1000 + BASE_HISTORICAL_FEE, 0)
+    );
+    assert_eq!(
+        compute_transaction_resource_fee(
+            &change_resource(|res: &mut TransactionResources| {
+                res.instructions = 123_451_234;
+            }),
+            &fee_config,
+        ),
+        (12_345_124 + BASE_HISTORICAL_FEE, 0)
+    );
+    assert_eq!(
+        compute_transaction_resource_fee(
+            &change_resource(|res: &mut TransactionResources| {
+                res.instructions = u32::MAX;
+            }),
+            &fee_config,
+        ),
+        (429_496_730 + BASE_HISTORICAL_FEE, 0)
+    );
+
+    // Read entries
+    assert_eq!(
+        compute_transaction_resource_fee(
+            &change_resource(|res: &mut TransactionResources| {
+                res.read_entries = 1;
+            }),
+            &fee_config,
+        ),
+        (2000 * 1 + BASE_HISTORICAL_FEE, 0)
+    );
+    assert_eq!(
+        compute_transaction_resource_fee(
+            &change_resource(|res: &mut TransactionResources| {
+                res.read_entries = 5;
+            }),
+            &fee_config,
+        ),
+        (2000 * 5 + BASE_HISTORICAL_FEE, 0)
+    );
+    assert_eq!(
+        compute_transaction_resource_fee(
+            &change_resource(|res: &mut TransactionResources| {
+                res.read_entries = u32::MAX;
+            }),
+            &fee_config,
+        ),
+        (8_589_934_590_000 + BASE_HISTORICAL_FEE, 0)
+    );
+
+    // Write entries
+    assert_eq!(
+        compute_transaction_resource_fee(
+            &change_resource(|res: &mut TransactionResources| {
+                res.write_entries = 1;
+            }),
+            &fee_config,
+        ),
+        // Write entries are also counted towards the read entry fee.
+        (2000 + 3000 + BASE_HISTORICAL_FEE, 0)
+    );
+    assert_eq!(
+        compute_transaction_resource_fee(
+            &change_resource(|res: &mut TransactionResources| {
+                res.write_entries = 5;
+            }),
+            &fee_config,
+        ),
+        ((2000 + 3000) * 5 + BASE_HISTORICAL_FEE, 0)
+    );
+    assert_eq!(
+        compute_transaction_resource_fee(
+            &change_resource(|res: &mut TransactionResources| {
+                res.write_entries = u32::MAX;
+            }),
+            &fee_config,
+        ),
+        (
+            8_589_934_590_000 + 12_884_901_885_000 + BASE_HISTORICAL_FEE,
+            0
+        )
+    );
+
+    // Read bytes
+    assert_eq!(
+        compute_transaction_resource_fee(
+            &change_resource(|res: &mut TransactionResources| {
+                res.read_bytes = 1;
+            }),
+            &fee_config,
+        ),
+        // ceil(1 * 4000 / 1024) = 4
+        (4 + BASE_HISTORICAL_FEE, 0)
+    );
+    assert_eq!(
+        compute_transaction_resource_fee(
+            &change_resource(|res: &mut TransactionResources| {
+                res.read_bytes = 5 * 1024;
+            }),
+            &fee_config,
+        ),
+        (5 * 4000 + BASE_HISTORICAL_FEE, 0)
+    );
+    assert_eq!(
+        compute_transaction_resource_fee(
+            &change_resource(|res: &mut TransactionResources| {
+                res.read_bytes = 5 * 1024 + 1;
+            }),
+            &fee_config,
+        ),
+        (20_004 + BASE_HISTORICAL_FEE, 0)
+    );
+    assert_eq!(
+        compute_transaction_resource_fee(
+            &change_resource(|res: &mut TransactionResources| {
+                res.read_bytes = u32::MAX;
+            }),
+            &fee_config,
+        ),
+        (16_777_215_997 + BASE_HISTORICAL_FEE, 0)
+    );
+
+    // Write bytes
+    assert_eq!(
+        compute_transaction_resource_fee(
+            &change_resource(|res: &mut TransactionResources| {
+                res.write_bytes = 1;
+            }),
+            &fee_config,
+        ),
+        // ceil(1 * 5000 / 1024) = 4
+        (5 + BASE_HISTORICAL_FEE, 0)
+    );
+    assert_eq!(
+        compute_transaction_resource_fee(
+            &change_resource(|res: &mut TransactionResources| {
+                res.write_bytes = 5 * 1024;
+            }),
+            &fee_config,
+        ),
+        (5 * 5000 + BASE_HISTORICAL_FEE, 0)
+    );
+    assert_eq!(
+        compute_transaction_resource_fee(
+            &change_resource(|res: &mut TransactionResources| {
+                res.write_bytes = 5 * 1024 + 1;
+            }),
+            &fee_config,
+        ),
+        (25_005 + BASE_HISTORICAL_FEE, 0)
+    );
+    assert_eq!(
+        compute_transaction_resource_fee(
+            &change_resource(|res: &mut TransactionResources| {
+                res.write_bytes = u32::MAX;
+            }),
+            &fee_config,
+        ),
+        (20_971_519_996 + BASE_HISTORICAL_FEE, 0)
+    );
+
+    // Transaction size (affected by historical + tx size fees)
+    assert_eq!(
+        compute_transaction_resource_fee(
+            &change_resource(|res: &mut TransactionResources| {
+                res.transaction_size_bytes = 1;
+            }),
+            &fee_config,
+        ),
+        // Historical fee: ceil(1 * 6000 / 1024) = 6
+        // Tx size fee: ceil(1 * 8000 / 1024) = 8
+        (6 + 8 + BASE_HISTORICAL_FEE, 0)
+    );
+
+    assert_eq!(
+        compute_transaction_resource_fee(
+            &change_resource(|res: &mut TransactionResources| {
+                res.transaction_size_bytes = 5 * 1024;
+            }),
+            &fee_config,
+        ),
+        ((6000 + 8000) * 5 + BASE_HISTORICAL_FEE, 0)
+    );
+
+    assert_eq!(
+        compute_transaction_resource_fee(
+            &change_resource(|res: &mut TransactionResources| {
+                res.transaction_size_bytes = 5 * 1024 + 1;
+            }),
+            &fee_config,
+        ),
+        (30_006 + 40_008 + BASE_HISTORICAL_FEE, 0)
+    );
+
+    assert_eq!(
+        compute_transaction_resource_fee(
+            &change_resource(|res: &mut TransactionResources| {
+                res.transaction_size_bytes = u32::MAX;
+            }),
+            &fee_config,
+        ),
+        // BASE_HISTORICAL_FEE is omitted as it's saturated with overall
+        // `transaction_size_bytes`.
+        (25_165_823_995 + 33_554_431_993, 0)
+    );
+
+    // Events size
+    assert_eq!(
+        compute_transaction_resource_fee(
+            &change_resource(|res: &mut TransactionResources| {
+                res.contract_events_size_bytes = 1;
+            }),
+            &fee_config,
+        ),
+        // ceil(1 * 7000 / 1024) = 7
+        (BASE_HISTORICAL_FEE, 7)
+    );
+    assert_eq!(
+        compute_transaction_resource_fee(
+            &change_resource(|res: &mut TransactionResources| {
+                res.contract_events_size_bytes = 5 * 1024;
+            }),
+            &fee_config,
+        ),
+        (BASE_HISTORICAL_FEE, 5 * 7000)
+    );
+    assert_eq!(
+        compute_transaction_resource_fee(
+            &change_resource(|res: &mut TransactionResources| {
+                res.contract_events_size_bytes = 5 * 1024 + 1;
+            }),
+            &fee_config,
+        ),
+        (BASE_HISTORICAL_FEE, 35_007)
+    );
+    assert_eq!(
+        compute_transaction_resource_fee(
+            &change_resource(|res: &mut TransactionResources| {
+                res.contract_events_size_bytes = u32::MAX;
+            }),
+            &fee_config,
+        ),
+        (BASE_HISTORICAL_FEE, 29_360_127_994)
     );
 }
 
@@ -299,7 +601,7 @@ fn test_rent_extend_fees_with_only_extend() {
                     new_size_bytes: 10 * 1024,
                     old_live_until_ledger: 100_000,
                     new_live_until_ledger: 300_000,
-                }
+                },
             ],
             &fee_config,
             50_000,
@@ -405,7 +707,7 @@ fn test_rent_extend_fees_with_only_size_change() {
                     new_size_bytes: 100_000,
                     old_live_until_ledger: 100_000,
                     new_live_until_ledger: 100_000,
-                }
+                },
             ],
             &fee_config,
             25_000,
@@ -479,7 +781,7 @@ fn test_rent_extend_with_size_change_and_extend() {
                     new_size_bytes: 100_000,
                     old_live_until_ledger: 100_000,
                     new_live_until_ledger: 300_000,
-                }
+                },
             ],
             &fee_config,
             25_000,