1pub use checked::*;
6
7#[sui_macros::with_checked_arithmetic]
8pub mod checked {
9
10 use crate::sui_types::gas::SuiGasStatusAPI;
11 use crate::temporary_store::TemporaryStore;
12 use either::Either;
13 use indexmap::IndexMap;
14 use sui_protocol_config::ProtocolConfig;
15 use sui_types::deny_list_v2::CONFIG_SETTING_DYNAMIC_FIELD_SIZE_FOR_GAS;
16 use sui_types::digests::TransactionDigest;
17 use sui_types::error::ExecutionErrorTrait;
18 use sui_types::gas::{GasCostSummary, SuiGasStatus, deduct_gas};
19 use sui_types::gas_model::gas_predicates::{
20 charge_upgrades, dont_charge_budget_on_storage_oog, refresh_gas_payment_location,
21 };
22 use sui_types::{
23 accumulator_event::AccumulatorEvent,
24 base_types::{ObjectID, ObjectRef, SuiAddress},
25 error::ExecutionError,
26 gas_model::tables::GasStatus,
27 is_system_package,
28 object::Data,
29 };
30 use tracing::trace;
31
32 #[derive(Debug)]
37 pub struct GasCharger {
38 tx_digest: TransactionDigest,
39 gas_model_version: u64,
40 payment: PaymentMetadata,
41 gas_status: SuiGasStatus,
42 }
43
44 #[derive(Debug)]
50 enum PaymentMetadata {
51 Unmetered,
52 Gasless,
53 Smash(SmashMetadata),
55 }
56
57 #[derive(Debug)]
63 struct SmashMetadata {
64 gas_charge_location: PaymentLocation,
67 total_smashed: u64,
69 smash_target: PaymentMethod,
72 smashed_payments: IndexMap<PaymentLocation, PaymentMethod>,
75 }
76
77 #[derive(Debug)]
81 pub struct PaymentKind(PaymentKind_);
82
83 #[derive(Debug)]
86 enum PaymentKind_ {
87 Unmetered,
88 Gasless,
89 Smash(IndexMap<PaymentLocation, PaymentMethod>),
92 }
93
94 #[derive(Debug)]
97 pub enum PaymentMethod {
98 Coin(ObjectRef),
99 AddressBalance(SuiAddress, u64),
100 }
101
102 #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
105 pub enum PaymentLocation {
106 Coin(ObjectID),
107 AddressBalance(SuiAddress),
108 }
109
110 #[derive(Debug, Clone, Copy)]
115 pub struct GasPayment {
116 pub location: PaymentLocation,
119 pub amount: u64,
121 }
122
123 impl GasCharger {
124 pub fn new(
125 tx_digest: TransactionDigest,
126 payment_kind: PaymentKind,
127 gas_status: SuiGasStatus,
128 temporary_store: &mut TemporaryStore<'_>,
129 protocol_config: &ProtocolConfig,
130 ) -> Self {
131 let gas_model_version = protocol_config.gas_model_version();
132 let payment = match payment_kind.0 {
133 PaymentKind_::Unmetered => PaymentMetadata::Unmetered,
134 PaymentKind_::Gasless => PaymentMetadata::Gasless,
135 PaymentKind_::Smash(mut payment_methods) => {
136 let (_, smash_target) = payment_methods.shift_remove_index(0).unwrap();
137 let mut metadata = SmashMetadata {
138 total_smashed: 0,
140 gas_charge_location: smash_target.location(),
141 smash_target,
142 smashed_payments: payment_methods,
143 };
144 metadata.smash_gas(&tx_digest, temporary_store);
145 PaymentMetadata::Smash(metadata)
146 }
147 };
148 Self {
149 tx_digest,
150 gas_model_version,
151 payment,
152 gas_status,
153 }
154 }
155
156 pub fn new_unmetered(tx_digest: TransactionDigest) -> Self {
157 Self {
158 tx_digest,
159 gas_model_version: 6, payment: PaymentMetadata::Unmetered,
161 gas_status: SuiGasStatus::new_unmetered(),
162 }
163 }
164
165 pub(crate) fn used_coins(&self) -> impl Iterator<Item = &'_ ObjectRef> {
168 match &self.payment {
169 PaymentMetadata::Unmetered | PaymentMetadata::Gasless => {
170 Either::Left(std::iter::empty())
171 }
172 PaymentMetadata::Smash(metadata) => Either::Right(metadata.used_coins()),
173 }
174 }
175
176 pub fn override_gas_charge_location(
178 &mut self,
179 location: PaymentLocation,
180 ) -> Result<(), ExecutionError> {
181 if let PaymentMetadata::Smash(metadata) = &mut self.payment {
182 metadata.gas_charge_location = location;
183 Ok(())
184 } else {
185 invariant_violation!("Can only override gas charge location in the smash-gas case")
186 }
187 }
188
189 pub fn gas_payment_amount(&self) -> Option<GasPayment> {
196 match &self.payment {
197 PaymentMetadata::Unmetered | PaymentMetadata::Gasless => None,
198 PaymentMetadata::Smash(metadata) => Some(GasPayment {
199 location: metadata.smash_target.location(),
200 amount: metadata.total_smashed,
201 }),
202 }
203 }
204
205 pub(crate) fn gas_payment_location(&self) -> Option<PaymentLocation> {
206 match &self.payment {
207 PaymentMetadata::Unmetered | PaymentMetadata::Gasless => None,
208 PaymentMetadata::Smash(metadata) => Some(metadata.gas_charge_location),
209 }
210 }
211
212 pub fn gas_budget(&self) -> u64 {
213 self.gas_status.gas_budget()
214 }
215
216 pub fn unmetered_storage_rebate(&self) -> u64 {
217 self.gas_status.unmetered_storage_rebate()
218 }
219
220 pub fn no_charges(&self) -> bool {
221 self.gas_status.gas_used() == 0
222 && self.gas_status.storage_rebate() == 0
223 && self.gas_status.storage_gas_units() == 0
224 }
225
226 pub fn is_unmetered(&self) -> bool {
227 self.gas_status.is_unmetered()
228 }
229
230 pub fn move_gas_status(&self) -> &GasStatus {
231 self.gas_status.move_gas_status()
232 }
233
234 pub fn move_gas_status_mut(&mut self) -> &mut GasStatus {
235 self.gas_status.move_gas_status_mut()
236 }
237
238 pub fn into_gas_status(self) -> SuiGasStatus {
239 self.gas_status
240 }
241
242 pub fn summary(&self) -> GasCostSummary {
243 self.gas_status.summary()
244 }
245
246 fn smash_gas(&mut self, temporary_store: &mut TemporaryStore<'_>) {
254 match &mut self.payment {
255 PaymentMetadata::Unmetered | PaymentMetadata::Gasless => (),
256 PaymentMetadata::Smash(smash_metadata) => {
257 smash_metadata.smash_gas(&self.tx_digest, temporary_store);
258 }
259 }
260 }
261
262 pub fn track_storage_mutation(
267 &mut self,
268 object_id: ObjectID,
269 new_size: usize,
270 storage_rebate: u64,
271 ) -> u64 {
272 self.gas_status
273 .track_storage_mutation(object_id, new_size, storage_rebate)
274 }
275
276 pub fn reset_storage_cost_and_rebate(&mut self) {
277 self.gas_status.reset_storage_cost_and_rebate();
278 }
279
280 pub fn charge_publish_package(&mut self, size: usize) -> Result<(), ExecutionError> {
281 self.gas_status.charge_publish_package(size)
282 }
283
284 pub fn charge_upgrade_package(&mut self, size: usize) -> Result<(), ExecutionError> {
285 if charge_upgrades(self.gas_model_version) {
286 self.gas_status.charge_publish_package(size)
287 } else {
288 Ok(())
289 }
290 }
291
292 pub fn charge_input_objects(
293 &mut self,
294 temporary_store: &TemporaryStore<'_>,
295 ) -> Result<(), ExecutionError> {
296 let objects = temporary_store.objects();
297 let _object_count = objects.len();
299 let total_size = temporary_store
301 .objects()
302 .iter()
303 .filter(|(id, _)| !is_system_package(**id))
305 .map(|(_, obj)| obj.object_size_for_gas_metering())
306 .sum();
307 self.gas_status.charge_storage_read(total_size)
308 }
309
310 pub fn charge_coin_transfers(
311 &mut self,
312 protocol_config: &ProtocolConfig,
313 num_non_gas_coin_owners: u64,
314 ) -> Result<(), ExecutionError> {
315 let bytes_read_per_owner = CONFIG_SETTING_DYNAMIC_FIELD_SIZE_FOR_GAS;
319 let cost_per_byte =
322 protocol_config.dynamic_field_borrow_child_object_type_cost_per_byte() as usize;
323 let cost_per_owner = bytes_read_per_owner * cost_per_byte;
324 let owner_cost = cost_per_owner * (num_non_gas_coin_owners as usize);
325 self.gas_status.charge_storage_read(owner_cost)
326 }
327
328 pub fn reset(&mut self, temporary_store: &mut TemporaryStore<'_>) {
331 temporary_store.drop_writes();
332 self.gas_status.reset_storage_cost_and_rebate();
333 self.smash_gas(temporary_store);
334 }
335
336 pub fn charge_gas<T, E: ExecutionErrorTrait>(
347 &mut self,
348 temporary_store: &mut TemporaryStore<'_>,
349 protocol_config: &ProtocolConfig,
350 execution_result: &mut Result<T, E>,
351 ) -> GasCostSummary {
352 debug_assert!(self.gas_status.storage_rebate() == 0);
355 debug_assert!(self.gas_status.storage_gas_units() == 0);
356
357 if !matches!(&self.payment, PaymentMetadata::Unmetered) {
358 let is_move_abort = execution_result
360 .as_ref()
361 .err()
362 .map(|err| {
363 matches!(
364 err.kind(),
365 sui_types::execution_status::ExecutionErrorKind::MoveAbort(_, _)
366 )
367 })
368 .unwrap_or(false);
369 if let Err(err) = self.gas_status.bucketize_computation(Some(is_move_abort))
371 && execution_result.is_ok()
372 {
373 *execution_result = Err(err.into());
374 }
375
376 if execution_result.is_err() {
378 self.reset(temporary_store);
379 }
380 }
381
382 temporary_store.ensure_active_inputs_mutated();
384 temporary_store.collect_storage_and_rebate(self);
385
386 if matches!(&self.payment, PaymentMetadata::Unmetered) {
387 return GasCostSummary::default();
388 }
389 let gas_payment_location = self.gas_payment_location();
390 if let Some(PaymentLocation::Coin(_)) = gas_payment_location {
391 #[skip_checked_arithmetic]
392 trace!(target: "replay_gas_info", "Gas smashing has occurred for this transaction");
393 }
394
395 if execution_result
396 .as_ref()
397 .err()
398 .map(|err| {
399 matches!(
400 err.kind(),
401 sui_types::execution_status::ExecutionErrorKind::InsufficientFundsForWithdraw
402 )
403 })
404 .unwrap_or(false)
405 && matches!(gas_payment_location, Some(PaymentLocation::AddressBalance(_))) {
406 debug_assert!(!protocol_config.early_exit_on_iffw(), "Should have not reached charge gas in this case with IFFW");
407 return GasCostSummary::default();
410 }
411
412 self.compute_storage_and_rebate(temporary_store, execution_result);
413
414 let gas_payment_location = if refresh_gas_payment_location(self.gas_model_version) {
415 self.gas_payment_location()
416 } else {
417 gas_payment_location
418 };
419
420 let cost_summary = self.gas_status.summary();
421
422 let Some(gas_payment_location) = gas_payment_location else {
423 assert!(
425 matches!(self.payment, PaymentMetadata::Gasless),
426 "Only gasless transactions should reach this point without a payment location"
427 );
428 if execution_result.is_err() {
429 return GasCostSummary::default();
430 }
431 let storage_cost = cost_summary.storage_cost;
434 assert!(
435 storage_cost == 0,
436 "Gasless transaction must not incur storage cost, got {storage_cost}"
437 );
438 let sender_rebate = cost_summary.storage_rebate;
439 return GasCostSummary {
440 computation_cost: sender_rebate,
441 storage_cost: 0,
442 storage_rebate: sender_rebate,
443 non_refundable_storage_fee: cost_summary.non_refundable_storage_fee,
444 };
445 };
446
447 let net_change = cost_summary.net_gas_usage();
448
449 match gas_payment_location {
450 PaymentLocation::AddressBalance(payer_address) => {
451 if net_change != 0 {
453 let balance_type = sui_types::balance::Balance::type_tag(
454 sui_types::gas_coin::GAS::type_tag(),
455 );
456 let event = AccumulatorEvent::from_balance_change(
457 payer_address,
458 balance_type,
459 net_change.checked_neg().unwrap(),
460 )
461 .expect("Failed to create accumulator event for gas charging");
462 temporary_store.add_accumulator_event(event);
463 }
464 }
465 PaymentLocation::Coin(gas_object_id) => {
466 let mut gas_object =
467 temporary_store.read_object(&gas_object_id).unwrap().clone();
468 deduct_gas(&mut gas_object, net_change);
469 #[skip_checked_arithmetic]
470 trace!(net_change, gas_obj_id =? gas_object.id(), gas_obj_ver =? gas_object.version(), "Updated gas object");
471 temporary_store.mutate_new_or_input_object(gas_object);
472 }
473 }
474 cost_summary
475 }
476
477 fn compute_storage_and_rebate<T, E: ExecutionErrorTrait>(
489 &mut self,
490 temporary_store: &mut TemporaryStore<'_>,
491 execution_result: &mut Result<T, E>,
492 ) {
493 if dont_charge_budget_on_storage_oog(self.gas_model_version) {
494 self.handle_storage_and_rebate_v2(temporary_store, execution_result)
495 } else {
496 self.handle_storage_and_rebate_v1(temporary_store, execution_result)
497 }
498 }
499
500 fn handle_storage_and_rebate_v1<T, E: ExecutionErrorTrait>(
501 &mut self,
502 temporary_store: &mut TemporaryStore<'_>,
503 execution_result: &mut Result<T, E>,
504 ) {
505 if let Err(err) = self.gas_status.charge_storage_and_rebate() {
506 self.reset(temporary_store);
507 self.gas_status.adjust_computation_on_out_of_gas();
508 temporary_store.ensure_active_inputs_mutated();
509 temporary_store.collect_rebate(self);
510 if execution_result.is_ok() {
511 *execution_result = Err(err.into());
512 }
513 }
514 }
515
516 fn handle_storage_and_rebate_v2<T, E: ExecutionErrorTrait>(
517 &mut self,
518 temporary_store: &mut TemporaryStore<'_>,
519 execution_result: &mut Result<T, E>,
520 ) {
521 if let Err(err) = self.gas_status.charge_storage_and_rebate() {
522 self.reset(temporary_store);
526 temporary_store.ensure_active_inputs_mutated();
527 temporary_store.collect_storage_and_rebate(self);
528 if let Err(err) = self.gas_status.charge_storage_and_rebate() {
529 self.reset(temporary_store);
532 self.gas_status.adjust_computation_on_out_of_gas();
533 temporary_store.ensure_active_inputs_mutated();
534 temporary_store.collect_rebate(self);
535 if execution_result.is_ok() {
536 *execution_result = Err(err.into());
537 }
538 } else if execution_result.is_ok() {
539 *execution_result = Err(err.into());
540 }
541 }
542 }
543 }
544
545 impl SmashMetadata {
546 fn payment_methods(&self) -> impl Iterator<Item = &'_ PaymentMethod> {
548 std::iter::once(&self.smash_target).chain(self.smashed_payments.values())
549 }
550
551 fn smash_gas(
552 &mut self,
553 tx_digest: &TransactionDigest,
554 temporary_store: &mut TemporaryStore<'_>,
555 ) {
556 self.gas_charge_location = self.smash_target.location();
558
559 let total_smashed = self
561 .payment_methods()
562 .map(|payment| match payment {
563 PaymentMethod::AddressBalance(_, reservation) => Ok(*reservation),
564 PaymentMethod::Coin(obj_ref) => {
565 let obj_data = temporary_store
566 .objects()
567 .get(&obj_ref.0)
568 .map(|obj| &obj.data);
569 let Some(Data::Move(move_obj)) = obj_data else {
570 return Err(ExecutionError::invariant_violation(
571 "Provided non-gas coin object as input for gas!",
572 ));
573 };
574 if !move_obj.type_().is_gas_coin() {
575 return Err(ExecutionError::invariant_violation(
576 "Provided non-gas coin object as input for gas!",
577 ));
578 }
579 Ok(move_obj.get_coin_value_unsafe())
580 }
581 })
582 .collect::<Result<Vec<u64>, ExecutionError>>()
583 .unwrap_or_else(|_| {
586 panic!(
587 "Unable to process gas payments for transaction {}",
588 tx_digest
589 )
590 })
591 .iter()
592 .sum();
593 debug_assert!(
597 self.total_smashed == 0 || self.total_smashed == total_smashed,
598 "Gas smashing should not change after a reset"
599 );
600 self.total_smashed = total_smashed;
601
602 let smash_location = self.smash_target.location();
603 for payment_method in self.smashed_payments.values() {
605 let location = payment_method.location();
606 assert_ne!(location, smash_location, "Payment methods must be unique");
607 match payment_method {
608 PaymentMethod::AddressBalance(sui_address, reservation) => {
609 let balance_type = sui_types::balance::Balance::type_tag(
610 sui_types::gas_coin::GAS::type_tag(),
611 );
612 let event = AccumulatorEvent::from_balance_change(
613 *sui_address,
614 balance_type,
615 i64::try_from(*reservation).unwrap().checked_neg().unwrap(),
616 )
617 .expect("Failed to create accumulator event for gas smashing");
618 temporary_store.add_accumulator_event(event);
619 }
620 PaymentMethod::Coin((id, _, _)) => {
621 temporary_store.delete_input_object(id);
622 }
623 }
624 }
625 match &self.smash_target {
626 PaymentMethod::AddressBalance(sui_address, reservation) => {
627 let deposit = total_smashed - *reservation;
631 if deposit != 0 {
632 let balance_type = sui_types::balance::Balance::type_tag(
633 sui_types::gas_coin::GAS::type_tag(),
634 );
635 let event = AccumulatorEvent::from_balance_change(
636 *sui_address,
637 balance_type,
638 i64::try_from(deposit).unwrap(),
639 )
640 .expect("Failed to create accumulator event for gas smashing");
641 temporary_store.add_accumulator_event(event);
642 }
643 }
644 PaymentMethod::Coin((gas_coin_id, _, _)) => {
645 let mut primary_gas_object = temporary_store
646 .objects()
647 .get(gas_coin_id)
648 .unwrap_or_else(|| {
650 panic!(
651 "Invariant violation: gas coin not found in store in txn {}",
652 tx_digest
653 )
654 })
655 .clone();
656 primary_gas_object
657 .data
658 .try_as_move_mut()
659 .unwrap_or_else(|| {
661 panic!(
662 "Invariant violation: invalid coin object in txn {}",
663 tx_digest
664 )
665 })
666 .set_coin_value_unsafe(total_smashed);
667 temporary_store.mutate_input_object(primary_gas_object);
668 }
669 }
670 }
671
672 fn used_coins(&self) -> impl Iterator<Item = &'_ ObjectRef> {
673 self.payment_methods().filter_map(|method| match method {
674 PaymentMethod::Coin(obj_ref) => Some(obj_ref),
675 PaymentMethod::AddressBalance(_, _) => None,
676 })
677 }
678 }
679
680 impl PaymentKind {
681 pub fn unmetered() -> Self {
682 Self(PaymentKind_::Unmetered)
683 }
684
685 pub fn gasless() -> Self {
686 Self(PaymentKind_::Gasless)
687 }
688
689 pub fn smash(payment_methods: Vec<PaymentMethod>) -> Option<Self> {
690 debug_assert!(
691 !payment_methods.is_empty(),
692 "GasCharger must have at least one payment method"
693 );
694 if payment_methods.is_empty() {
695 return None;
696 }
697 let mut unique_methods = IndexMap::new();
698 for payment_method in payment_methods {
699 match (
700 unique_methods.entry(payment_method.location()),
701 payment_method,
702 ) {
703 (indexmap::map::Entry::Vacant(entry), payment_method) => {
704 entry.insert(payment_method);
705 }
706 (
707 indexmap::map::Entry::Occupied(mut occupied),
708 PaymentMethod::AddressBalance(other, additional),
709 ) => {
710 let PaymentMethod::AddressBalance(addr, amount) = occupied.get_mut() else {
711 unreachable!("Payment method does not match location")
712 };
713 assert_eq!(*addr, other, "Payment method does not match location");
714 *amount += additional;
715 }
716 (indexmap::map::Entry::Occupied(_), _) => {
717 debug_assert!(
718 false,
719 "Duplicate coin payment method found, \
720 which should have been prevented by input checks"
721 );
722 return None;
723 }
724 }
725 }
726 Some(Self(PaymentKind_::Smash(unique_methods)))
727 }
728 }
729
730 impl PaymentMethod {
731 pub fn location(&self) -> PaymentLocation {
732 match self {
733 PaymentMethod::Coin(obj_ref) => PaymentLocation::Coin(obj_ref.0),
734 PaymentMethod::AddressBalance(addr, _) => PaymentLocation::AddressBalance(*addr),
735 }
736 }
737 }
738}