unc_vm_runner/
profile.rs

1pub use profile_v2::ProfileDataV2;
2
3use borsh::{BorshDeserialize, BorshSerialize};
4use enum_map::{enum_map, Enum, EnumMap};
5use std::fmt;
6use strum::IntoEnumIterator;
7use unc_parameters::{ActionCosts, ExtCosts, ExtCostsConfig};
8use unc_primitives_core::types::{Compute, Gas};
9
10mod profile_v2;
11
12/// Profile of gas consumption.
13#[derive(Clone, PartialEq, Eq)]
14pub struct ProfileDataV3 {
15    /// Gas spent on sending or executing actions.
16    actions_profile: EnumMap<ActionCosts, Gas>,
17    /// Non-action gas spent outside the WASM VM while executing a contract.
18    wasm_ext_profile: EnumMap<ExtCosts, Gas>,
19    /// Gas spent on execution inside the WASM VM.
20    wasm_gas: Gas,
21}
22
23impl Default for ProfileDataV3 {
24    fn default() -> ProfileDataV3 {
25        ProfileDataV3::new()
26    }
27}
28
29impl ProfileDataV3 {
30    #[inline]
31    pub fn new() -> Self {
32        Self {
33            actions_profile: enum_map! { _ => 0 },
34            wasm_ext_profile: enum_map! { _ => 0 },
35            wasm_gas: 0,
36        }
37    }
38
39    /// Test instance with unique numbers in each field.
40    pub fn test() -> Self {
41        let mut profile_data = ProfileDataV3::default();
42        for (i, cost) in ExtCosts::iter().enumerate() {
43            profile_data.add_ext_cost(cost, i as Gas);
44        }
45        for (i, cost) in ActionCosts::iter().enumerate() {
46            profile_data.add_action_cost(cost, i as Gas + 1000);
47        }
48        profile_data
49    }
50
51    #[inline]
52    pub fn merge(&mut self, other: &ProfileDataV3) {
53        for ((_, gas), (_, other_gas)) in
54            self.actions_profile.iter_mut().zip(other.actions_profile.iter())
55        {
56            *gas = gas.saturating_add(*other_gas);
57        }
58        for ((_, gas), (_, other_gas)) in
59            self.wasm_ext_profile.iter_mut().zip(other.wasm_ext_profile.iter())
60        {
61            *gas = gas.saturating_add(*other_gas);
62        }
63        self.wasm_gas = self.wasm_gas.saturating_add(other.wasm_gas);
64    }
65
66    #[inline]
67    pub fn add_action_cost(&mut self, action: ActionCosts, value: Gas) {
68        self.actions_profile[action] = self.actions_profile[action].saturating_add(value);
69    }
70
71    #[inline]
72    pub fn add_ext_cost(&mut self, ext: ExtCosts, value: Gas) {
73        self.wasm_ext_profile[ext] = self.wasm_ext_profile[ext].saturating_add(value);
74    }
75
76    /// WasmInstruction is the only cost we don't explicitly account for.
77    /// Instead, we compute it at the end of contract call as the difference
78    /// between total gas burnt and what we've explicitly accounted for in the
79    /// profile.
80    ///
81    /// This is because WasmInstruction is the hottest cost and is implemented
82    /// with the help on the VM side, so we don't want to have profiling logic
83    /// there both for simplicity and efficiency reasons.
84    pub fn compute_wasm_instruction_cost(&mut self, total_gas_burnt: Gas) {
85        self.wasm_gas =
86            total_gas_burnt.saturating_sub(self.action_gas()).saturating_sub(self.host_gas());
87    }
88
89    pub fn get_action_cost(&self, action: ActionCosts) -> Gas {
90        self.actions_profile[action]
91    }
92
93    pub fn get_ext_cost(&self, ext: ExtCosts) -> Gas {
94        self.wasm_ext_profile[ext]
95    }
96
97    pub fn get_wasm_cost(&self) -> Gas {
98        self.wasm_gas
99    }
100
101    fn host_gas(&self) -> Gas {
102        self.wasm_ext_profile.as_slice().iter().copied().fold(0, Gas::saturating_add)
103    }
104
105    pub fn action_gas(&self) -> Gas {
106        self.actions_profile.as_slice().iter().copied().fold(0, Gas::saturating_add)
107    }
108
109    /// Returns total compute usage of host calls.
110    pub fn total_compute_usage(&self, ext_costs_config: &ExtCostsConfig) -> Compute {
111        let ext_compute_cost = self
112            .wasm_ext_profile
113            .iter()
114            .map(|(key, value)| {
115                // Technically, gas cost might be zero while the compute cost is non-zero. To
116                // handle this case, we would need to explicitly count number of calls, not just
117                // the total gas usage.
118                // We don't have such costs at the moment, so this case is not implemented.
119                debug_assert!(key.gas(ext_costs_config) > 0 || key.compute(ext_costs_config) == 0);
120
121                if *value == 0 {
122                    return *value;
123                }
124                // If the `value` is non-zero, the gas cost also must be non-zero.
125                debug_assert!(key.gas(ext_costs_config) != 0);
126                ((*value as u128).saturating_mul(key.compute(ext_costs_config) as u128)
127                    / (key.gas(ext_costs_config) as u128)) as u64
128            })
129            .fold(0, Compute::saturating_add);
130
131        // We currently only support compute costs for host calls. In the future we might add
132        // them for actions as well.
133        ext_compute_cost.saturating_add(self.action_gas()).saturating_add(self.get_wasm_cost())
134    }
135}
136
137impl BorshDeserialize for ProfileDataV3 {
138    fn deserialize_reader<R: std::io::Read>(rd: &mut R) -> std::io::Result<Self> {
139        let actions_array: Vec<u64> = BorshDeserialize::deserialize_reader(rd)?;
140        let ext_array: Vec<u64> = BorshDeserialize::deserialize_reader(rd)?;
141        let wasm_gas: u64 = BorshDeserialize::deserialize_reader(rd)?;
142
143        // Mapping raw arrays to enum maps.
144        // The enum map could be smaller or larger than the raw array.
145        // Extra values in the array that are unknown to the current binary will
146        // be ignored. Missing values are filled with 0.
147        let actions_profile = enum_map! {
148            cost => actions_array.get(borsh_action_index(cost)).copied().unwrap_or(0)
149        };
150        let wasm_ext_profile = enum_map! {
151            cost => ext_array.get(borsh_ext_index(cost)).copied().unwrap_or(0)
152        };
153
154        Ok(Self { actions_profile, wasm_ext_profile, wasm_gas })
155    }
156}
157
158impl BorshSerialize for ProfileDataV3 {
159    fn serialize<W: std::io::Write>(&self, writer: &mut W) -> Result<(), std::io::Error> {
160        let mut actions_costs: Vec<u64> = vec![0u64; ActionCosts::LENGTH];
161        for (cost, gas) in self.actions_profile.iter() {
162            actions_costs[borsh_action_index(cost)] = *gas;
163        }
164        BorshSerialize::serialize(&actions_costs, writer)?;
165
166        let mut ext_costs: Vec<u64> = vec![0u64; ExtCosts::LENGTH];
167        for (cost, gas) in self.wasm_ext_profile.iter() {
168            ext_costs[borsh_ext_index(cost)] = *gas;
169        }
170        BorshSerialize::serialize(&ext_costs, writer)?;
171
172        let wasm_cost: u64 = self.wasm_gas;
173        BorshSerialize::serialize(&wasm_cost, writer)
174    }
175}
176
177/// Fixed index of an action cost for borsh (de)serialization.
178///
179/// We use borsh to store profiles on the DB and borsh is quite fragile with
180/// changes. This mapping is to decouple the Rust enum from the borsh
181/// representation. The enum can be changed freely but here in the mapping we
182/// can only append more values at the end.
183///
184/// TODO: Consider changing this to a different format (e.g. protobuf) because
185/// canonical representation is not required here.
186const fn borsh_action_index(action: ActionCosts) -> usize {
187    // actual indices are defined on the enum variants
188    action as usize
189}
190
191/// Fixed index of an ext cost for borsh (de)serialization.
192///
193/// We use borsh to store profiles on the DB and borsh is quite fragile with
194/// changes. This mapping is to decouple the Rust enum from the borsh
195/// representation. The enum can be changed freely but here in the mapping we
196/// can only append more values at the end.
197///
198/// TODO: Consider changing this to a different format (e.g. protobuf) because
199/// canonical representation is not required here.
200const fn borsh_ext_index(ext: ExtCosts) -> usize {
201    // actual indices are defined on the enum variants
202    ext as usize
203}
204
205impl fmt::Debug for ProfileDataV3 {
206    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207        use num_rational::Ratio;
208        let host_gas = self.host_gas();
209        let action_gas = self.action_gas();
210
211        writeln!(f, "------------------------------")?;
212        writeln!(f, "Action gas: {}", action_gas)?;
213        writeln!(f, "------ Host functions --------")?;
214        for cost in ExtCosts::iter() {
215            let d = self.get_ext_cost(cost);
216            if d != 0 {
217                writeln!(
218                    f,
219                    "{} -> {} [{}% host]",
220                    cost,
221                    d,
222                    Ratio::new(d * 100, core::cmp::max(host_gas, 1)).to_integer(),
223                )?;
224            }
225        }
226        writeln!(f, "------ Actions --------")?;
227        for cost in ActionCosts::iter() {
228            let d = self.get_action_cost(cost);
229            if d != 0 {
230                writeln!(f, "{} -> {}", cost, d)?;
231            }
232        }
233        writeln!(f, "------------------------------")?;
234        Ok(())
235    }
236}
237
238/// Tests for ProfileDataV3
239#[cfg(test)]
240mod test {
241    use super::*;
242
243    #[test]
244    #[cfg(not(feature = "nightly"))]
245    fn test_profile_data_debug() {
246        let profile_data = ProfileDataV3::test();
247        // we don't care about exact formatting, but the numbers should not change unexpectedly
248        let pretty_debug_str = format!("{profile_data:#?}");
249        expect_test::expect![[r#"
250            ------------------------------
251            Action gas: 18153
252            ------ Host functions --------
253            contract_loading_base -> 1 [0% host]
254            contract_loading_bytes -> 2 [0% host]
255            read_memory_base -> 3 [0% host]
256            read_memory_byte -> 4 [0% host]
257            write_memory_base -> 5 [0% host]
258            write_memory_byte -> 6 [0% host]
259            read_register_base -> 7 [0% host]
260            read_register_byte -> 8 [0% host]
261            write_register_base -> 9 [0% host]
262            write_register_byte -> 10 [0% host]
263            utf8_decoding_base -> 11 [0% host]
264            utf8_decoding_byte -> 12 [0% host]
265            utf16_decoding_base -> 13 [0% host]
266            utf16_decoding_byte -> 14 [0% host]
267            sha256_base -> 15 [0% host]
268            sha256_byte -> 16 [0% host]
269            keccak256_base -> 17 [0% host]
270            keccak256_byte -> 18 [0% host]
271            keccak512_base -> 19 [0% host]
272            keccak512_byte -> 20 [1% host]
273            ripemd160_base -> 21 [1% host]
274            ripemd160_block -> 22 [1% host]
275            ecrecover_base -> 23 [1% host]
276            log_base -> 24 [1% host]
277            log_byte -> 25 [1% host]
278            storage_write_base -> 26 [1% host]
279            storage_write_key_byte -> 27 [1% host]
280            storage_write_value_byte -> 28 [1% host]
281            storage_write_evicted_byte -> 29 [1% host]
282            storage_read_base -> 30 [1% host]
283            storage_read_key_byte -> 31 [1% host]
284            storage_read_value_byte -> 32 [1% host]
285            storage_remove_base -> 33 [1% host]
286            storage_remove_key_byte -> 34 [1% host]
287            storage_remove_ret_value_byte -> 35 [1% host]
288            storage_has_key_base -> 36 [1% host]
289            storage_has_key_byte -> 37 [1% host]
290            storage_iter_create_prefix_base -> 38 [1% host]
291            storage_iter_create_prefix_byte -> 39 [1% host]
292            storage_iter_create_range_base -> 40 [2% host]
293            storage_iter_create_from_byte -> 41 [2% host]
294            storage_iter_create_to_byte -> 42 [2% host]
295            storage_iter_next_base -> 43 [2% host]
296            storage_iter_next_key_byte -> 44 [2% host]
297            storage_iter_next_value_byte -> 45 [2% host]
298            touching_trie_node -> 46 [2% host]
299            read_cached_trie_node -> 47 [2% host]
300            promise_and_base -> 48 [2% host]
301            promise_and_per_promise -> 49 [2% host]
302            promise_return -> 50 [2% host]
303            validator_pledge_base -> 51 [2% host]
304            validator_total_pledge_base -> 52 [2% host]
305            alt_bn128_g1_multiexp_base -> 53 [2% host]
306            alt_bn128_g1_multiexp_element -> 54 [2% host]
307            alt_bn128_pairing_check_base -> 55 [2% host]
308            alt_bn128_pairing_check_element -> 56 [2% host]
309            alt_bn128_g1_sum_base -> 57 [2% host]
310            alt_bn128_g1_sum_element -> 58 [2% host]
311            ed25519_verify_base -> 59 [3% host]
312            ed25519_verify_byte -> 60 [3% host]
313            validator_power_base -> 61 [3% host]
314            validator_total_power_base -> 62 [3% host]
315            ------ Actions --------
316            create_account -> 1000
317            delete_account -> 1001
318            deploy_contract_base -> 1002
319            deploy_contract_byte -> 1003
320            function_call_base -> 1004
321            function_call_byte -> 1005
322            transfer -> 1006
323            pledge -> 1007
324            add_full_access_key -> 1008
325            add_function_call_key_base -> 1009
326            add_function_call_key_byte -> 1010
327            delete_key -> 1011
328            new_action_receipt -> 1012
329            new_data_receipt_base -> 1013
330            new_data_receipt_byte -> 1014
331            delegate -> 1015
332            register_rsa2048_keys -> 1016
333            create_rsa2048_challenge -> 1017
334            ------------------------------
335        "#]]
336        .assert_eq(&pretty_debug_str)
337    }
338
339    #[test]
340    fn test_profile_data_debug_no_data() {
341        let profile_data = ProfileDataV3::default();
342        // we don't care about exact formatting, but at least it should not panic
343        println!("{:#?}", &profile_data);
344    }
345
346    #[test]
347    fn test_no_panic_on_overflow() {
348        let mut profile_data = ProfileDataV3::default();
349        profile_data.add_action_cost(ActionCosts::add_full_access_key, u64::MAX);
350        profile_data.add_action_cost(ActionCosts::add_full_access_key, u64::MAX);
351
352        let res = profile_data.get_action_cost(ActionCosts::add_full_access_key);
353        assert_eq!(res, u64::MAX);
354    }
355
356    #[test]
357    fn test_merge() {
358        let mut profile_data = ProfileDataV3::default();
359        profile_data.add_action_cost(ActionCosts::add_full_access_key, 111);
360        profile_data.add_ext_cost(ExtCosts::storage_read_base, 11);
361
362        let mut profile_data2 = ProfileDataV3::default();
363        profile_data2.add_action_cost(ActionCosts::add_full_access_key, 222);
364        profile_data2.add_ext_cost(ExtCosts::storage_read_base, 22);
365
366        profile_data.merge(&profile_data2);
367        assert_eq!(profile_data.get_action_cost(ActionCosts::add_full_access_key), 333);
368        assert_eq!(profile_data.get_ext_cost(ExtCosts::storage_read_base), 33);
369    }
370
371    #[test]
372    fn test_total_compute_usage() {
373        let ext_costs_config = ExtCostsConfig::test_with_undercharging_factor(3);
374        let mut profile_data = ProfileDataV3::default();
375        profile_data.add_ext_cost(
376            ExtCosts::storage_read_base,
377            2 * ExtCosts::storage_read_base.gas(&ext_costs_config),
378        );
379        profile_data.add_ext_cost(
380            ExtCosts::storage_write_base,
381            5 * ExtCosts::storage_write_base.gas(&ext_costs_config),
382        );
383        profile_data.add_action_cost(ActionCosts::function_call_base, 100);
384
385        assert_eq!(
386            profile_data.total_compute_usage(&ext_costs_config),
387            3 * profile_data.host_gas() + profile_data.action_gas()
388        );
389    }
390
391    #[test]
392    fn test_borsh_ser_deser() {
393        let mut profile_data = ProfileDataV3::default();
394        for (i, cost) in ExtCosts::iter().enumerate() {
395            profile_data.add_ext_cost(cost, i as Gas);
396        }
397        for (i, cost) in ActionCosts::iter().enumerate() {
398            profile_data.add_action_cost(cost, i as Gas + 1000);
399        }
400        let buf = borsh::to_vec(&profile_data).expect("failed serializing a normal profile");
401
402        let restored: ProfileDataV3 = BorshDeserialize::deserialize(&mut buf.as_slice())
403            .expect("failed deserializing a normal profile");
404
405        assert_eq!(profile_data, restored);
406    }
407
408    #[test]
409    fn test_borsh_incomplete_profile() {
410        let action_profile = vec![50u64, 60];
411        let ext_profile = vec![100u64, 200];
412        let wasm_cost = 99u64;
413        let input = manually_encode_profile_v2(action_profile, ext_profile, wasm_cost);
414
415        let profile: ProfileDataV3 = BorshDeserialize::deserialize(&mut input.as_slice())
416            .expect("should be able to parse a profile with less entries");
417
418        assert_eq!(50, profile.get_action_cost(ActionCosts::create_account));
419        assert_eq!(60, profile.get_action_cost(ActionCosts::delete_account));
420        assert_eq!(0, profile.get_action_cost(ActionCosts::deploy_contract_base));
421
422        assert_eq!(100, profile.get_ext_cost(ExtCosts::base));
423        assert_eq!(200, profile.get_ext_cost(ExtCosts::contract_loading_base));
424        assert_eq!(0, profile.get_ext_cost(ExtCosts::contract_loading_bytes));
425    }
426
427    #[test]
428    fn test_borsh_larger_profile_than_current() {
429        let action_profile = vec![1234u64; ActionCosts::LENGTH + 5];
430        let ext_profile = vec![5678u64; ExtCosts::LENGTH + 10];
431        let wasm_cost = 90u64;
432        let input = manually_encode_profile_v2(action_profile, ext_profile, wasm_cost);
433
434        let profile: ProfileDataV3 = BorshDeserialize::deserialize(&mut input.as_slice()).expect(
435            "should be able to parse a profile with more entries than the current version has",
436        );
437
438        for action in ActionCosts::iter() {
439            assert_eq!(1234, profile.get_action_cost(action), "{action:?}");
440        }
441
442        for ext in ExtCosts::iter() {
443            assert_eq!(5678, profile.get_ext_cost(ext), "{ext:?}");
444        }
445
446        assert_eq!(90, profile.wasm_gas);
447    }
448
449    #[track_caller]
450    fn manually_encode_profile_v2(
451        action_profile: Vec<u64>,
452        ext_profile: Vec<u64>,
453        wasm_cost: u64,
454    ) -> Vec<u8> {
455        let mut input = vec![];
456        BorshSerialize::serialize(&action_profile, &mut input).unwrap();
457        BorshSerialize::serialize(&ext_profile, &mut input).unwrap();
458        BorshSerialize::serialize(&wasm_cost, &mut input).unwrap();
459        input
460    }
461}