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#[derive(Clone, PartialEq, Eq)]
14pub struct ProfileDataV3 {
15 actions_profile: EnumMap<ActionCosts, Gas>,
17 wasm_ext_profile: EnumMap<ExtCosts, Gas>,
19 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 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 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 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 debug_assert!(key.gas(ext_costs_config) > 0 || key.compute(ext_costs_config) == 0);
120
121 if *value == 0 {
122 return *value;
123 }
124 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 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 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
177const fn borsh_action_index(action: ActionCosts) -> usize {
187 action as usize
189}
190
191const fn borsh_ext_index(ext: ExtCosts) -> usize {
201 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#[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 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 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}