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 execution_result: &mut Result<T, E>,
350 ) -> GasCostSummary {
351 debug_assert!(self.gas_status.storage_rebate() == 0);
354 debug_assert!(self.gas_status.storage_gas_units() == 0);
355
356 if !matches!(&self.payment, PaymentMetadata::Unmetered) {
357 let is_move_abort = execution_result
359 .as_ref()
360 .err()
361 .map(|err| {
362 matches!(
363 err.kind(),
364 sui_types::execution_status::ExecutionErrorKind::MoveAbort(_, _)
365 )
366 })
367 .unwrap_or(false);
368 if let Err(err) = self.gas_status.bucketize_computation(Some(is_move_abort))
370 && execution_result.is_ok()
371 {
372 *execution_result = Err(err.into());
373 }
374
375 if execution_result.is_err() {
377 self.reset(temporary_store);
378 }
379 }
380
381 temporary_store.ensure_active_inputs_mutated();
383 temporary_store.collect_storage_and_rebate(self);
384
385 if matches!(&self.payment, PaymentMetadata::Unmetered) {
386 return GasCostSummary::default();
387 }
388 let gas_payment_location = self.gas_payment_location();
389 if let Some(PaymentLocation::Coin(_)) = gas_payment_location {
390 #[skip_checked_arithmetic]
391 trace!(target: "replay_gas_info", "Gas smashing has occurred for this transaction");
392 }
393
394 if execution_result
395 .as_ref()
396 .err()
397 .map(|err| {
398 matches!(
399 err.kind(),
400 sui_types::execution_status::ExecutionErrorKind::InsufficientFundsForWithdraw
401 )
402 })
403 .unwrap_or(false)
404 && matches!(gas_payment_location, Some(PaymentLocation::AddressBalance(_))) {
405 return GasCostSummary::default();
408 }
409
410 self.compute_storage_and_rebate(temporary_store, execution_result);
411
412 let gas_payment_location = if refresh_gas_payment_location(self.gas_model_version) {
413 self.gas_payment_location()
414 } else {
415 gas_payment_location
416 };
417
418 let cost_summary = self.gas_status.summary();
419
420 let Some(gas_payment_location) = gas_payment_location else {
421 assert!(
423 matches!(self.payment, PaymentMetadata::Gasless),
424 "Only gasless transactions should reach this point without a payment location"
425 );
426 if execution_result.is_err() {
427 return GasCostSummary::default();
428 }
429 let storage_cost = cost_summary.storage_cost;
432 assert!(
433 storage_cost == 0,
434 "Gasless transaction must not incur storage cost, got {storage_cost}"
435 );
436 let sender_rebate = cost_summary.storage_rebate;
437 return GasCostSummary {
438 computation_cost: sender_rebate,
439 storage_cost: 0,
440 storage_rebate: sender_rebate,
441 non_refundable_storage_fee: cost_summary.non_refundable_storage_fee,
442 };
443 };
444
445 let net_change = cost_summary.net_gas_usage();
446
447 match gas_payment_location {
448 PaymentLocation::AddressBalance(payer_address) => {
449 if net_change != 0 {
451 let balance_type = sui_types::balance::Balance::type_tag(
452 sui_types::gas_coin::GAS::type_tag(),
453 );
454 let event = AccumulatorEvent::from_balance_change(
455 payer_address,
456 balance_type,
457 net_change.checked_neg().unwrap(),
458 )
459 .expect("Failed to create accumulator event for gas charging");
460 temporary_store.add_accumulator_event(event);
461 }
462 }
463 PaymentLocation::Coin(gas_object_id) => {
464 let mut gas_object =
465 temporary_store.read_object(&gas_object_id).unwrap().clone();
466 deduct_gas(&mut gas_object, net_change);
467 #[skip_checked_arithmetic]
468 trace!(net_change, gas_obj_id =? gas_object.id(), gas_obj_ver =? gas_object.version(), "Updated gas object");
469 temporary_store.mutate_new_or_input_object(gas_object);
470 }
471 }
472 cost_summary
473 }
474
475 fn compute_storage_and_rebate<T, E: ExecutionErrorTrait>(
487 &mut self,
488 temporary_store: &mut TemporaryStore<'_>,
489 execution_result: &mut Result<T, E>,
490 ) {
491 if dont_charge_budget_on_storage_oog(self.gas_model_version) {
492 self.handle_storage_and_rebate_v2(temporary_store, execution_result)
493 } else {
494 self.handle_storage_and_rebate_v1(temporary_store, execution_result)
495 }
496 }
497
498 fn handle_storage_and_rebate_v1<T, E: ExecutionErrorTrait>(
499 &mut self,
500 temporary_store: &mut TemporaryStore<'_>,
501 execution_result: &mut Result<T, E>,
502 ) {
503 if let Err(err) = self.gas_status.charge_storage_and_rebate() {
504 self.reset(temporary_store);
505 self.gas_status.adjust_computation_on_out_of_gas();
506 temporary_store.ensure_active_inputs_mutated();
507 temporary_store.collect_rebate(self);
508 if execution_result.is_ok() {
509 *execution_result = Err(err.into());
510 }
511 }
512 }
513
514 fn handle_storage_and_rebate_v2<T, E: ExecutionErrorTrait>(
515 &mut self,
516 temporary_store: &mut TemporaryStore<'_>,
517 execution_result: &mut Result<T, E>,
518 ) {
519 if let Err(err) = self.gas_status.charge_storage_and_rebate() {
520 self.reset(temporary_store);
524 temporary_store.ensure_active_inputs_mutated();
525 temporary_store.collect_storage_and_rebate(self);
526 if let Err(err) = self.gas_status.charge_storage_and_rebate() {
527 self.reset(temporary_store);
530 self.gas_status.adjust_computation_on_out_of_gas();
531 temporary_store.ensure_active_inputs_mutated();
532 temporary_store.collect_rebate(self);
533 if execution_result.is_ok() {
534 *execution_result = Err(err.into());
535 }
536 } else if execution_result.is_ok() {
537 *execution_result = Err(err.into());
538 }
539 }
540 }
541 }
542
543 impl SmashMetadata {
544 fn payment_methods(&self) -> impl Iterator<Item = &'_ PaymentMethod> {
546 std::iter::once(&self.smash_target).chain(self.smashed_payments.values())
547 }
548
549 fn smash_gas(
550 &mut self,
551 tx_digest: &TransactionDigest,
552 temporary_store: &mut TemporaryStore<'_>,
553 ) {
554 self.gas_charge_location = self.smash_target.location();
556
557 let total_smashed = self
559 .payment_methods()
560 .map(|payment| match payment {
561 PaymentMethod::AddressBalance(_, reservation) => Ok(*reservation),
562 PaymentMethod::Coin(obj_ref) => {
563 let obj_data = temporary_store
564 .objects()
565 .get(&obj_ref.0)
566 .map(|obj| &obj.data);
567 let Some(Data::Move(move_obj)) = obj_data else {
568 return Err(ExecutionError::invariant_violation(
569 "Provided non-gas coin object as input for gas!",
570 ));
571 };
572 if !move_obj.type_().is_gas_coin() {
573 return Err(ExecutionError::invariant_violation(
574 "Provided non-gas coin object as input for gas!",
575 ));
576 }
577 Ok(move_obj.get_coin_value_unsafe())
578 }
579 })
580 .collect::<Result<Vec<u64>, ExecutionError>>()
581 .unwrap_or_else(|_| {
584 panic!(
585 "Unable to process gas payments for transaction {}",
586 tx_digest
587 )
588 })
589 .iter()
590 .sum();
591 debug_assert!(
595 self.total_smashed == 0 || self.total_smashed == total_smashed,
596 "Gas smashing should not change after a reset"
597 );
598 self.total_smashed = total_smashed;
599
600 let smash_location = self.smash_target.location();
601 for payment_method in self.smashed_payments.values() {
603 let location = payment_method.location();
604 assert_ne!(location, smash_location, "Payment methods must be unique");
605 match payment_method {
606 PaymentMethod::AddressBalance(sui_address, reservation) => {
607 let balance_type = sui_types::balance::Balance::type_tag(
608 sui_types::gas_coin::GAS::type_tag(),
609 );
610 let event = AccumulatorEvent::from_balance_change(
611 *sui_address,
612 balance_type,
613 i64::try_from(*reservation).unwrap().checked_neg().unwrap(),
614 )
615 .expect("Failed to create accumulator event for gas smashing");
616 temporary_store.add_accumulator_event(event);
617 }
618 PaymentMethod::Coin((id, _, _)) => {
619 temporary_store.delete_input_object(id);
620 }
621 }
622 }
623 match &self.smash_target {
624 PaymentMethod::AddressBalance(sui_address, reservation) => {
625 let deposit = total_smashed - *reservation;
629 if deposit != 0 {
630 let balance_type = sui_types::balance::Balance::type_tag(
631 sui_types::gas_coin::GAS::type_tag(),
632 );
633 let event = AccumulatorEvent::from_balance_change(
634 *sui_address,
635 balance_type,
636 i64::try_from(deposit).unwrap(),
637 )
638 .expect("Failed to create accumulator event for gas smashing");
639 temporary_store.add_accumulator_event(event);
640 }
641 }
642 PaymentMethod::Coin((gas_coin_id, _, _)) => {
643 let mut primary_gas_object = temporary_store
644 .objects()
645 .get(gas_coin_id)
646 .unwrap_or_else(|| {
648 panic!(
649 "Invariant violation: gas coin not found in store in txn {}",
650 tx_digest
651 )
652 })
653 .clone();
654 primary_gas_object
655 .data
656 .try_as_move_mut()
657 .unwrap_or_else(|| {
659 panic!(
660 "Invariant violation: invalid coin object in txn {}",
661 tx_digest
662 )
663 })
664 .set_coin_value_unsafe(total_smashed);
665 temporary_store.mutate_input_object(primary_gas_object);
666 }
667 }
668 }
669
670 fn used_coins(&self) -> impl Iterator<Item = &'_ ObjectRef> {
671 self.payment_methods().filter_map(|method| match method {
672 PaymentMethod::Coin(obj_ref) => Some(obj_ref),
673 PaymentMethod::AddressBalance(_, _) => None,
674 })
675 }
676 }
677
678 impl PaymentKind {
679 pub fn unmetered() -> Self {
680 Self(PaymentKind_::Unmetered)
681 }
682
683 pub fn gasless() -> Self {
684 Self(PaymentKind_::Gasless)
685 }
686
687 pub fn smash(payment_methods: Vec<PaymentMethod>) -> Option<Self> {
688 debug_assert!(
689 !payment_methods.is_empty(),
690 "GasCharger must have at least one payment method"
691 );
692 if payment_methods.is_empty() {
693 return None;
694 }
695 let mut unique_methods = IndexMap::new();
696 for payment_method in payment_methods {
697 match (
698 unique_methods.entry(payment_method.location()),
699 payment_method,
700 ) {
701 (indexmap::map::Entry::Vacant(entry), payment_method) => {
702 entry.insert(payment_method);
703 }
704 (
705 indexmap::map::Entry::Occupied(mut occupied),
706 PaymentMethod::AddressBalance(other, additional),
707 ) => {
708 let PaymentMethod::AddressBalance(addr, amount) = occupied.get_mut() else {
709 unreachable!("Payment method does not match location")
710 };
711 assert_eq!(*addr, other, "Payment method does not match location");
712 *amount += additional;
713 }
714 (indexmap::map::Entry::Occupied(_), _) => {
715 debug_assert!(
716 false,
717 "Duplicate coin payment method found, \
718 which should have been prevented by input checks"
719 );
720 return None;
721 }
722 }
723 }
724 Some(Self(PaymentKind_::Smash(unique_methods)))
725 }
726 }
727
728 impl PaymentMethod {
729 pub fn location(&self) -> PaymentLocation {
730 match self {
731 PaymentMethod::Coin(obj_ref) => PaymentLocation::Coin(obj_ref.0),
732 PaymentMethod::AddressBalance(addr, _) => PaymentLocation::AddressBalance(*addr),
733 }
734 }
735 }
736}