soroban_env_host/budget/
model.rs

1use crate::{
2    xdr::{ContractCostParamEntry, ScErrorCode, ScErrorType},
3    HostError,
4};
5use core::fmt::{Debug, Display};
6
7/// We provide a "cost model" object that evaluates a linear expression:
8///
9///    f(x) = I * (a + b * Option<x>)
10///
11/// I is a simple scale factor that represents number of iterations the linear
12/// model inside the parentheses is repeated.
13///
14/// a, b are "fixed" parameters at construction time (extracted from an on-chain
15/// cost schedule, so technically not _totally_ fixed) and Option<x> is some
16/// abstract input variable -- say, event counts or object sizes -- provided at
17/// runtime. If the input cannot be defined, i.e., the cost is constant,
18/// input-independent, then pass in `None` as the input.
19///
20/// The same `CostModel` type, i.e. `CostType` (applied to different parameters
21/// and variables) is used for calculating memory as well as CPU time.
22///
23/// The various `CostType`s are carefully choosen such that 1. their underlying
24/// cost characteristics (both cpu and memory) at runtime can be described
25/// sufficiently by a linear model and 2. they together encompass the vast
26/// majority of available operations done by the `env` -- the host and the VM.
27///
28/// The parameters for a `CostModel` are calibrated empirically. See this
29/// crate's benchmarks for more details.
30pub trait HostCostModel {
31    fn evaluate(&self, iterations: u64, input: Option<u64>) -> Result<u64, HostError>;
32
33    #[cfg(any(test, feature = "testutils", feature = "bench"))]
34    fn reset(&mut self);
35}
36
37/// The number of bits to scale the linear term by. The linear coefficient has
38/// been scaled by this factor during parameter fitting to retain more significant
39/// digits. Thus to get the cost from the raw input, we need to scale the result
40/// back by the same factor.
41const COST_MODEL_LIN_TERM_SCALE_BITS: u32 = 7;
42
43/// A helper type that wraps an u64 to signify the wrapped value have been scaled.
44#[derive(Clone, Copy, Default, Debug)]
45pub struct ScaledU64(pub(crate) u64);
46
47impl ScaledU64 {
48    pub const fn from_unscaled_u64(u: u64) -> Self {
49        ScaledU64(u << COST_MODEL_LIN_TERM_SCALE_BITS)
50    }
51
52    pub const fn unscale(self) -> u64 {
53        self.0 >> COST_MODEL_LIN_TERM_SCALE_BITS
54    }
55
56    pub const fn is_zero(&self) -> bool {
57        self.0 == 0
58    }
59
60    pub const fn saturating_mul(&self, rhs: u64) -> Self {
61        ScaledU64(self.0.saturating_mul(rhs))
62    }
63
64    pub const fn safe_div(&self, rhs: u64) -> Self {
65        ScaledU64(match self.0.checked_div(rhs) {
66            Some(v) => v,
67            None => 0,
68        })
69    }
70}
71
72impl Display for ScaledU64 {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        write!(f, "{}", self.0)
75    }
76}
77
78#[cfg(feature = "bench")]
79impl From<f64> for ScaledU64 {
80    fn from(unscaled: f64) -> Self {
81        let scaled = unscaled * ((1 << COST_MODEL_LIN_TERM_SCALE_BITS) as f64);
82        // We err on the side of overestimation by applying `ceil` to the input.
83        ScaledU64(scaled.ceil() as u64)
84    }
85}
86
87#[derive(Clone, Copy, Debug, Default)]
88pub struct MeteredCostComponent {
89    pub const_term: u64,
90    pub lin_term: ScaledU64,
91}
92
93impl TryFrom<&ContractCostParamEntry> for MeteredCostComponent {
94    type Error = HostError;
95
96    fn try_from(entry: &ContractCostParamEntry) -> Result<Self, Self::Error> {
97        if entry.const_term < 0 || entry.linear_term < 0 {
98            return Err((ScErrorType::Context, ScErrorCode::InvalidInput).into());
99        }
100        Ok(MeteredCostComponent {
101            const_term: entry.const_term as u64,
102            lin_term: ScaledU64(entry.linear_term as u64),
103        })
104    }
105}
106
107impl TryFrom<ContractCostParamEntry> for MeteredCostComponent {
108    type Error = HostError;
109
110    fn try_from(entry: ContractCostParamEntry) -> Result<Self, Self::Error> {
111        Self::try_from(&entry)
112    }
113}
114
115impl HostCostModel for MeteredCostComponent {
116    fn evaluate(&self, iterations: u64, input: Option<u64>) -> Result<u64, HostError> {
117        let const_term = self.const_term.saturating_mul(iterations);
118        match input {
119            Some(input) => {
120                let mut res = const_term;
121                if !self.lin_term.is_zero() {
122                    let lin_cost = self
123                        .lin_term
124                        .saturating_mul(input)
125                        .saturating_mul(iterations);
126                    res = res.saturating_add(lin_cost.unscale())
127                }
128                Ok(res)
129            }
130            None => Ok(const_term),
131        }
132    }
133
134    #[cfg(any(test, feature = "testutils", feature = "bench"))]
135    fn reset(&mut self) {
136        self.const_term = 0;
137        self.lin_term = ScaledU64(0);
138    }
139}
140
141mod test {
142    #[allow(unused)]
143    use super::{HostCostModel, MeteredCostComponent, ScaledU64};
144
145    #[test]
146    fn test_model_evaluation_with_rounding() {
147        let test_model = MeteredCostComponent {
148            const_term: 3,
149            lin_term: ScaledU64(5),
150        };
151        // low iteration + low input
152        // the constant part is 3, the linear part is 5 >> 7 == 0, total is 3
153        assert_eq!(3, test_model.evaluate(1, Some(1)).unwrap());
154        // low iteration + high input
155        // the contant part is 3, the linear part is (26 * 5) >> 7 == 1, total is 4
156        assert_eq!(4, test_model.evaluate(1, Some(26)).unwrap());
157        // high iteration + low input
158        // the constant part is 26 * 3 == 78, the linear part is (26 * 5) >> 7 == 1, total is 79
159        assert_eq!(79, test_model.evaluate(26, Some(1)).unwrap());
160    }
161}