Repository: stellar/rs-soroban-env
Pull request #315 (2022-08-04 21:29:41)

Add metering to Val conversions, host function invocation
### What

- Adds a new budget type that covers for simple val conversion. E.g `RawVal` to a non-object `ScVal`, or to a `ScObject::U64`. With a test.
- Add `HostFunction` metering to the dispatcher functions. With a test.
- Add several testing utils functions to make it easy to test budget and metering.
- Refactored the invocation tests code to use `utils` functions.

### Why

Part of the bigger picture of budget metering.

### Known limitations

[TODO or N/A]



Patch:
diff --git a/soroban-env-host/src/budget.rs b/soroban-env-host/src/budget.rs
index 1f0e76fb..5ada5037 100644
--- a/soroban-env-host/src/budget.rs
+++ b/soroban-env-host/src/budget.rs
@@ -15,6 +15,10 @@ pub enum CostType {
     VisitObject = 8,
     PushFrame = 9,
     PopFrame = 10,
+    // Tracks a single Val (RawVal or primative Object like U64) <=> ScVal
+    // conversion cost. Most of these Val counterparts in ScVal (except e.g.
+    // Symbol) consumes a single int64 and therefore is a constant overhead.
+    ValXdrConv = 11,
 }
 
 // TODO: add XDR support for iterating over all the elements of an enum
@@ -32,6 +36,7 @@ impl CostType {
             CostType::VisitObject,
             CostType::PushFrame,
             CostType::PopFrame,
+            CostType::ValXdrConv,
         ];
         VARIANTS.iter()
     }
@@ -83,6 +88,15 @@ impl CostModel {
         }
         res
     }
+
+    #[cfg(test)]
+    pub fn reset(&mut self) {
+        self.const_param = 0;
+        self.log_param = 0;
+        self.log_base_param = 0;
+        self.lin_param = 0;
+        self.quad_param = 0;
+    }
 }
 
 // Returns the next power of 2 >= the input, avoiding overflow and returning 2
@@ -161,9 +175,6 @@ impl BudgetDimension {
     }
 
     pub fn charge(&mut self, ty: CostType, input: u64) -> Result<(), HostError> {
-        if self.limit == u64::MAX {
-            return Ok(());
-        }
         let cm = self.get_cost_model(ty);
         self.count = self.count.saturating_add(cm.evaluate(input));
         if self.is_over_budget() {
@@ -173,6 +184,14 @@ impl BudgetDimension {
             Ok(())
         }
     }
+
+    // Resets all model parameters to zero (so that we can override and test individual ones later).
+    #[cfg(test)]
+    pub fn reset_models(&mut self) {
+        for model in &mut self.cost_models {
+            model.reset()
+        }
+    }
 }
 
 impl Default for BudgetDimension {
@@ -228,6 +247,19 @@ impl Budget {
             *i = 0;
         }
     }
+
+    #[cfg(test)]
+    pub fn reset_limits(&mut self, cpu: u64, mem: u64) {
+        self.cpu_insns.reset(cpu);
+        self.mem_bytes.reset(mem);
+        self.reset_inputs()
+    }
+
+    #[cfg(test)]
+    pub fn reset_models(&mut self) {
+        self.cpu_insns.reset_models();
+        self.mem_bytes.reset_models();
+    }
 }
 
 impl Default for Budget {
@@ -250,7 +282,19 @@ impl Default for Budget {
         b.cpu_insns
             .get_cost_model_mut(CostType::WasmInsnExec)
             .lin_param = 73;
-        // TODO: set cpu, mem model parameters for other CostTypes
+        // TODO: to be calibrated
+        b.cpu_insns
+            .get_cost_model_mut(CostType::ValXdrConv)
+            .lin_param = 1;
+        b.mem_bytes
+            .get_cost_model_mut(CostType::ValXdrConv)
+            .lin_param = 1;
+        b.cpu_insns
+            .get_cost_model_mut(CostType::ValXdrConv)
+            .const_param = 100;
+        b.mem_bytes
+            .get_cost_model_mut(CostType::ValXdrConv)
+            .const_param = 10;
 
         // Some "reasonable defaults": 640k of RAM and 100usec.
         //
diff --git a/soroban-env-host/src/host.rs b/soroban-env-host/src/host.rs
index 247b133d..5e8f6063 100644
--- a/soroban-env-host/src/host.rs
+++ b/soroban-env-host/src/host.rs
@@ -347,11 +347,6 @@ impl Host {
         }
     }
 
-    pub(crate) fn from_host_val(&self, val: RawVal) -> Result<ScVal, HostError> {
-        ScVal::try_from_val(self, val)
-            .map_err(|_| self.err_status(ScHostValErrorCode::UnknownError))
-    }
-
     // Testing interface to create values directly for later use via Env functions.
     pub fn inject_val(&self, v: &ScVal) -> Result<RawVal, HostError> {
         self.to_host_val(v).map(Into::into)
@@ -374,7 +369,19 @@ impl Host {
         })
     }
 
