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,
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 fn gas_budget(&self) -> u64 {
206 self.gas_status.gas_budget()
207 }
208
209 pub fn unmetered_storage_rebate(&self) -> u64 {
210 self.gas_status.unmetered_storage_rebate()
211 }
212
213 pub fn no_charges(&self) -> bool {
214 self.gas_status.gas_used() == 0
215 && self.gas_status.storage_rebate() == 0
216 && self.gas_status.storage_gas_units() == 0
217 }
218
219 pub fn is_unmetered(&self) -> bool {
220 self.gas_status.is_unmetered()
221 }
222
223 pub fn move_gas_status(&self) -> &GasStatus {
224 self.gas_status.move_gas_status()
225 }
226
227 pub fn move_gas_status_mut(&mut self) -> &mut GasStatus {
228 self.gas_status.move_gas_status_mut()
229 }
230
231 pub fn into_gas_status(self) -> SuiGasStatus {
232 self.gas_status
233 }
234
235 pub fn summary(&self) -> GasCostSummary {
236 self.gas_status.summary()
237 }
238
239 fn smash_gas(&mut self, temporary_store: &mut TemporaryStore<'_>) {
247 match &mut self.payment {
248 PaymentMetadata::Unmetered | PaymentMetadata::Gasless => (),
249 PaymentMetadata::Smash(smash_metadata) => {
250 smash_metadata.smash_gas(&self.tx_digest, temporary_store);
251 }
252 }
253 }
254
255 pub fn track_storage_mutation(
260 &mut self,
261 object_id: ObjectID,
262 new_size: usize,
263 storage_rebate: u64,
264 ) -> u64 {
265 self.gas_status
266 .track_storage_mutation(object_id, new_size, storage_rebate)
267 }
268
269 pub fn reset_storage_cost_and_rebate(&mut self) {
270 self.gas_status.reset_storage_cost_and_rebate();
271 }
272
273 pub fn charge_publish_package(&mut self, size: usize) -> Result<(), ExecutionError> {
274 self.gas_status.charge_publish_package(size)
275 }
276
277 pub fn charge_upgrade_package(&mut self, size: usize) -> Result<(), ExecutionError> {
278 if charge_upgrades(self.gas_model_version) {
279 self.gas_status.charge_publish_package(size)
280 } else {
281 Ok(())
282 }
283 }
284
285 pub fn charge_input_objects(
286 &mut self,
287 temporary_store: &TemporaryStore<'_>,
288 ) -> Result<(), ExecutionError> {
289 let objects = temporary_store.objects();
290 let _object_count = objects.len();
292 let total_size = temporary_store
294 .objects()
295 .iter()
296 .filter(|(id, _)| !is_system_package(**id))
298 .map(|(_, obj)| obj.object_size_for_gas_metering())
299 .sum();
300 self.gas_status.charge_storage_read(total_size)
301 }
302
303 pub fn charge_coin_transfers(
304 &mut self,
305 protocol_config: &ProtocolConfig,
306 num_non_gas_coin_owners: u64,
307 ) -> Result<(), ExecutionError> {
308 let bytes_read_per_owner = CONFIG_SETTING_DYNAMIC_FIELD_SIZE_FOR_GAS;
312 let cost_per_byte =
315 protocol_config.dynamic_field_borrow_child_object_type_cost_per_byte() as usize;
316 let cost_per_owner = bytes_read_per_owner * cost_per_byte;
317 let owner_cost = cost_per_owner * (num_non_gas_coin_owners as usize);
318 self.gas_status.charge_storage_read(owner_cost)
319 }
320
321 pub fn reset(&mut self, temporary_store: &mut TemporaryStore<'_>) {
324 temporary_store.drop_writes();
325 self.gas_status.reset_storage_cost_and_rebate();
326 self.smash_gas(temporary_store);
327 }
328
329 pub fn charge_gas<T, E: ExecutionErrorTrait>(
340 &mut self,
341 temporary_store: &mut TemporaryStore<'_>,
342 execution_result: &mut Result<T, E>,
343 ) -> GasCostSummary {
344 debug_assert!(self.gas_status.storage_rebate() == 0);
347 debug_assert!(self.gas_status.storage_gas_units() == 0);
348
349 if !matches!(&self.payment, PaymentMetadata::Unmetered) {
350 let is_move_abort = execution_result
352 .as_ref()
353 .err()
354 .map(|err| {
355 matches!(
356 err.kind(),
357 sui_types::execution_status::ExecutionErrorKind::MoveAbort(_, _)
358 )
359 })
360 .unwrap_or(false);
361 if let Err(err) = self.gas_status.bucketize_computation(Some(is_move_abort))
363 && execution_result.is_ok()
364 {
365 *execution_result = Err(err.into());
366 }
367
368 if execution_result.is_err() {
370 self.reset(temporary_store);
371 }
372 }
373
374 temporary_store.ensure_active_inputs_mutated();
376 temporary_store.collect_storage_and_rebate(self);
377
378 let gas_payment_location = match &self.payment {
379 PaymentMetadata::Unmetered => {
380 return GasCostSummary::default();
381 }
382 PaymentMetadata::Gasless => None,
383 PaymentMetadata::Smash(metadata) => Some(metadata.gas_charge_location),
384 };
385 if let Some(PaymentLocation::Coin(_)) = gas_payment_location {
386 #[skip_checked_arithmetic]
387 trace!(target: "replay_gas_info", "Gas smashing has occurred for this transaction");
388 }
389
390 if execution_result
391 .as_ref()
392 .err()
393 .map(|err| {
394 matches!(
395 err.kind(),
396 sui_types::execution_status::ExecutionErrorKind::InsufficientFundsForWithdraw
397 )
398 })
399 .unwrap_or(false)
400 && matches!(gas_payment_location, Some(PaymentLocation::AddressBalance(_))) {
401 return GasCostSummary::default();
404 }
405
406 self.compute_storage_and_rebate(temporary_store, execution_result);
407
408 let cost_summary = self.gas_status.summary();
409
410 let Some(gas_payment_location) = gas_payment_location else {
411 assert!(
413 matches!(self.payment, PaymentMetadata::Gasless),
414 "Only gasless transactions should reach this point without a payment location"
415 );
416 if execution_result.is_err() {
417 return GasCostSummary::default();
418 }
419 let storage_cost = cost_summary.storage_cost;
422 assert!(
423 storage_cost == 0,
424 "Gasless transaction must not incur storage cost, got {storage_cost}"
425 );
426 let sender_rebate = cost_summary.storage_rebate;
427 return GasCostSummary {
428 computation_cost: sender_rebate,
429 storage_cost: 0,
430 storage_rebate: sender_rebate,
431 non_refundable_storage_fee: cost_summary.non_refundable_storage_fee,
432 };
433 };
434
435 let net_change = cost_summary.net_gas_usage();
436
437 match gas_payment_location {
438 PaymentLocation::AddressBalance(payer_address) => {
439 if net_change != 0 {
441 let balance_type = sui_types::balance::Balance::type_tag(
442 sui_types::gas_coin::GAS::type_tag(),
443 );
444 let event = AccumulatorEvent::from_balance_change(
445 payer_address,
446 balance_type,
447 net_change.checked_neg().unwrap(),
448 )
449 .expect("Failed to create accumulator event for gas charging");
450 temporary_store.add_accumulator_event(event);
451 }
452 }
453 PaymentLocation::Coin(gas_object_id) => {
454 let mut gas_object =
455 temporary_store.read_object(&gas_object_id).unwrap().clone();
456 deduct_gas(&mut gas_object, net_change);
457 #[skip_checked_arithmetic]
458 trace!(net_change, gas_obj_id =? gas_object.id(), gas_obj_ver =? gas_object.version(), "Updated gas object");
459 temporary_store.mutate_new_or_input_object(gas_object);
460 }
461 }
462 cost_summary
463 }
464
465 fn compute_storage_and_rebate<T, E: ExecutionErrorTrait>(
477 &mut self,
478 temporary_store: &mut TemporaryStore<'_>,
479 execution_result: &mut Result<T, E>,
480 ) {
481 if dont_charge_budget_on_storage_oog(self.gas_model_version) {
482 self.handle_storage_and_rebate_v2(temporary_store, execution_result)
483 } else {
484 self.handle_storage_and_rebate_v1(temporary_store, execution_result)
485 }
486 }
487
488 fn handle_storage_and_rebate_v1<T, E: ExecutionErrorTrait>(
489 &mut self,
490 temporary_store: &mut TemporaryStore<'_>,
491 execution_result: &mut Result<T, E>,
492 ) {
493 if let Err(err) = self.gas_status.charge_storage_and_rebate() {
494 self.reset(temporary_store);
495 self.gas_status.adjust_computation_on_out_of_gas();
496 temporary_store.ensure_active_inputs_mutated();
497 temporary_store.collect_rebate(self);
498 if execution_result.is_ok() {
499 *execution_result = Err(err.into());
500 }
501 }
502 }
503
504 fn handle_storage_and_rebate_v2<T, E: ExecutionErrorTrait>(
505 &mut self,
506 temporary_store: &mut TemporaryStore<'_>,
507 execution_result: &mut Result<T, E>,
508 ) {
509 if let Err(err) = self.gas_status.charge_storage_and_rebate() {
510 self.reset(temporary_store);
514 temporary_store.ensure_active_inputs_mutated();
515 temporary_store.collect_storage_and_rebate(self);
516 if let Err(err) = self.gas_status.charge_storage_and_rebate() {
517 self.reset(temporary_store);
520 self.gas_status.adjust_computation_on_out_of_gas();
521 temporary_store.ensure_active_inputs_mutated();
522 temporary_store.collect_rebate(self);
523 if execution_result.is_ok() {
524 *execution_result = Err(err.into());
525 }
526 } else if execution_result.is_ok() {
527 *execution_result = Err(err.into());
528 }
529 }
530 }
531 }
532
533 impl SmashMetadata {
534 fn payment_methods(&self) -> impl Iterator<Item = &'_ PaymentMethod> {
536 std::iter::once(&self.smash_target).chain(self.smashed_payments.values())
537 }
538
539 fn smash_gas(
540 &mut self,
541 tx_digest: &TransactionDigest,
542 temporary_store: &mut TemporaryStore<'_>,
543 ) {
544 self.gas_charge_location = self.smash_target.location();
546
547 let total_smashed = self
549 .payment_methods()
550 .map(|payment| match payment {
551 PaymentMethod::AddressBalance(_, reservation) => Ok(*reservation),
552 PaymentMethod::Coin(obj_ref) => {
553 let obj_data = temporary_store
554 .objects()
555 .get(&obj_ref.0)
556 .map(|obj| &obj.data);
557 let Some(Data::Move(move_obj)) = obj_data else {
558 return Err(ExecutionError::invariant_violation(
559 "Provided non-gas coin object as input for gas!",
560 ));
561 };
562 if !move_obj.type_().is_gas_coin() {
563 return Err(ExecutionError::invariant_violation(
564 "Provided non-gas coin object as input for gas!",
565 ));
566 }
567 Ok(move_obj.get_coin_value_unsafe())
568 }
569 })
570 .collect::<Result<Vec<u64>, ExecutionError>>()
571 .unwrap_or_else(|_| {
574 panic!(
575 "Unable to process gas payments for transaction {}",
576 tx_digest
577 )
578 })
579 .iter()
580 .sum();
581 debug_assert!(
585 self.total_smashed == 0 || self.total_smashed == total_smashed,
586 "Gas smashing should not change after a reset"
587 );
588 self.total_smashed = total_smashed;
589
590 let smash_location = self.smash_target.location();
591 for payment_method in self.smashed_payments.values() {
593 let location = payment_method.location();
594 assert_ne!(location, smash_location, "Payment methods must be unique");
595 match payment_method {
596 PaymentMethod::AddressBalance(sui_address, reservation) => {
597 let balance_type = sui_types::balance::Balance::type_tag(
598 sui_types::gas_coin::GAS::type_tag(),
599 );
600 let event = AccumulatorEvent::from_balance_change(
601 *sui_address,
602 balance_type,
603 i64::try_from(*reservation).unwrap().checked_neg().unwrap(),
604 )
605 .expect("Failed to create accumulator event for gas smashing");
606 temporary_store.add_accumulator_event(event);
607 }
608 PaymentMethod::Coin((id, _, _)) => {
609 temporary_store.delete_input_object(id);
610 }
611 }
612 }
613 match &self.smash_target {
614 PaymentMethod::AddressBalance(sui_address, reservation) => {
615 let deposit = total_smashed - *reservation;
619 if deposit != 0 {
620 let balance_type = sui_types::balance::Balance::type_tag(
621 sui_types::gas_coin::GAS::type_tag(),
622 );
623 let event = AccumulatorEvent::from_balance_change(
624 *sui_address,
625 balance_type,
626 i64::try_from(deposit).unwrap(),
627 )
628 .expect("Failed to create accumulator event for gas smashing");
629 temporary_store.add_accumulator_event(event);
630 }
631 }
632 PaymentMethod::Coin((gas_coin_id, _, _)) => {
633 let mut primary_gas_object = temporary_store
634 .objects()
635 .get(gas_coin_id)
636 .unwrap_or_else(|| {
638 panic!(
639 "Invariant violation: gas coin not found in store in txn {}",
640 tx_digest
641 )
642 })
643 .clone();
644 primary_gas_object
645 .data
646 .try_as_move_mut()
647 .unwrap_or_else(|| {
649 panic!(
650 "Invariant violation: invalid coin object in txn {}",
651 tx_digest
652 )
653 })
654 .set_coin_value_unsafe(total_smashed);
655 temporary_store.mutate_input_object(primary_gas_object);
656 }
657 }
658 }
659
660 fn used_coins(&self) -> impl Iterator<Item = &'_ ObjectRef> {
661 self.payment_methods().filter_map(|method| match method {
662 PaymentMethod::Coin(obj_ref) => Some(obj_ref),
663 PaymentMethod::AddressBalance(_, _) => None,
664 })
665 }
666 }
667
668 impl PaymentKind {
669 pub fn unmetered() -> Self {
670 Self(PaymentKind_::Unmetered)
671 }
672
673 pub fn gasless() -> Self {
674 Self(PaymentKind_::Gasless)
675 }
676
677 pub fn smash(payment_methods: Vec<PaymentMethod>) -> Option<Self> {
678 debug_assert!(
679 !payment_methods.is_empty(),
680 "GasCharger must have at least one payment method"
681 );
682 if payment_methods.is_empty() {
683 return None;
684 }
685 let mut unique_methods = IndexMap::new();
686 for payment_method in payment_methods {
687 match (
688 unique_methods.entry(payment_method.location()),
689 payment_method,
690 ) {
691 (indexmap::map::Entry::Vacant(entry), payment_method) => {
692 entry.insert(payment_method);
693 }
694 (
695 indexmap::map::Entry::Occupied(mut occupied),
696 PaymentMethod::AddressBalance(other, additional),
697 ) => {
698 let PaymentMethod::AddressBalance(addr, amount) = occupied.get_mut() else {
699 unreachable!("Payment method does not match location")
700 };
701 assert_eq!(*addr, other, "Payment method does not match location");
702 *amount += additional;
703 }
704 (indexmap::map::Entry::Occupied(_), _) => {
705 debug_assert!(
706 false,
707 "Duplicate coin payment method found, \
708 which should have been prevented by input checks"
709 );
710 return None;
711 }
712 }
713 }
714 Some(Self(PaymentKind_::Smash(unique_methods)))
715 }
716 }
717
718 impl PaymentMethod {
719 pub fn location(&self) -> PaymentLocation {
720 match self {
721 PaymentMethod::Coin(obj_ref) => PaymentLocation::Coin(obj_ref.0),
722 PaymentMethod::AddressBalance(addr, _) => PaymentLocation::AddressBalance(*addr),
723 }
724 }
725 }
726}