soroban_env_host/budget/
util.rs

1#[cfg(any(
2    test,
3    feature = "testutils",
4    feature = "bench",
5    feature = "recording_mode"
6))]
7use crate::{budget::Budget, HostError};
8
9#[cfg(any(test, feature = "testutils", feature = "bench"))]
10use crate::host::error::TryBorrowOrErr;
11
12#[cfg(any(test, feature = "testutils"))]
13use crate::{budget::model::ScaledU64, xdr::ContractCostType};
14
15#[cfg(any(test, feature = "testutils", feature = "bench"))]
16impl Budget {
17    pub fn reset_models(&self) -> Result<(), HostError> {
18        self.with_mut_budget(|mut b| {
19            b.cpu_insns.reset_models();
20            b.mem_bytes.reset_models();
21            Ok(())
22        })
23    }
24
25    pub(crate) fn track_wasm_mem_alloc(&self, delta: u64) -> Result<(), HostError> {
26        let mut bgt = self.0.try_borrow_mut_or_err()?;
27        bgt.tracker.wasm_memory = bgt.tracker.wasm_memory.saturating_add(delta);
28        Ok(())
29    }
30
31    pub fn get_wasm_mem_alloc(&self) -> Result<u64, HostError> {
32        Ok(self.0.try_borrow_or_err()?.tracker.wasm_memory)
33    }
34
35    pub fn reset_unlimited(&self) -> Result<(), HostError> {
36        self.reset_unlimited_cpu()?;
37        self.reset_unlimited_mem()?;
38        Ok(())
39    }
40
41    pub fn reset_unlimited_cpu(&self) -> Result<(), HostError> {
42        self.with_mut_budget(|mut b| {
43            b.cpu_insns.reset(u64::MAX);
44            Ok(())
45        })?; // panic means multiple-mut-borrow bug
46        self.reset_tracker()
47    }
48
49    pub fn reset_unlimited_mem(&self) -> Result<(), HostError> {
50        self.with_mut_budget(|mut b| {
51            b.mem_bytes.reset(u64::MAX);
52            Ok(())
53        })?;
54        self.reset_tracker()
55    }
56
57    pub fn cpu_limit_exceeded(&self) -> Result<bool, HostError> {
58        let cpu = &self.0.try_borrow_or_err()?.cpu_insns;
59        Ok(cpu.total_count > cpu.limit)
60    }
61
62    pub fn mem_limit_exceeded(&self) -> Result<bool, HostError> {
63        let mem = &self.0.try_borrow_or_err()?.mem_bytes;
64        Ok(mem.total_count > mem.limit)
65    }
66
67    pub fn reset_tracker(&self) -> Result<(), HostError> {
68        self.0.try_borrow_mut_or_err()?.tracker.reset();
69        Ok(())
70    }
71
72    pub fn reset(&self) -> Result<(), HostError> {
73        self.with_mut_budget(|mut b| {
74            b.cpu_insns.reset_count();
75            b.mem_bytes.reset_count();
76            Ok(())
77        })?;
78        self.reset_tracker()
79    }
80
81    pub fn reset_cpu_limit(&self, cpu: u64) -> Result<(), HostError> {
82        self.with_mut_budget(|mut b| {
83            b.cpu_insns.reset(cpu);
84            Ok(())
85        })?;
86        self.reset_tracker()
87    }
88
89    pub fn reset_limits(&self, cpu: u64, mem: u64) -> Result<(), HostError> {
90        self.with_mut_budget(|mut b| {
91            b.cpu_insns.reset(cpu);
92            b.mem_bytes.reset(mem);
93            Ok(())
94        })?;
95        self.reset_tracker()
96    }
97
98    /// Resets the `FuelConfig` we pass into Wasmi before running calibration.
99    /// Wasmi instruction calibration requires running the same Wasmi insn
100    /// a fixed number of times, record their actual cpu and mem consumption, then
101    /// divide those numbers by the number of iterations, which is the fuel count.
102    /// Fuel count is kept tracked on the Wasmi side, based on the `FuelConfig`
103    /// of a specific fuel category. In order to get the correct, unscaled fuel
104    /// count, we have to preset all the `FuelConfig` entries to 1.
105    pub fn reset_fuel_config(&self) -> Result<(), HostError> {
106        self.0.try_borrow_mut_or_err()?.fuel_costs = wasmi::FuelCosts::default();
107        Ok(())
108    }
109
110    pub fn get_shadow_cpu_insns_consumed(&self) -> Result<u64, HostError> {
111        Ok(self.0.try_borrow_or_err()?.cpu_insns.shadow_total_count)
112    }
113
114    pub fn get_shadow_mem_bytes_consumed(&self) -> Result<u64, HostError> {
115        Ok(self.0.try_borrow_or_err()?.mem_bytes.shadow_total_count)
116    }
117
118    #[allow(unused)]
119    pub fn shadow_cpu_limit_exceeded(&self) -> Result<bool, HostError> {
120        let cpu = &self.0.try_borrow_or_err()?.cpu_insns;
121        Ok(cpu.shadow_total_count > cpu.shadow_limit)
122    }
123
124    pub fn shadow_mem_limit_exceeded(&self) -> Result<bool, HostError> {
125        let mem = &self.0.try_borrow_or_err()?.mem_bytes;
126        Ok(mem.shadow_total_count > mem.shadow_limit)
127    }
128}
129
130#[cfg(any(test, feature = "testutils"))]
131impl Budget {
132    pub(crate) fn override_model_with_scaled_params(
133        &self,
134        ty: ContractCostType,
135        const_cpu: u64,
136        lin_cpu: ScaledU64,
137        const_mem: u64,
138        lin_mem: ScaledU64,
139    ) -> Result<(), HostError> {
140        let mut bgt = self.0.try_borrow_mut_or_err()?;
141        let cpu_model = bgt.cpu_insns.get_cost_model_mut(ty)?;
142        cpu_model.const_term = const_cpu;
143        cpu_model.lin_term = lin_cpu;
144
145        let mem_model = bgt.mem_bytes.get_cost_model_mut(ty)?;
146        mem_model.const_term = const_mem;
147        mem_model.lin_term = lin_mem;
148        Ok(())
149    }
150
151    pub(crate) fn override_model_with_unscaled_params(
152        &self,
153        ty: ContractCostType,
154        const_cpu: u64,
155        lin_cpu: u64,
156        const_mem: u64,
157        lin_mem: u64,
158    ) -> Result<(), HostError> {
159        self.override_model_with_scaled_params(
160            ty,
161            const_cpu,
162            ScaledU64::from_unscaled_u64(lin_cpu),
163            const_mem,
164            ScaledU64::from_unscaled_u64(lin_mem),
165        )
166    }
167}
168
169#[cfg(any(test, feature = "recording_mode", feature = "testutils"))]
170impl Budget {
171    /// Variant of `with_shadow_mode`, enabled only in testing and
172    /// non-production scenarios, that produces a `Result<>` rather than eating
173    /// errors and return values the way `with_shadow_mode` does.
174    ///
175    /// This is undesirable specifically because it has a return value which may
176    /// vary between `Ok` and `Err` depending on whether the shadow budget is
177    /// exhausted, and the shadow budget varies based on debug status --
178    /// something that is not identical from one host to the next.
179    ///
180    /// However, in testing and non-production workflows, sometimes we need the
181    /// convenience of temporarily "turning off" the budget. This can happen for
182    /// several reasons: we want some test logic to not affect the
183    /// production budget, or we want to maintain an accurate prediction of
184    /// production budget during preflight. In the latter case, we want to
185    /// exclude preflight-only logic from the budget. By routing metering to the
186    /// shadow budget instead of turning the budget off completely, it offers
187    /// some DOS-mitigation.
188    ///
189    /// If in doubt, do not use this function.
190    pub(crate) fn with_observable_shadow_mode<T, F>(&self, f: F) -> Result<T, HostError>
191    where
192        F: FnOnce() -> Result<T, HostError>,
193    {
194        let mut prev = false;
195        let should_execute = self.with_mut_budget(|mut b| {
196            prev = b.is_in_shadow_mode;
197            b.is_in_shadow_mode = true;
198            b.cpu_insns
199                .check_budget_limit(super::dimension::IsShadowMode(true))?;
200            b.mem_bytes
201                .check_budget_limit(super::dimension::IsShadowMode(true))
202        });
203
204        let rt = match should_execute {
205            Ok(_) => f(),
206            Err(e) => Err(e),
207        };
208
209        self.with_mut_budget(|mut b| {
210            b.is_in_shadow_mode = prev;
211            Ok(())
212        })?;
213
214        rt
215    }
216}