+    pub(crate) fn from_host_val(&self, val: RawVal) -> Result<ScVal, HostError> {
+        // Charges a single unit to for the RawVal -> ScVal conversion.
+        // The actual conversion logic occurs in the `common` crate, which
+        // translates a u64 into another form defined by the xdr.
+        // For an `Object`, the actual structural conversion (such as byte
+        // cloning) occurs in `from_host_obj` and is metered there.
+        self.charge_budget(CostType::ValXdrConv, 1)?;
+        ScVal::try_from_val(self, val)
+            .map_err(|_| self.err_status(ScHostValErrorCode::UnknownError))
+    }
+
     pub(crate) fn to_host_val(&self, v: &ScVal) -> Result<HostVal, HostError> {
+        self.charge_budget(CostType::ValXdrConv, 1)?;
         let rv = v
             .try_into_val(self)
             .map_err(|_| self.err_status(ScHostValErrorCode::UnknownError))?;
@@ -383,48 +390,58 @@ impl Host {
 
     pub(crate) fn from_host_obj(&self, ob: Object) -> Result<ScObject, HostError> {
         unsafe {
-            self.unchecked_visit_val_obj(ob.into(), |ob| match ob {
-                None => Err(self.err_status(ScHostObjErrorCode::UnknownReference)),
-                Some(ho) => match ho {
-                    HostObject::Vec(vv) => {
-                        let mut sv = Vec::new();
-                        for e in vv.iter() {
-                            sv.push(self.from_host_val(e.val)?);
+            self.unchecked_visit_val_obj(ob.into(), |ob| {
+                // This accounts for conversion of "primitive" objects (e.g U64)
+                // and the "shell" of a complex object (ScMap). Any non-trivial
+                // work such as byte cloning, has to be accounted for and
+                // metered in indivial match arms.
+                self.charge_budget(CostType::ValXdrConv, 1)?;
+                match ob {
+                    None => Err(self.err_status(ScHostObjErrorCode::UnknownReference)),
+                    Some(ho) => match ho {
+                        HostObject::Vec(vv) => {
+                            let mut sv = Vec::new();
+                            for e in vv.iter() {
+                                sv.push(self.from_host_val(e.val)?);
+                            }
+                            Ok(ScObject::Vec(ScVec(self.map_err(sv.try_into())?)))
                         }
-                        Ok(ScObject::Vec(ScVec(self.map_err(sv.try_into())?)))
-                    }
-                    HostObject::Map(mm) => {
-                        let mut mv = Vec::new();
-                        for (k, v) in mm.iter() {
-                            let key = self.from_host_val(k.val)?;
-                            let val = self.from_host_val(v.val)?;
-                            mv.push(ScMapEntry { key, val });
+                        HostObject::Map(mm) => {
+                            let mut mv = Vec::new();
+                            for (k, v) in mm.iter() {
+                                let key = self.from_host_val(k.val)?;
+                                let val = self.from_host_val(v.val)?;
+                                mv.push(ScMapEntry { key, val });
+                            }
+                            Ok(ScObject::Map(ScMap(self.map_err(mv.try_into())?)))
                         }
-                        Ok(ScObject::Map(ScMap(self.map_err(mv.try_into())?)))
-                    }
-                    HostObject::U64(u) => Ok(ScObject::U64(*u)),
-                    HostObject::I64(i) => Ok(ScObject::I64(*i)),
-                    HostObject::Bin(b) => Ok(ScObject::Binary(self.map_err(b.clone().try_into())?)),
-                    HostObject::BigInt(bi) => {
-                        let (sign, data) = bi.to_bytes_be();
-                        match sign {
-                            Sign::Minus => Ok(ScObject::BigInt(ScBigInt::Negative(
-                                self.map_err(data.try_into())?,
-                            ))),
-                            Sign::NoSign => Ok(ScObject::BigInt(ScBigInt::Zero)),
-                            Sign::Plus => Ok(ScObject::BigInt(ScBigInt::Positive(
-                                self.map_err(data.try_into())?,
-                            ))),
+                        HostObject::U64(u) => Ok(ScObject::U64(*u)),
+                        HostObject::I64(i) => Ok(ScObject::I64(*i)),
+                        HostObject::Bin(b) => {
+                            Ok(ScObject::Binary(self.map_err(b.clone().try_into())?))
                         }
-                    }
-                    HostObject::Hash(_) => todo!(),
-                    HostObject::PublicKey(_) => todo!(),
-                },
+                        HostObject::BigInt(bi) => {
+                            let (sign, data) = bi.to_bytes_be();
+                            match sign {
+                                Sign::Minus => Ok(ScObject::BigInt(ScBigInt::Negative(
+                                    self.map_err(data.try_into())?,
+                                ))),
+                                Sign::NoSign => Ok(ScObject::BigInt(ScBigInt::Zero)),
+                                Sign::Plus => Ok(ScObject::BigInt(ScBigInt::Positive(
+                                    self.map_err(data.try_into())?,
+                                ))),
+                            }
+                        }
+                        HostObject::Hash(_) => todo!(),
+                        HostObject::PublicKey(_) => todo!(),
+                    },
+                }
             })
         }
     }
 
     pub(crate) fn to_host_obj(&self, ob: &ScObject) -> Result<HostObj, HostError> {
+        self.charge_budget(CostType::ValXdrConv, 1)?;
         match ob {
             ScObject::Vec(v) => {
                 let mut vv = Vector::new();
@@ -766,7 +783,6 @@ impl CheckedEnv for Host {
     }
 
     fn obj_cmp(&self, a: RawVal, b: RawVal) -> Result<i64, HostError> {
-        self.charge_budget(CostType::HostFunction, 2)?;
         let res = unsafe {
             self.unchecked_visit_val_obj(a, |ao| {
                 self.unchecked_visit_val_obj(b, |bo| Ok(ao.cmp(&bo)))
diff --git a/soroban-env-host/src/vm/dispatch.rs b/soroban-env-host/src/vm/dispatch.rs
index bff6e1e2..efca4960 100644
--- a/soroban-env-host/src/vm/dispatch.rs
+++ b/soroban-env-host/src/vm/dispatch.rs
@@ -1,4 +1,4 @@
-use crate::{CheckedEnv, Host, Object, RawVal, Symbol};
+use crate::{budget::CostType, CheckedEnv, Host, Object, RawVal, Symbol};
 use soroban_env_common::call_macro_with_all_host_functions;
 use wasmi::{RuntimeArgs, RuntimeValue};
 
@@ -120,6 +120,7 @@ macro_rules! generate_dispatch_functions {
                 pub(crate) fn $fn_id(host: &mut Host, _vmargs: RuntimeArgs) ->
                     Result<RuntimeValue, wasmi::Trap>
                 {
+                    host.charge_budget(CostType::HostFunction, _vmargs.len() as u64)?;
                     Ok(dispatch_function_helper!{host, _vmargs, fn $fn_id $args }?.into())
                 }
             )*


Test patch:
diff --git a/soroban-env-host/src/test.rs b/soroban-env-host/src/test.rs
index 8684794e..a7fbd927 100644
--- a/soroban-env-host/src/test.rs
+++ b/soroban-env-host/src/test.rs
@@ -7,6 +7,8 @@ mod crypto;
 mod map;
 mod vec;
 
+#[cfg(feature = "vm")]
+mod budget_metering;
 #[cfg(feature = "vm")]
 mod invocation;
 #[cfg(all(feature = "vm", feature = "testutils"))]
diff --git a/soroban-env-host/src/test/budget_metering.rs b/soroban-env-host/src/test/budget_metering.rs
new file mode 100644
index 00000000..b0fb5c83
--- /dev/null
+++ b/soroban-env-host/src/test/budget_metering.rs
@@ -0,0 +1,60 @@
+use super::wasm_examples::VEC;
+use crate::{
+    budget::CostType,
+    xdr::{ScMap, ScMapEntry, ScObject, ScVal},
+    CheckedEnv, Host, HostError, Symbol,
+};
+
+#[test]
+fn xdr_object_conversion() -> Result<(), HostError> {
+    let host = Host::test_host()
+        .test_budget()
+        .enable_model(CostType::ValXdrConv);
+    let scmap: ScMap = host.map_err(
+        vec![
+            ScMapEntry {
+                key: ScVal::U32(1),
+                val: ScVal::U32(2),
+            },
+            ScMapEntry {
+                key: ScVal::U32(2),
+                val: ScVal::U32(4),
+            },
+        ]
+        .try_into(),
+    )?;
+    let scobj = ScObject::Map(scmap);
+    host.to_host_obj(&scobj)?;
+
+    host.get_budget(|budget| {
+        assert_eq!(budget.get_input(CostType::ValXdrConv), 5);
+        assert_eq!(budget.cpu_insns.get_count(), 50);
+        assert_eq!(budget.mem_bytes.get_count(), 5);
+    });
+    Ok(())
+}
+
+#[test]
+fn vm_hostfn_invocation() -> Result<(), HostError> {
+    let dummy_id = [0; 32];
+    let storage = Host::test_storage_with_contracts(vec![dummy_id.into()], vec![VEC]);
+    let host = Host::with_storage(storage)
+        .test_budget()
+        .enable_model(CostType::HostFunction);
+
+    let obj = host.test_bin_obj(&dummy_id)?;
+    // `vec_err` is a test contract function which calls `vec_new` (1 param)
+    // and `vec_put` (3 params) so total 4 inputs to the budget from `CostType::HostFunction`.
+    let sym = Symbol::from_str("vec_err");
+    let args = host.test_vec_obj::<u32>(&[1])?;
+
+    // try_call
+    host.try_call(obj.to_object(), sym.into(), args.clone().into())?;
+    host.get_budget(|budget| {
+        assert_eq!(budget.get_input(CostType::HostFunction), 4);
+        assert_eq!(budget.cpu_insns.get_count(), 40);
+        assert_eq!(budget.mem_bytes.get_count(), 4);
+    });
+
+    Ok(())
+}
diff --git a/soroban-env-host/src/test/invocation.rs b/soroban-env-host/src/test/invocation.rs
index 6509397e..e9dc0052 100644
--- a/soroban-env-host/src/test/invocation.rs
+++ b/soroban-env-host/src/test/invocation.rs
@@ -1,12 +1,7 @@
 use super::wasm_examples::{ADD_I32, CALL, VEC};
 use crate::{
-    im_rc::OrdMap,
-    storage::{AccessType, Footprint, Storage},
     vm::Vm,
-    xdr::{
-        ContractDataEntry, Hash, LedgerEntry, LedgerEntryData, LedgerEntryExt, LedgerKey,
-        LedgerKeyContractData, ScHostObjErrorCode, ScObject, ScStatic, ScVal, ScVec,
-    },
+    xdr::{Hash, ScHostObjErrorCode, ScVal, ScVec},
     CheckedEnv, Host, HostError, Status, Symbol, Tag,
 };
 
@@ -36,37 +31,13 @@ fn invoke_single_contract_function() -> Result<(), HostError> {
 
 #[test]
 fn invoke_cross_contract() -> Result<(), HostError> {
-    let contract_id: Hash = [0; 32].into();
-    let key = ScVal::Static(ScStatic::LedgerKeyContractCodeWasm);
-    let storage_key = LedgerKey::ContractData(LedgerKeyContractData {
-        contract_id: contract_id.clone(),
-        key: key.clone(),
-    });
-    // We unwrap here rather than host.map_err because the host doesn't exist yet.
-    let scob = ScObject::Binary(ADD_I32.try_into().unwrap());
-    let val = ScVal::Object(Some(scob));
-    let le = LedgerEntry {
-        last_modified_ledger_seq: 0,
-        data: LedgerEntryData::ContractData(ContractDataEntry {
-            contract_id,
-            key,
-            val,
-        }),
-        ext: LedgerEntryExt::V0,
-    };
-    let map = OrdMap::unit(storage_key.clone(), Some(le));
-    let mut footprint = Footprint::default();
-    footprint.record_access(&storage_key, AccessType::ReadOnly);
-
-    // initialize storage and host
-    let storage = Storage::with_enforcing_footprint_and_map(footprint, map);
+    let dummy_id = [0; 32];
+    let storage = Host::test_storage_with_contracts(vec![dummy_id.into()], vec![ADD_I32]);
     let host = Host::with_storage(storage);
-    // create a dummy contract obj as the caller
-    let obj = host.test_bin_obj(&[0; 32])?;
+    let obj = host.test_bin_obj(&dummy_id)?;
     // prepare arguments
     let sym = Symbol::from_str("add");
     let args = host.test_vec_obj::<i32>(&[1, 2])?;
-
     let res = host.call(obj.to_object(), sym.into(), args.into())?;
     assert!(res.is::<i32>());
     assert!(res.get_tag() == Tag::I32);
@@ -77,34 +48,11 @@ fn invoke_cross_contract() -> Result<(), HostError> {
 
 #[test]
 fn invoke_cross_contract_with_err() -> Result<(), HostError> {
-    let contract_id: Hash = [0; 32].into();
-    let key = ScVal::Static(ScStatic::LedgerKeyContractCodeWasm);
-    let storage_key = LedgerKey::ContractData(LedgerKeyContractData {
-        contract_id: contract_id.clone(),
-        key: key.clone(),
-    });
-    // We unwrap here rather than host.map_err because the host doesn't exist yet.
-    let scob = ScObject::Binary(VEC.try_into().unwrap());
-    let val = ScVal::Object(Some(scob));
-    let le = LedgerEntry {
-        last_modified_ledger_seq: 0,
-        data: LedgerEntryData::ContractData(ContractDataEntry {
-            contract_id,
-            key,
-            val,
-        }),
-        ext: LedgerEntryExt::V0,
-    };
-    let map = OrdMap::unit(storage_key.clone(), Some(le));
-
-    let mut footprint = Footprint::default();
-    footprint.record_access(&storage_key, AccessType::ReadOnly);
-
-    // initialize storage and host
-    let storage = Storage::with_enforcing_footprint_and_map(footprint, map);
+    let dummy_id = [0; 32];
+    let storage = Host::test_storage_with_contracts(vec![dummy_id.into()], vec![VEC]);
     let host = Host::with_storage(storage);
-    // create a dummy contract obj as the caller
-    let obj = host.test_bin_obj(&[0; 32])?;
+
+    let obj = host.test_bin_obj(&dummy_id)?;
     // prepare arguments
     let sym = Symbol::from_str("vec_err");
     let args = host.test_vec_obj::<u32>(&[1])?;
@@ -124,55 +72,15 @@ fn invoke_cross_contract_with_err() -> Result<(), HostError> {
 
 #[test]
 fn invoke_cross_contract_lvl2_nested_with_err() -> Result<(), HostError> {
-    // 1st level, the calling contract
-    let id0: Hash = [0; 32].into();
-    let key = ScVal::Static(ScStatic::LedgerKeyContractCodeWasm);
-    let storage_key0 = LedgerKey::ContractData(LedgerKeyContractData {
-        contract_id: id0.clone(),
-        key: key.clone(),
-    });
-    let scob0 = ScObject::Binary(CALL.try_into().unwrap());
-    let val0 = ScVal::Object(Some(scob0));
-    let le0 = LedgerEntry {
-        last_modified_ledger_seq: 0,
-        data: LedgerEntryData::ContractData(ContractDataEntry {
-            contract_id: id0,
-            key: key.clone(),
-            val: val0,
-        }),
-        ext: LedgerEntryExt::V0,
-    };
-
-    // 2nd level, the guest contract
-    let id1: Hash = [1; 32].into();
-    let storage_key1 = LedgerKey::ContractData(LedgerKeyContractData {
-        contract_id: id1.clone(),
-        key: key.clone(),
-    });
-    let scob1 = ScObject::Binary(VEC.try_into().unwrap());
-    let val1 = ScVal::Object(Some(scob1));
-    let le1 = LedgerEntry {
-        last_modified_ledger_seq: 0,
-        data: LedgerEntryData::ContractData(ContractDataEntry {
-            contract_id: id1,
-            key,
-            val: val1,
-        }),
-        ext: LedgerEntryExt::V0,
-    };
-
-    // create storage map and footprint
-    let mut map = OrdMap::unit(storage_key0.clone(), Some(le0));
-    map.insert(storage_key1.clone(), Some(le1));
-    let mut footprint = Footprint::default();
-    footprint.record_access(&storage_key0, AccessType::ReadOnly);
-    footprint.record_access(&storage_key1, AccessType::ReadOnly);
-
-    // initialize storage and host
-    let storage = Storage::with_enforcing_footprint_and_map(footprint, map);
+    let dummy_id0 = [0; 32]; // the calling contract
+    let dummy_id1 = [1; 32]; // the called (failing) contract
+    let storage = Host::test_storage_with_contracts(
+        vec![dummy_id0.into(), dummy_id1.into()],
+        vec![CALL, VEC],
+    );
     let host = Host::with_storage(storage);
     // prepare arguments
-    let obj = host.test_bin_obj(&[0; 32])?;
+    let obj = host.test_bin_obj(&dummy_id0)?;
     let sym = Symbol::from_str("delegate");
     let args = host.test_vec_obj::<u32>(&[1])?;
     // try call
diff --git a/soroban-env-host/src/test/util.rs b/soroban-env-host/src/test/util.rs
index e8730fe7..b182b001 100644
--- a/soroban-env-host/src/test/util.rs
+++ b/soroban-env-host/src/test/util.rs
@@ -1,6 +1,12 @@
 use crate::{
+    budget::CostType,
     host_object::{HostObj, HostVal},
-    xdr::{ScObject, ScVal, ScVec},
+    im_rc::OrdMap,
+    storage::{AccessType, Footprint, Storage},
+    xdr::{
+        ContractDataEntry, Hash, LedgerEntry, LedgerEntryData, LedgerEntryExt, LedgerKey,
+        LedgerKeyContractData, ScObject, ScStatic, ScVal, ScVec,
+    },
     Host, HostError,
 };
 
@@ -21,7 +27,59 @@ impl AsScVal for i32 {
     }
 }
 
+#[allow(dead_code)]
 impl Host {
+    pub(crate) fn test_host() -> Self {
+        Host::default()
+    }
+
+    pub(crate) fn test_budget(self) -> Self {
+        self.get_budget_mut(|budget| {
+            budget.reset_limits(100_000, 100_000); // something big but finite that we may exceed
+            budget.reset_models();
+        });
+        self
+    }
+
+    pub(crate) fn enable_model(self, ty: CostType) -> Self {
+        self.get_budget_mut(|budget| {
+            budget.cpu_insns.get_cost_model_mut(ty).lin_param = 10;
+            budget.mem_bytes.get_cost_model_mut(ty).lin_param = 1;
+        });
+        self
+    }
+
+    pub(crate) fn test_storage_with_contracts(
+        ids: Vec<Hash>,
+        codes: Vec<&'static [u8]>,
+    ) -> Storage {
+        let it = ids.iter().zip(codes.iter());
+        let mut footprint = Footprint::default();
+        let mut map = OrdMap::default();
+        for (_, (id, contract)) in it.enumerate() {
+            // We unwrap here rather than host.map_err because the host doesn't exist yet.
+            let contract = ScObject::Binary((*contract).try_into().unwrap());
+            let key = ScVal::Static(ScStatic::LedgerKeyContractCodeWasm);
+            let storage_key = LedgerKey::ContractData(LedgerKeyContractData {
+                contract_id: id.clone(),
+                key: key.clone(),
+            });
+            let val = ScVal::Object(Some(contract));
+            let le = LedgerEntry {
+                last_modified_ledger_seq: 0,
+                data: LedgerEntryData::ContractData(ContractDataEntry {
+                    contract_id: id.clone(),
+                    key,
+                    val,
+                }),
+                ext: LedgerEntryExt::V0,
+            };
+            map.insert(storage_key.clone(), Some(le));
+            footprint.record_access(&storage_key, AccessType::ReadOnly);
+        }
+        Storage::with_enforcing_footprint_and_map(footprint, map)
+    }
+
     pub(crate) fn test_scvec<T: AsScVal>(&self, vals: &[T]) -> Result<ScVec, HostError> {
         let v: Vec<ScVal> = vals.iter().map(|x| x.as_scval()).collect();
         self.map_err(v.try_into())