1pub use checked::*;
6
7#[sui_macros::with_checked_arithmetic]
8mod checked {
9 use crate::error::{UserInputError, UserInputResult};
10 use crate::gas::{self, GasCostSummary, GasUsageReport, SuiGasStatusAPI};
11 use crate::gas_model::gas_predicates::{cost_table_for_version, txn_base_cost_as_multiplier};
12 use crate::gas_model::units_types::CostTable;
13 use crate::transaction::ObjectReadResult;
14 use crate::{
15 ObjectID,
16 error::{ExecutionError, ExecutionErrorKind},
17 gas_model::tables::{GasStatus, ZERO_COST_SCHEDULE},
18 };
19 use move_core_types::vm_status::StatusCode;
20 use serde::{Deserialize, Serialize};
21 use sui_protocol_config::*;
22
23 #[allow(dead_code)]
27 pub(crate) struct ComputationBucket {
28 min: u64,
29 max: u64,
30 cost: u64,
31 }
32
33 impl ComputationBucket {
34 fn new(min: u64, max: u64, cost: u64) -> Self {
35 ComputationBucket { min, max, cost }
36 }
37
38 fn simple(min: u64, max: u64) -> Self {
39 Self::new(min, max, max)
40 }
41 }
42
43 fn get_bucket_cost(table: &[ComputationBucket], computation_cost: u64) -> u64 {
44 for bucket in table {
45 if bucket.max >= computation_cost {
46 return bucket.cost;
47 }
48 }
49 match table.last() {
50 None => 5_000_000,
52 Some(bucket) => bucket.cost,
53 }
54 }
55
56 fn computation_bucket(max_bucket_cost: u64) -> Vec<ComputationBucket> {
59 assert!(max_bucket_cost >= 5_000_000);
60 vec![
61 ComputationBucket::simple(0, 1_000),
62 ComputationBucket::simple(1_000, 5_000),
63 ComputationBucket::simple(5_000, 10_000),
64 ComputationBucket::simple(10_000, 20_000),
65 ComputationBucket::simple(20_000, 50_000),
66 ComputationBucket::simple(50_000, 200_000),
67 ComputationBucket::simple(200_000, 1_000_000),
68 ComputationBucket::simple(1_000_000, max_bucket_cost),
69 ]
70 }
71
72 fn sender_rebate(storage_rebate: u64, storage_rebate_rate: u64) -> u64 {
75 const BASIS_POINTS: u128 = 10000;
78 (((storage_rebate as u128 * storage_rebate_rate as u128)
79 + (BASIS_POINTS / 2)) / BASIS_POINTS) as u64
81 }
82
83 pub struct SuiCostTable {
85 pub(crate) min_transaction_cost: u64,
88 pub(crate) max_gas_budget: u64,
90 package_publish_per_byte_cost: u64,
94 object_read_per_byte_cost: u64,
97 storage_per_byte_cost: u64,
101 pub execution_cost_table: CostTable,
103 computation_bucket: Vec<ComputationBucket>,
105 max_gas_price_rgp_factor_for_aborted_transactions: Option<u64>,
107 }
108
109 impl std::fmt::Debug for SuiCostTable {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 write!(f, "SuiCostTable(...)")
113 }
114 }
115
116 impl SuiCostTable {
117 pub(crate) fn new(c: &ProtocolConfig, gas_price: u64) -> Self {
118 let min_transaction_cost = if txn_base_cost_as_multiplier(c) {
121 c.base_tx_cost_fixed() * gas_price
122 } else {
123 c.base_tx_cost_fixed()
124 };
125 Self {
126 min_transaction_cost,
127 max_gas_budget: c.max_tx_gas(),
128 package_publish_per_byte_cost: c.package_publish_cost_per_byte(),
129 object_read_per_byte_cost: c.obj_access_cost_read_per_byte(),
130 storage_per_byte_cost: c.obj_data_cost_refundable(),
131 execution_cost_table: cost_table_for_version(c.gas_model_version()),
132 computation_bucket: computation_bucket(c.max_gas_computation_bucket()),
133 max_gas_price_rgp_factor_for_aborted_transactions: c
134 .max_gas_price_rgp_factor_for_aborted_transactions_as_option(),
135 }
136 }
137
138 pub(crate) fn unmetered() -> Self {
139 Self {
140 min_transaction_cost: 0,
141 max_gas_budget: u64::MAX,
142 package_publish_per_byte_cost: 0,
143 object_read_per_byte_cost: 0,
144 storage_per_byte_cost: 0,
145 execution_cost_table: ZERO_COST_SCHEDULE.clone(),
146 computation_bucket: computation_bucket(5_000_000),
148 max_gas_price_rgp_factor_for_aborted_transactions: None,
149 }
150 }
151 }
152
153 #[derive(Debug, Clone, Serialize, Deserialize)]
154 pub struct PerObjectStorage {
155 pub storage_cost: u64,
161 pub storage_rebate: u64,
165 pub new_size: u64,
167 }
168
169 #[derive(Debug, Clone, Copy)]
170 enum GasRoundingMode {
171 Bucketize,
173 Stepped(u64),
175 KeepHalfDigits,
177 }
178
179 #[allow(dead_code)]
180 #[derive(Debug)]
181 pub struct SuiGasStatus {
182 pub gas_status: GasStatus,
184 cost_table: SuiCostTable,
186 gas_budget: u64,
189 computation_cost: u64,
193 charge: bool,
195 gas_price: u64,
201 reference_gas_price: u64,
203 storage_gas_price: u64,
211 per_object_storage: Vec<(ObjectID, PerObjectStorage)>,
214 rebate_rate: u64,
216 unmetered_storage_rebate: u64,
219 gas_rounding_mode: GasRoundingMode,
221 }
222
223 impl SuiGasStatus {
224 fn new(
225 move_gas_status: GasStatus,
226 gas_budget: u64,
227 charge: bool,
228 gas_price: u64,
229 reference_gas_price: u64,
230 storage_gas_price: u64,
231 rebate_rate: u64,
232 gas_rounding_mode: GasRoundingMode,
233 cost_table: SuiCostTable,
234 ) -> SuiGasStatus {
235 let gas_rounding_mode = match gas_rounding_mode {
236 GasRoundingMode::Bucketize => GasRoundingMode::Bucketize,
237 GasRoundingMode::Stepped(val) => GasRoundingMode::Stepped(val.max(1)),
238 GasRoundingMode::KeepHalfDigits => GasRoundingMode::KeepHalfDigits,
239 };
240 SuiGasStatus {
241 gas_status: move_gas_status,
242 gas_budget,
243 charge,
244 computation_cost: 0,
245 gas_price,
246 reference_gas_price,
247 storage_gas_price,
248 per_object_storage: Vec::new(),
249 rebate_rate,
250 unmetered_storage_rebate: 0,
251 gas_rounding_mode,
252 cost_table,
253 }
254 }
255
256 pub(crate) fn new_with_budget(
257 gas_budget: u64,
258 gas_price: u64,
259 reference_gas_price: u64,
260 config: &ProtocolConfig,
261 ) -> SuiGasStatus {
262 let storage_gas_price = config.storage_gas_price();
263 let max_computation_budget = config.max_gas_computation_bucket() * gas_price;
264 let computation_budget = if gas_budget > max_computation_budget {
265 max_computation_budget
266 } else {
267 gas_budget
268 };
269 let sui_cost_table = SuiCostTable::new(config, gas_price);
270 let gas_rounding_mode = if config.gas_rounding_halve_digits() {
271 GasRoundingMode::KeepHalfDigits
272 } else if let Some(step) = config.gas_rounding_step_as_option() {
273 GasRoundingMode::Stepped(step)
274 } else {
275 GasRoundingMode::Bucketize
276 };
277 Self::new(
278 GasStatus::new(
279 sui_cost_table.execution_cost_table.clone(),
280 computation_budget,
281 gas_price,
282 config.gas_model_version(),
283 ),
284 gas_budget,
285 true,
286 gas_price,
287 reference_gas_price,
288 storage_gas_price,
289 config.storage_rebate_rate(),
290 gas_rounding_mode,
291 sui_cost_table,
292 )
293 }
294
295 pub fn new_unmetered() -> SuiGasStatus {
296 Self::new(
297 GasStatus::new_unmetered(),
298 0,
299 false,
300 0,
301 0,
302 0,
303 0,
304 GasRoundingMode::Bucketize,
305 SuiCostTable::unmetered(),
306 )
307 }
308
309 pub fn reference_gas_price(&self) -> u64 {
310 self.reference_gas_price
311 }
312
313 pub(crate) fn check_gas_balance(
318 &self,
319 gas_objs: &[&ObjectReadResult],
320 gas_budget: u64,
321 ) -> UserInputResult {
322 self.check_gas_objects(gas_objs)?;
323 self.check_gas_data(gas_objs, gas_budget)
324 }
325
326 pub(crate) fn check_gas_objects(&self, gas_objs: &[&ObjectReadResult]) -> UserInputResult {
328 for gas_object in gas_objs {
330 if let Some(obj) = gas_object.as_object() {
333 if !obj.is_address_owned() {
334 return Err(UserInputError::GasObjectNotOwnedObject {
335 owner: obj.owner.clone(),
336 });
337 }
338 } else {
339 return Err(UserInputError::MissingGasPayment);
342 }
343 }
344 Ok(())
345 }
346
347 pub(crate) fn check_gas_data(
349 &self,
350 gas_objs: &[&ObjectReadResult],
351 gas_budget: u64,
352 ) -> UserInputResult {
353 if gas_budget > self.cost_table.max_gas_budget {
355 return Err(UserInputError::GasBudgetTooHigh {
356 gas_budget,
357 max_budget: self.cost_table.max_gas_budget,
358 });
359 }
360 if gas_budget < self.cost_table.min_transaction_cost {
361 return Err(UserInputError::GasBudgetTooLow {
362 gas_budget,
363 min_budget: self.cost_table.min_transaction_cost,
364 });
365 }
366
367 let mut gas_balance = 0u128;
369 for gas_obj in gas_objs {
370 gas_balance += gas::get_gas_balance(gas_obj.as_object().ok_or(
371 UserInputError::InvalidGasObject {
372 object_id: gas_obj.id(),
373 },
374 )?)? as u128;
375 }
376 if gas_balance < gas_budget as u128 {
377 Err(UserInputError::GasBalanceTooLow {
378 gas_balance,
379 needed_gas_amount: gas_budget as u128,
380 })
381 } else {
382 Ok(())
383 }
384 }
385
386 fn storage_cost(&self) -> u64 {
387 self.storage_gas_units()
388 }
389
390 pub fn per_object_storage(&self) -> &Vec<(ObjectID, PerObjectStorage)> {
391 &self.per_object_storage
392 }
393 }
394
395 impl SuiGasStatusAPI for SuiGasStatus {
396 fn is_unmetered(&self) -> bool {
397 !self.charge
398 }
399
400 fn move_gas_status(&self) -> &GasStatus {
401 &self.gas_status
402 }
403
404 fn move_gas_status_mut(&mut self) -> &mut GasStatus {
405 &mut self.gas_status
406 }
407
408 fn bucketize_computation(&mut self, aborted: Option<bool>) -> Result<(), ExecutionError> {
409 let gas_used = self.gas_status.gas_used_pre_gas_price();
410 let effective_gas_price = if self
411 .cost_table
412 .max_gas_price_rgp_factor_for_aborted_transactions
413 .is_some()
414 && aborted.unwrap_or(false)
415 {
416 let max_gas_price_for_aborted_txns = self
419 .cost_table
420 .max_gas_price_rgp_factor_for_aborted_transactions
421 .unwrap()
422 * self.reference_gas_price;
423 self.gas_price.min(max_gas_price_for_aborted_txns)
424 } else {
425 self.gas_price
427 };
428 let gas_used = match self.gas_rounding_mode {
429 GasRoundingMode::KeepHalfDigits => {
430 half_digits_rounding(gas_used) * effective_gas_price
431 }
432 GasRoundingMode::Stepped(gas_rounding) => {
433 if gas_used > 0 && gas_used % gas_rounding == 0 {
434 gas_used * effective_gas_price
435 } else {
436 ((gas_used / gas_rounding) + 1) * gas_rounding * effective_gas_price
437 }
438 }
439 GasRoundingMode::Bucketize => {
440 let bucket_cost =
441 get_bucket_cost(&self.cost_table.computation_bucket, gas_used);
442 bucket_cost * effective_gas_price
445 }
446 };
447 if self.gas_budget <= gas_used {
448 self.computation_cost = self.gas_budget;
449 Err(ExecutionErrorKind::InsufficientGas.into())
450 } else {
451 self.computation_cost = gas_used;
452 Ok(())
453 }
454 }
455
456 fn summary(&self) -> GasCostSummary {
460 let storage_rebate = self.storage_rebate();
462 let sender_rebate = sender_rebate(storage_rebate, self.rebate_rate);
463 assert!(sender_rebate <= storage_rebate);
464 let non_refundable_storage_fee = storage_rebate - sender_rebate;
465 GasCostSummary {
466 computation_cost: self.computation_cost,
467 storage_cost: self.storage_cost(),
468 storage_rebate: sender_rebate,
469 non_refundable_storage_fee,
470 }
471 }
472
473 fn gas_budget(&self) -> u64 {
474 self.gas_budget
475 }
476
477 fn gas_price(&self) -> u64 {
478 self.gas_price
479 }
480
481 fn reference_gas_price(&self) -> u64 {
482 self.reference_gas_price
483 }
484
485 fn storage_gas_units(&self) -> u64 {
486 self.per_object_storage
487 .iter()
488 .map(|(_, per_object)| per_object.storage_cost)
489 .sum()
490 }
491
492 fn storage_rebate(&self) -> u64 {
493 self.per_object_storage
494 .iter()
495 .map(|(_, per_object)| per_object.storage_rebate)
496 .sum()
497 }
498
499 fn unmetered_storage_rebate(&self) -> u64 {
500 self.unmetered_storage_rebate
501 }
502
503 fn gas_used(&self) -> u64 {
504 self.gas_status.gas_used_pre_gas_price()
505 }
506
507 fn reset_storage_cost_and_rebate(&mut self) {
508 self.per_object_storage = Vec::new();
509 self.unmetered_storage_rebate = 0;
510 }
511
512 fn charge_storage_read(&mut self, size: usize) -> Result<(), ExecutionError> {
513 self.gas_status
514 .charge_bytes(size, self.cost_table.object_read_per_byte_cost)
515 .map_err(|e| {
516 debug_assert_eq!(e.major_status(), StatusCode::OUT_OF_GAS);
517 ExecutionErrorKind::InsufficientGas.into()
518 })
519 }
520
521 fn charge_publish_package(&mut self, size: usize) -> Result<(), ExecutionError> {
522 self.gas_status
523 .charge_bytes(size, self.cost_table.package_publish_per_byte_cost)
524 .map_err(|e| {
525 debug_assert_eq!(e.major_status(), StatusCode::OUT_OF_GAS);
526 ExecutionErrorKind::InsufficientGas.into()
527 })
528 }
529
530 fn track_storage_mutation(
535 &mut self,
536 object_id: ObjectID,
537 new_size: usize,
538 storage_rebate: u64,
539 ) -> u64 {
540 if self.is_unmetered() {
541 self.unmetered_storage_rebate += storage_rebate;
542 return 0;
543 }
544
545 let new_size = new_size as u64;
547 let storage_cost =
548 new_size * self.cost_table.storage_per_byte_cost * self.storage_gas_price;
549 self.per_object_storage.push((
552 object_id,
553 PerObjectStorage {
554 storage_cost,
555 storage_rebate,
556 new_size,
557 },
558 ));
559 storage_cost
561 }
562
563 fn charge_storage_and_rebate(&mut self) -> Result<(), ExecutionError> {
564 let storage_rebate = self.storage_rebate();
565 let storage_cost = self.storage_cost();
566 let sender_rebate = sender_rebate(storage_rebate, self.rebate_rate);
567 assert!(sender_rebate <= storage_rebate);
568 if sender_rebate >= storage_cost {
569 Ok(())
572 } else {
573 let gas_left = self.gas_budget - self.computation_cost;
574 if gas_left < storage_cost - sender_rebate {
576 Err(ExecutionErrorKind::InsufficientGas.into())
580 } else {
581 Ok(())
582 }
583 }
584 }
585
586 fn adjust_computation_on_out_of_gas(&mut self) {
587 self.per_object_storage = Vec::new();
588 self.computation_cost = self.gas_budget;
589 }
590
591 fn gas_usage_report(&self) -> GasUsageReport {
592 GasUsageReport {
593 cost_summary: self.summary(),
594 gas_used: self.gas_used(),
595 gas_price: self.gas_price(),
596 reference_gas_price: self.reference_gas_price(),
597 per_object_storage: self.per_object_storage().clone(),
598 gas_budget: self.gas_budget(),
599 storage_gas_price: self.storage_gas_price,
600 rebate_rate: self.rebate_rate,
601 }
602 }
603 }
604
605 fn half_digits_rounding(n: u64) -> u64 {
606 if n < 1000 {
607 return 1000;
608 }
609 let digits = n.ilog10();
610 let drop = digits / 2;
611 let base = 10u64.pow(drop);
612 n.div_ceil(base) * base
613 }
614
615 #[test]
616 fn test_half_digits_rounding() {
617 assert_eq!(half_digits_rounding(0), 1000);
618 assert_eq!(half_digits_rounding(1), 1000);
619 assert_eq!(half_digits_rounding(999), 1000);
620 assert_eq!(half_digits_rounding(1000), 1000);
621 assert_eq!(half_digits_rounding(1001), 1010);
622 assert_eq!(half_digits_rounding(1050), 1050);
623 assert_eq!(half_digits_rounding(1999), 2000);
624 assert_eq!(half_digits_rounding(20_000), 20_000);
625 assert_eq!(half_digits_rounding(20_001), 20_100);
626 assert_eq!(half_digits_rounding(20_500), 20_500);
627 assert_eq!(half_digits_rounding(29_999), 30_000);
628 assert_eq!(half_digits_rounding(300_000), 300_000);
629 assert_eq!(half_digits_rounding(300_001), 300_100);
630 assert_eq!(half_digits_rounding(305_500), 305_500);
631 assert_eq!(half_digits_rounding(305_501), 305_600);
632 assert_eq!(half_digits_rounding(999_999), 1_000_000);
633 assert_eq!(half_digits_rounding(1_000_000), 1_000_000);
634 assert_eq!(half_digits_rounding(1_000_001), 1_001_000);
635 assert_eq!(half_digits_rounding(1_005_000), 1_005_000);
636 assert_eq!(half_digits_rounding(1_005_001), 1_006_000);
637 assert_eq!(half_digits_rounding(1_999_999), 2_000_000);
638 assert_eq!(half_digits_rounding(10_000_001), 10_001_000);
639 assert_eq!(half_digits_rounding(100_000_001), 100_010_000);
640 }
641}