Repository: stellar/rs-soroban-env
Pull request #1079 (2023-09-21 00:02:30)

Calibration for PRNG and charge budget
### What

Resolves https://github.com/stellar/rs-soroban-env/issues/1059

~Still waiting on the xdr changes (https://github.com/stellar/stellar-xdr/pull/151 and rs-stellar-xdr) to use the right cost type. Currently using Sha256 cost type as placeholder.~

Added a couple of minor improvements and cleanups.

### Why

[TODO: Why this change is being made. Include any context required to understand the why.]

### Known limitations

[TODO or N/A]



Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 21f43a86..6a38d8e0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1490,7 +1490,7 @@ dependencies = [
 [[package]]
 name = "stellar-xdr"
 version = "20.0.0-rc1"
-source = "git+https://github.com/stellar/rs-stellar-xdr?rev=d5ce0c9e7aa83461773a6e81662067f35d39e4c1#d5ce0c9e7aa83461773a6e81662067f35d39e4c1"
+source = "git+https://github.com/stellar/rs-stellar-xdr?rev=c9d24eb4fae1f9968f6620e06750c1fce7a5d6c3#c9d24eb4fae1f9968f6620e06750c1fce7a5d6c3"
 dependencies = [
  "arbitrary",
  "base64 0.13.1",
diff --git a/Cargo.toml b/Cargo.toml
index d9837ab8..649ef78f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -28,7 +28,7 @@ soroban-native-sdk-macros = { version = "20.0.0-rc2", path = "soroban-native-sdk
 [workspace.dependencies.stellar-xdr]
 version = "20.0.0-rc1"
 git = "https://github.com/stellar/rs-stellar-xdr"
-rev = "d5ce0c9e7aa83461773a6e81662067f35d39e4c1"
+rev = "c9d24eb4fae1f9968f6620e06750c1fce7a5d6c3"
 default-features = false
 
 [workspace.dependencies.wasmi]
diff --git a/soroban-env-host/benches/common/cost_types/mod.rs b/soroban-env-host/benches/common/cost_types/mod.rs
index 32a7aed4..128bd36a 100644
--- a/soroban-env-host/benches/common/cost_types/mod.rs
+++ b/soroban-env-host/benches/common/cost_types/mod.rs
@@ -9,6 +9,7 @@ mod host_mem_cpy;
 mod invoke;
 mod map_ops;
 mod num_ops;
+mod prng;
 mod recover_ecdsa_secp256k1_key;
 mod val_deser;
 mod val_ser;
@@ -29,6 +30,7 @@ pub(crate) use host_mem_cpy::*;
 pub(crate) use invoke::*;
 pub(crate) use map_ops::*;
 pub(crate) use num_ops::*;
+pub(crate) use prng::*;
 pub(crate) use recover_ecdsa_secp256k1_key::*;
 pub(crate) use val_deser::*;
 pub(crate) use val_ser::*;
diff --git a/soroban-env-host/benches/common/cost_types/prng.rs b/soroban-env-host/benches/common/cost_types/prng.rs
new file mode 100644
index 00000000..4b1e3a67
--- /dev/null
+++ b/soroban-env-host/benches/common/cost_types/prng.rs
@@ -0,0 +1,18 @@
+use crate::common::HostCostMeasurement;
+use rand::{rngs::StdRng, SeedableRng};
+use rand_chacha::ChaCha20Rng;
+use soroban_env_host::{cost_runner::ChaCha20DrawBytesRun, Host};
+
+pub(crate) struct ChaCha20DrawBytesMeasure;
+
+impl HostCostMeasurement for ChaCha20DrawBytesMeasure {
+    type Runner = ChaCha20DrawBytesRun;
+
+    fn new_random_case(_host: &Host, _rng: &mut StdRng, input: u64) -> (ChaCha20Rng, Vec<u8>) {
+        let size = 1 + input * Self::STEP_SIZE;
+        let seed = [0u8; 32];
+        let rng = ChaCha20Rng::from_seed(seed);
+        let dest = vec![0u8; size as usize];
+        (rng, dest)
+    }
+}
diff --git a/soroban-env-host/benches/common/mod.rs b/soroban-env-host/benches/common/mod.rs
index e8c852d5..bd320dc8 100644
--- a/soroban-env-host/benches/common/mod.rs
+++ b/soroban-env-host/benches/common/mod.rs
@@ -87,6 +87,7 @@ pub(crate) fn for_each_host_cost_measurement<B: Benchmark>(
     call_bench::<B, Int256DivMeasure>(&mut params)?;
     call_bench::<B, Int256PowMeasure>(&mut params)?;
     call_bench::<B, Int256ShiftMeasure>(&mut params)?;
+    call_bench::<B, ChaCha20DrawBytesMeasure>(&mut params)?;
 
     if get_explicit_bench_names().is_none() {
         for cost in ContractCostType::variants() {
diff --git a/soroban-env-host/benches/common/modelfit.rs b/soroban-env-host/benches/common/modelfit.rs
index afe20863..1f8b18c9 100644
--- a/soroban-env-host/benches/common/modelfit.rs
+++ b/soroban-env-host/benches/common/modelfit.rs
@@ -1,4 +1,3 @@
-use log::info;
 use std::collections::HashSet;
 use std::str::FromStr;
 
diff --git a/soroban-env-host/benches/common/util.rs b/soroban-env-host/benches/common/util.rs
index ef2501fa..9dce9d51 100644
--- a/soroban-env-host/benches/common/util.rs
+++ b/soroban-env-host/benches/common/util.rs
@@ -1,7 +1,12 @@
-use soroban_env_host::{budget::AsBudget, Host, Val, I256};
+use soroban_env_host::{budget::AsBudget, meta, Host, LedgerInfo, Val, I256};
 
 pub(crate) fn test_host() -> Host {
     let host = Host::default();
+    host.set_ledger_info(LedgerInfo {
+        protocol_version: meta::get_ledger_protocol_version(meta::INTERFACE_VERSION),
+        ..Default::default()
+    })
+    .unwrap();
     host.as_budget().reset_unlimited().unwrap();
     host.as_budget().reset_fuel_config().unwrap();
     host
diff --git a/soroban-env-host/src/budget.rs b/soroban-env-host/src/budget.rs
index 21592b68..6c01d0c5 100644
--- a/soroban-env-host/src/budget.rs
+++ b/soroban-env-host/src/budget.rs
@@ -430,6 +430,7 @@ impl BudgetImpl {
                 ContractCostType::Int256Div => (),
                 ContractCostType::Int256Pow => (),
                 ContractCostType::Int256Shift => (),
+                ContractCostType::ChaCha20DrawBytes => init_input(i), // number of random bytes to draw
             }
         }
     }
@@ -619,6 +620,10 @@ impl Default for BudgetImpl {
                     cpu.const_term = 412;
                     cpu.lin_term = ScaledU64(0);
                 }
+                ContractCostType::ChaCha20DrawBytes => {
+                    cpu.const_term = 4907;
+                    cpu.lin_term = ScaledU64(2461);
+                }
             }
 
             // define the memory cost model parameters
@@ -740,6 +745,10 @@ impl Default for BudgetImpl {
                     mem.const_term = 119;
                     mem.lin_term = ScaledU64(0);
                 }
+                ContractCostType::ChaCha20DrawBytes => {
+                    mem.const_term = 0;
+                    mem.lin_term = ScaledU64(0);
+                }
             }
 
             b.init_tracker();
@@ -1102,6 +1111,13 @@ impl Budget {
         )
     }
 
+    /// Resets the `FuelConfig` we pass into Wasmi before running calibration.
+    /// Wasmi instruction calibration requires running the same Wasmi insn
+    /// a fixed number of times, record their actual cpu and mem consumption, then
+    /// divide those numbers by the number of iterations, which is the fuel count.
+    /// Fuel count is kept tracked on the Wasmi side, based on the `FuelConfig`
+    /// of a specific fuel category. In order to get the correct, unscaled fuel
+    /// count, we have to preset all the `FuelConfig` entries to 1.
     #[cfg(any(test, feature = "testutils"))]
     pub fn reset_fuel_config(&self) -> Result<(), HostError> {
         self.0.try_borrow_mut_or_err()?.fuel_config.reset();
@@ -1113,7 +1129,7 @@ impl Budget {
     }
 
     // generate a wasmi fuel cost schedule based on our calibration
-    pub fn wasmi_fuel_costs(&self) -> Result<FuelCosts, HostError> {
+    pub(crate) fn wasmi_fuel_costs(&self) -> Result<FuelCosts, HostError> {
         let config = &self.0.try_borrow_or_err()?.fuel_config;
         let mut costs = FuelCosts::default();
         costs.base = config.base;
diff --git a/soroban-env-host/src/cost_runner/cost_types/compute_sha256_hash.rs b/soroban-env-host/src/cost_runner/cost_types/compute_sha256_hash.rs
index e9dc0c57..cd770e2c 100644
--- a/soroban-env-host/src/cost_runner/cost_types/compute_sha256_hash.rs
+++ b/soroban-env-host/src/cost_runner/cost_types/compute_sha256_hash.rs
@@ -1,6 +1,6 @@
 use std::hint::black_box;
 
-use crate::{cost_runner::CostRunner, xdr::ContractCostType};
+use crate::{cost_runner::CostRunner, host::crypto::sha256_hash_from_bytes, xdr::ContractCostType};
 
 pub struct ComputeSha256HashRun;
 
@@ -12,10 +12,7 @@ impl CostRunner for ComputeSha256HashRun {
     type RecycledType = (Option<Vec<u8>>, Vec<u8>);
 
     fn run_iter(host: &crate::Host, _iter: u64, sample: Self::SampleType) -> Self::RecycledType {
-        let hash = black_box(
-            host.sha256_hash_from_bytes(sample.as_slice())
-                .expect("sha256"),
-        );
+        let hash = black_box(sha256_hash_from_bytes(sample.as_slice(), host).expect("sha256"));
         (Some(hash), sample)
     }
 
diff --git a/soroban-env-host/src/cost_runner/cost_types/mod.rs b/soroban-env-host/src/cost_runner/cost_types/mod.rs
index 8c641699..620f8fa4 100644
--- a/soroban-env-host/src/cost_runner/cost_types/mod.rs
+++ b/soroban-env-host/src/cost_runner/cost_types/mod.rs
@@ -9,6 +9,7 @@ mod host_mem_cpy;
 mod invoke;
 mod map_ops;
 mod num_ops;
+mod prng;
 mod recover_ecdsa_secp256k1_key;
 mod val_deser;
 mod val_ser;
@@ -29,6 +30,7 @@ pub use host_mem_cpy::*;
 pub use invoke::*;
 pub use map_ops::*;
 pub use num_ops::*;
+pub use prng::*;
 pub use recover_ecdsa_secp256k1_key::*;
 pub use val_deser::*;
 pub use val_ser::*;
diff --git a/soroban-env-host/src/cost_runner/cost_types/prng.rs b/soroban-env-host/src/cost_runner/cost_types/prng.rs
new file mode 100644
index 00000000..4293f5e1
--- /dev/null
+++ b/soroban-env-host/src/cost_runner/cost_types/prng.rs
@@ -0,0 +1,36 @@
+use std::hint::black_box;
+
+use rand_chacha::ChaCha20Rng;
+
+use crate::{cost_runner::CostRunner, host::crypto::chacha20_fill_bytes, xdr::ContractCostType};
+
+pub struct ChaCha20DrawBytesRun;
+
+impl CostRunner for ChaCha20DrawBytesRun {
+    const COST_TYPE: ContractCostType = ContractCostType::ChaCha20DrawBytes;
+
+    type SampleType = (ChaCha20Rng, Vec<u8>);
+
+    type RecycledType = Self::SampleType;
+
+    fn run_iter(
+        host: &crate::Host,
+        _iter: u64,
+        mut sample: Self::SampleType,
+    ) -> Self::RecycledType {
+        black_box(
+            chacha20_fill_bytes(&mut sample.0, sample.1.as_mut_slice(), host)
+                .expect("chacha20 draw byte"),
+        );
+        black_box(sample)
+    }
+
+    fn run_baseline_iter(
+        host: &crate::Host,
+        _iter: u64,
+        sample: Self::SampleType,
+    ) -> Self::RecycledType {
+        black_box(host.charge_budget(Self::COST_TYPE, Some(0)).unwrap());
+        black_box(sample)
+    }
+}
diff --git a/soroban-env-host/src/host.rs b/soroban-env-host/src/host.rs
index 3f06cade..c597747d 100644
--- a/soroban-env-host/src/host.rs
+++ b/soroban-env-host/src/host.rs
@@ -746,8 +746,7 @@ impl Host {
     }
 
     fn upload_contract_wasm(&self, wasm: Vec<u8>) -> Result<BytesObject, HostError> {
-        let hash_bytes: [u8; 32] = self
-            .sha256_hash_from_bytes(wasm.as_slice())?
+        let hash_bytes: [u8; 32] = crypto::sha256_hash_from_bytes(wasm.as_slice(), self)?
             .try_into()
             .map_err(|_| {
                 self.err(
diff --git a/soroban-env-host/src/host/crypto.rs b/soroban-env-host/src/host/crypto.rs
index 99819a25..5db712d3 100644
--- a/soroban-env-host/src/host/crypto.rs
+++ b/soroban-env-host/src/host/crypto.rs
@@ -1,9 +1,11 @@
 use crate::{
-    budget::Budget,
+    budget::AsBudget,
     err,
     xdr::{ContractCostType, Hash, ScBytes, ScErrorCode, ScErrorType},
     BytesObject, Host, HostError, U32Val, Val,
 };
+use rand::RngCore;
+use rand_chacha::ChaCha20Rng;
 use sha2::Sha256;
 use sha3::Keccak256;
 
@@ -193,13 +195,12 @@ impl Host {
 
     // SHA256 functions
 
-    pub(crate) fn sha256_hash_from_bytes(&self, bytes: &[u8]) -> Result<Vec<u8>, HostError> {
-        sha256_hash_from_bytes(bytes, self.budget_ref())
-    }
-
-    pub fn sha256_hash_from_bytesobj_input(&self, x: BytesObject) -> Result<Vec<u8>, HostError> {
+    pub(crate) fn sha256_hash_from_bytesobj_input(
+        &self,
+        x: BytesObject,
+    ) -> Result<Vec<u8>, HostError> {
         self.visit_obj(x, |bytes: &ScBytes| {
-            let hash = self.sha256_hash_from_bytes(bytes.as_slice())?;
+            let hash = sha256_hash_from_bytes(bytes.as_slice(), self)?;
             if hash.len() != 32 {
                 return Err(err!(
                     self,
@@ -244,11 +245,26 @@ impl Host {
     }
 }
 
-pub(crate) fn sha256_hash_from_bytes(bytes: &[u8], budget: &Budget) -> Result<Vec<u8>, HostError> {
+pub(crate) fn sha256_hash_from_bytes(
+    bytes: &[u8],
+    budget: impl AsBudget,
+) -> Result<Vec<u8>, HostError> {
     let _span = tracy_span!("sha256");
-    budget.charge(
+    budget.as_budget().charge(
         ContractCostType::ComputeSha256Hash,
         Some(bytes.len() as u64),
     )?;
     Ok(<Sha256 as sha2::Digest>::digest(bytes).as_slice().to_vec())
 }
+
+pub(crate) fn chacha20_fill_bytes(
+    rng: &mut ChaCha20Rng,
+    dest: &mut [u8],
+    budget: impl AsBudget,
+) -> Result<(), HostError> {
+    budget
+        .as_budget()
+        .charge(ContractCostType::ChaCha20DrawBytes, Some(dest.len() as u64))?;
+    rng.fill_bytes(dest);
+    Ok(())
+}
diff --git a/soroban-env-host/src/host/prng.rs b/soroban-env-host/src/host/prng.rs
index 51fd9a9a..876b827e 100644
--- a/soroban-env-host/src/host/prng.rs
+++ b/soroban-env-host/src/host/prng.rs
@@ -1,4 +1,7 @@
-use super::declared_size::DeclaredSizeForMetering;
+use super::{
+    crypto::chacha20_fill_bytes, declared_size::DeclaredSizeForMetering,
+    metered_clone::MeteredContainer,
+};
 use crate::{
     budget::Budget,
     host::metered_clone::MeteredClone,
@@ -7,10 +10,7 @@ use crate::{
     HostError,
 };
 use rand::{distributions::Uniform, prelude::Distribution, seq::SliceRandom};
-use rand_chacha::{
-    rand_core::{RngCore, SeedableRng},
-    ChaCha20Rng,
-};
+use rand_chacha::{rand_core::SeedableRng, ChaCha20Rng};
 use std::ops::RangeInclusive;
 
 /// PRNG subsystem in the host, which provides best-effort pseudo-randomness to
@@ -86,8 +86,7 @@ static_assertions::const_assert_eq!(SEED_BYTES, 32);
 
 impl Prng {
     fn charge_prng_bytes(&self, budget: &Budget, count: u64) -> Result<(), HostError> {
-        // TODO: add a ContractCostType for drawing PRNG bytes
-        Ok(())
+        budget.charge(ContractCostType::ChaCha20DrawBytes, Some(count))
     }
 
     pub fn new_from_seed(seed: Seed) -> Self {
@@ -129,17 +128,15 @@ impl Prng {
     }
 
     pub(crate) fn bytes_new(&mut self, size: u32, budget: &Budget) -> Result<ScBytes, HostError> {
-        budget.charge(ContractCostType::HostMemAlloc, Some(size as u64))?;
-        self.charge_prng_bytes(budget, size as u64)?;
+        Vec::<u8>::charge_bulk_init_cpy(size as u64, budget)?;
         let mut vec = vec![0u8; size as usize];
-        self.0.fill_bytes(&mut vec);
+        chacha20_fill_bytes(&mut self.0, vec.as_mut_slice(), budget)?;
         Ok(ScBytes::try_from(vec)?)
     }
 
     pub(crate) fn sub_prng(&mut self, budget: &Budget) -> Result<Prng, HostError> {
         let mut new_seed: Seed = [0; SEED_BYTES as usize];
-        self.charge_prng_bytes(budget, SEED_BYTES)?;
-        self.0.fill_bytes(&mut new_seed);
+        chacha20_fill_bytes(&mut self.0, &mut new_seed, budget)?;
         budget.charge(ContractCostType::HostMemCpy, Some(SEED_BYTES))?;
         Ok(Self(ChaCha20Rng::from_seed(new_seed)))
     }


Test patch:
diff --git a/soroban-env-host/src/test/budget_metering.rs b/soroban-env-host/src/test/budget_metering.rs
index b4547341..f6c34065 100644
--- a/soroban-env-host/src/test/budget_metering.rs
+++ b/soroban-env-host/src/test/budget_metering.rs
@@ -349,6 +349,7 @@ fn total_amount_charged_from_random_inputs() -> Result<(), HostError> {
         (1, None),
         (1, None),
         (1, None),
+        (1, Some(1)),
     ];
 
     for ty in ContractCostType::variants() {
@@ -357,7 +358,7 @@ fn total_amount_charged_from_random_inputs() -> Result<(), HostError> {
     let actual = format!("{:?}", host.as_budget());
     expect![[r#"
         =====================================================================================================================================================================
-        Cpu limit: 100000000; used: 10115314
+        Cpu limit: 100000000; used: 10120240
         Mem limit: 41943040; used: 276044
         =====================================================================================================================================================================
         CostType                 iterations     input          cpu_insns      mem_bytes      const_term_cpu      lin_term_cpu        const_term_mem      lin_term_mem        
@@ -389,8 +390,9 @@ fn total_amount_charged_from_random_inputs() -> Result<(), HostError> {
         Int256Div                1              None           2333           119            2333                0                   119                 0                   
         Int256Pow                1              None           5212           119            5212                0                   119                 0                   
         Int256Shift              1              None           412            119            412                 0                   119                 0                   
+        ChaCha20DrawBytes        1              Some(1)        4926           0              4907                2461                0                   0                   
         =====================================================================================================================================================================
-        Total # times meter was called: 28
+        Total # times meter was called: 29
 
     "#]]
     .assert_eq(&actual);
diff --git a/soroban-env-host/src/test/util.rs b/soroban-env-host/src/test/util.rs
index af7f409b..8e5d5ea7 100644
--- a/soroban-env-host/src/test/util.rs
+++ b/soroban-env-host/src/test/util.rs
@@ -251,18 +251,38 @@ impl Host {
         let ht = HostTracker::start(None);
 
         let val = self.call(contract, func, args);
-        let cpu_consumed = budget
+        let cpu_metered = budget
             .get_cpu_insns_consumed()
             .expect("unable to retrieve cpu consumed");
-        let mem_consumed = budget
+        let mem_metered = budget
             .get_mem_bytes_consumed()
             .expect("unable to retrieve mem consumed");
 
-        let (cpu_insns, mem_bytes, _) = ht.stop();
+        let (cpu_actual, mem_actual, time_nsecs) = ht.stop();
+        let cpu_diff = (cpu_metered - cpu_actual) as i64;
+        let cpu_metered_diff_percent = 100 * cpu_diff / (cpu_metered as i64).max(1);
+        let mem_diff = (mem_metered - mem_actual) as i64;
+        let mem_metered_diff_percent = 100 * mem_diff / (mem_metered as i64).max(1);
+        let metered_insn_nsecs_ratio: f64 = (cpu_metered as f64) / (time_nsecs as f64).max(1.0);
+        let actual_insn_nsecs_ratio: f64 = (cpu_actual as f64) / (time_nsecs as f64).max(1.0);
+        println!();
         println!(
-                "\nmodel cpu consumed: {}, actual cpu insns {} \nmodel mem consumed: {}, actual mem bytes {}",
-                cpu_consumed, cpu_insns, mem_consumed, mem_bytes
-            );
+            "metered cpu insns: {}, actual cpu insns {}, diff: {} ({}%) \n",
+            cpu_metered, cpu_actual, cpu_diff, cpu_metered_diff_percent
+        );
+        println!(
+            "metered mem bytes: {}, actual mem bytes {}, diff: {} ({}%) \n",
+            mem_metered, mem_actual, mem_diff, mem_metered_diff_percent
+        );
+        println!(
+            "metered cpu_insn/time_nsecs ratio: {} \n",
+            metered_insn_nsecs_ratio
+        );
+        println!(
+            "actual cpu_insn/time_nsecs ratio: {} \n",
+            actual_insn_nsecs_ratio
+        );
+        println!();
 
         val
     }