iota_types/gas_model/gas_v1.rs
1// Copyright (c) 2021, Facebook, Inc. and its affiliates
2// Copyright (c) Mysten Labs, Inc.
3// Modifications Copyright (c) 2024 IOTA Stiftung
4// SPDX-License-Identifier: Apache-2.0
5
6pub use checked::*;
7
8#[iota_macros::with_checked_arithmetic]
9mod checked {
10 use iota_protocol_config::*;
11 use move_core_types::vm_status::StatusCode;
12
13 use crate::{
14 ObjectID,
15 error::{ExecutionError, ExecutionErrorKind, UserInputError, UserInputResult},
16 gas::{self, GasCostSummary, IotaGasStatusAPI},
17 gas_model::{
18 gas_predicates::cost_table_for_version,
19 tables::{GasStatus, ZERO_COST_SCHEDULE},
20 units_types::CostTable,
21 },
22 transaction::ObjectReadResult,
23 };
24
25 /// A bucket defines a range of units that will be priced the same.
26 /// After execution a call to `GasStatus::bucketize` will round the
27 /// computation cost to `cost` for the bucket ([`min`, `max`]) the gas
28 /// used falls into.
29 #[expect(dead_code)]
30 pub(crate) struct ComputationBucket {
31 min: u64,
32 max: u64,
33 cost: u64,
34 }
35
36 impl ComputationBucket {
37 fn new(min: u64, max: u64, cost: u64) -> Self {
38 ComputationBucket { min, max, cost }
39 }
40
41 fn simple(min: u64, max: u64) -> Self {
42 Self::new(min, max, max)
43 }
44 }
45
46 fn get_bucket_cost(table: &[ComputationBucket], computation_cost: u64) -> u64 {
47 for bucket in table {
48 if bucket.max >= computation_cost {
49 return bucket.cost;
50 }
51 }
52 match table.last() {
53 // maybe not a literal here could be better?
54 None => 5_000_000,
55 Some(bucket) => bucket.cost,
56 }
57 }
58
59 // define the bucket table for computation charging
60 // If versioning defines multiple functions and
61 fn computation_bucket(max_bucket_cost: u64) -> Vec<ComputationBucket> {
62 assert!(max_bucket_cost >= 5_000_000);
63 vec![
64 ComputationBucket::simple(0, 1_000),
65 ComputationBucket::simple(1_000, 5_000),
66 ComputationBucket::simple(5_000, 10_000),
67 ComputationBucket::simple(10_000, 20_000),
68 ComputationBucket::simple(20_000, 50_000),
69 ComputationBucket::simple(50_000, 200_000),
70 ComputationBucket::simple(200_000, 1_000_000),
71 ComputationBucket::simple(1_000_000, max_bucket_cost),
72 ]
73 }
74
75 /// Portion of the storage rebate that gets passed on to the transaction
76 /// sender. The remainder will be burned, then re-minted + added to the
77 /// storage fund at the next epoch change
78 fn sender_rebate(storage_rebate: u64, storage_rebate_rate: u64) -> u64 {
79 // we round storage rebate such that `>= x.5` goes to x+1 (rounds up) and
80 // `< x.5` goes to x (truncates). We replicate `f32/64::round()`
81 const BASIS_POINTS: u128 = 10000;
82 (((storage_rebate as u128 * storage_rebate_rate as u128)
83 + (BASIS_POINTS / 2)) // integer rounding adds half of the BASIS_POINTS (denominator)
84 / BASIS_POINTS) as u64
85 }
86
87 /// A list of constant costs of various operations in IOTA.
88 pub struct IotaCostTable {
89 /// A flat fee charged for every transaction. This is also the minimum
90 /// amount of gas charged for a transaction.
91 pub(crate) min_transaction_cost: u64,
92 /// Maximum allowable budget for a transaction.
93 pub(crate) max_gas_budget: u64,
94 /// Computation cost per byte charged for package publish. This cost is
95 /// primarily determined by the cost to verify and link a
96 /// package. Note that this does not include the cost of writing
97 /// the package to the store.
98 package_publish_per_byte_cost: u64,
99 /// Per byte cost to read objects from the store. This is computation
100 /// cost instead of storage cost because it does not change the
101 /// amount of data stored on the db.
102 object_read_per_byte_cost: u64,
103 /// Unit cost of a byte in the storage. This will be used both for
104 /// charging for new storage as well as rebating for deleting
105 /// storage. That is, we expect users to get full refund on the
106 /// object storage when it's deleted.
107 storage_per_byte_cost: u64,
108 /// Execution cost table to be used.
109 pub execution_cost_table: CostTable,
110 /// Computation buckets to cost transaction in price groups
111 computation_bucket: Vec<ComputationBucket>,
112 }
113
114 impl std::fmt::Debug for IotaCostTable {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 // TODO: dump the fields.
117 write!(f, "IotaCostTable(...)")
118 }
119 }
120
121 impl IotaCostTable {
122 pub(crate) fn new(c: &ProtocolConfig, gas_price: u64) -> Self {
123 // gas_price here is the Reference Gas Price, however we may decide
124 // to change it to be the price passed in the transaction
125 let min_transaction_cost = c.base_tx_cost_fixed() * gas_price;
126 Self {
127 min_transaction_cost,
128 max_gas_budget: c.max_tx_gas(),
129 package_publish_per_byte_cost: c.package_publish_cost_per_byte(),
130 object_read_per_byte_cost: c.obj_access_cost_read_per_byte(),
131 storage_per_byte_cost: c.obj_data_cost_refundable(),
132 execution_cost_table: cost_table_for_version(c.gas_model_version()),
133 computation_bucket: computation_bucket(c.max_gas_computation_bucket()),
134 }
135 }
136
137 pub(crate) fn unmetered() -> Self {
138 Self {
139 min_transaction_cost: 0,
140 max_gas_budget: u64::MAX,
141 package_publish_per_byte_cost: 0,
142 object_read_per_byte_cost: 0,
143 storage_per_byte_cost: 0,
144 execution_cost_table: ZERO_COST_SCHEDULE.clone(),
145 // should not matter
146 computation_bucket: computation_bucket(5_000_000),
147 }
148 }
149 }
150
151 #[derive(Debug)]
152 pub struct PerObjectStorage {
153 /// storage_cost is the total storage gas to charge. This is computed
154 /// at the end of execution while determining storage charges.
155 /// It tracks `storage_bytes * obj_data_cost_refundable` as
156 /// described in `storage_gas_price`
157 /// It has been multiplied by the storage gas price. This is the new
158 /// storage rebate.
159 pub storage_cost: u64,
160 /// storage_rebate is the storage rebate (in IOTA) for in this object.
161 /// This is computed at the end of execution while determining storage
162 /// charges. The value is in IOTA.
163 pub storage_rebate: u64,
164 /// The object size post-transaction in bytes
165 pub new_size: u64,
166 }
167
168 #[derive(Debug)]
169 pub struct IotaGasStatus {
170 /// GasStatus as used by the VM, that is all the VM sees
171 pub gas_status: GasStatus,
172 /// Cost table contains a set of constant/config for the gas
173 /// model/charging
174 cost_table: IotaCostTable,
175 /// Gas budget for this gas status instance.
176 /// Typically the gas budget as defined in the
177 /// `TransactionData::GasData`
178 gas_budget: u64,
179 /// Computation cost after execution. This is the result of the gas used
180 /// by the `GasStatus` properly bucketized.
181 /// Starts at 0 and it is assigned in `bucketize_computation`.
182 computation_cost: u64,
183 /// Whether to charge or go unmetered
184 charge: bool,
185 /// Gas price for computation.
186 /// This is a multiplier on the final charge as related to the RGP
187 /// (reference gas price). Checked at signing: `gas_price >=
188 /// reference_gas_price` and then conceptually
189 /// `final_computation_cost = total_computation_cost * gas_price /
190 /// reference_gas_price`
191 gas_price: u64,
192 // Reference gas price as defined in protocol config.
193 // If `protocol_defined_base_fee' is enabled, this is a mandatory base fee paid to the
194 // protocol.
195 reference_gas_price: u64,
196 /// Gas price for storage. This is a multiplier on the final charge
197 /// as related to the storage gas price defined in the system
198 /// (`ProtocolConfig::storage_gas_price`).
199 /// Conceptually, given a constant `obj_data_cost_refundable`
200 /// (defined in `ProtocolConfig::obj_data_cost_refundable`)
201 /// `total_storage_cost = storage_bytes * obj_data_cost_refundable`
202 /// `final_storage_cost = total_storage_cost * storage_gas_price`
203 storage_gas_price: u64,
204 /// Per Object Storage Cost and Storage Rebate, used to get accumulated
205 /// values at the end of execution to determine storage charges
206 /// and rebates.
207 per_object_storage: Vec<(ObjectID, PerObjectStorage)>,
208 // storage rebate rate as defined in the ProtocolConfig
209 rebate_rate: u64,
210 /// Amount of storage rebate accumulated when we are running in
211 /// unmetered mode (i.e. system transaction). This allows us to
212 /// track how much storage rebate we need to retain in system
213 /// transactions.
214 unmetered_storage_rebate: u64,
215 /// Rounding value to round up gas charges.
216 gas_rounding_step: Option<u64>,
217 /// Flag to indicate whether the protocol-defined base fee is enabled,
218 /// in which case the reference gas price is burned.
219 protocol_defined_base_fee: bool,
220 }
221
222 impl IotaGasStatus {
223 fn new(
224 move_gas_status: GasStatus,
225 gas_budget: u64,
226 charge: bool,
227 gas_price: u64,
228 reference_gas_price: u64,
229 storage_gas_price: u64,
230 rebate_rate: u64,
231 gas_rounding_step: Option<u64>,
232 cost_table: IotaCostTable,
233 protocol_defined_base_fee: bool,
234 ) -> IotaGasStatus {
235 let gas_rounding_step = gas_rounding_step.map(|val| val.max(1));
236 IotaGasStatus {
237 gas_status: move_gas_status,
238 gas_budget,
239 charge,
240 computation_cost: 0,
241 gas_price,
242 reference_gas_price,
243 storage_gas_price,
244 per_object_storage: Vec::new(),
245 rebate_rate,
246 unmetered_storage_rebate: 0,
247 gas_rounding_step,
248 cost_table,
249 protocol_defined_base_fee,
250 }
251 }
252
253 pub(crate) fn new_with_budget(
254 gas_budget: u64,
255 gas_price: u64,
256 reference_gas_price: u64,
257 config: &ProtocolConfig,
258 ) -> IotaGasStatus {
259 let storage_gas_price = config.storage_gas_price();
260 let computation_budget = computation_budget(gas_budget, gas_price, config);
261 let iota_cost_table = IotaCostTable::new(config, gas_price);
262 let gas_rounding_step = config.gas_rounding_step_as_option();
263 Self::new(
264 GasStatus::new(
265 iota_cost_table.execution_cost_table.clone(),
266 computation_budget,
267 gas_price,
268 config.gas_model_version(),
269 ),
270 gas_budget,
271 true,
272 gas_price,
273 reference_gas_price,
274 storage_gas_price,
275 config.storage_rebate_rate(),
276 gas_rounding_step,
277 iota_cost_table,
278 config.protocol_defined_base_fee(),
279 )
280 }
281
282 pub fn new_unmetered() -> IotaGasStatus {
283 Self::new(
284 GasStatus::new_unmetered(),
285 0,
286 false,
287 0,
288 0,
289 0,
290 0,
291 None,
292 IotaCostTable::unmetered(),
293 false,
294 )
295 }
296
297 pub fn reference_gas_price(&self) -> u64 {
298 self.reference_gas_price
299 }
300
301 // Check whether gas arguments are legit:
302 // 1. Gas object has an address owner.
303 // 2. Gas budget is between min and max budget allowed
304 // 3. Gas balance (all gas coins together) is bigger or equal to budget
305 pub(crate) fn check_gas_balance(
306 &self,
307 gas_objs: &[&ObjectReadResult],
308 gas_budget: u64,
309 ) -> UserInputResult {
310 // 1. All gas objects have an address owner
311 for gas_object in gas_objs {
312 // if as_object() returns None, it means the object has been deleted (and
313 // therefore must be a shared object).
314 if let Some(obj) = gas_object.as_object() {
315 if !obj.is_address_owned() {
316 return Err(UserInputError::GasObjectNotOwnedObject { owner: obj.owner });
317 }
318 } else {
319 // This case should never happen (because gas can't be a shared object), but we
320 // handle this case for future-proofing
321 return Err(UserInputError::MissingGasPayment);
322 }
323 }
324
325 // 2. Gas budget is between min and max budget allowed
326 if gas_budget > self.cost_table.max_gas_budget {
327 return Err(UserInputError::GasBudgetTooHigh {
328 gas_budget,
329 max_budget: self.cost_table.max_gas_budget,
330 });
331 }
332 if gas_budget < self.cost_table.min_transaction_cost {
333 return Err(UserInputError::GasBudgetTooLow {
334 gas_budget,
335 min_budget: self.cost_table.min_transaction_cost,
336 });
337 }
338
339 // 3. Gas balance (all gas coins together) is bigger or equal to budget
340 let mut gas_balance = 0u128;
341 for gas_obj in gas_objs {
342 // expect is safe because we already checked that all gas objects have an
343 // address owner
344 gas_balance +=
345 gas::get_gas_balance(gas_obj.as_object().expect("object must be owned"))?
346 as u128;
347 }
348 if gas_balance < gas_budget as u128 {
349 Err(UserInputError::GasBalanceTooLow {
350 gas_balance,
351 needed_gas_amount: gas_budget as u128,
352 })
353 } else {
354 Ok(())
355 }
356 }
357
358 fn storage_cost(&self) -> u64 {
359 self.storage_gas_units()
360 }
361
362 pub fn per_object_storage(&self) -> &Vec<(ObjectID, PerObjectStorage)> {
363 &self.per_object_storage
364 }
365 }
366
367 impl IotaGasStatusAPI for IotaGasStatus {
368 fn is_unmetered(&self) -> bool {
369 !self.charge
370 }
371
372 fn move_gas_status(&self) -> &GasStatus {
373 &self.gas_status
374 }
375
376 fn move_gas_status_mut(&mut self) -> &mut GasStatus {
377 &mut self.gas_status
378 }
379
380 fn bucketize_computation(&mut self) -> Result<(), ExecutionError> {
381 let mut computation_units = self.gas_status.gas_used_pre_gas_price();
382 if let Some(gas_rounding) = self.gas_rounding_step {
383 if gas_rounding > 0
384 && (computation_units == 0 || computation_units % gas_rounding > 0)
385 {
386 computation_units = ((computation_units / gas_rounding) + 1) * gas_rounding;
387 }
388 } else {
389 // use the max value of the bucket that the computation_units falls into.
390 computation_units =
391 get_bucket_cost(&self.cost_table.computation_bucket, computation_units);
392 };
393 let computation_cost = computation_units * self.gas_price;
394 if self.gas_budget <= computation_cost {
395 self.computation_cost = self.gas_budget;
396 Err(ExecutionErrorKind::InsufficientGas.into())
397 } else {
398 self.computation_cost = computation_cost;
399 Ok(())
400 }
401 }
402
403 /// Returns the final (computation cost, storage cost, storage rebate)
404 /// of the gas meter. We use initial budget, combined with
405 /// remaining gas and storage cost to derive computation cost.
406 fn summary(&self) -> GasCostSummary {
407 // compute computation cost burned and storage rebate, both rebate and non
408 // refundable fee
409 let computation_cost_burned = if self.protocol_defined_base_fee {
410 // when protocol_defined_base_fee is enabled, the computation cost burned is
411 // computed as follows:
412 // computation_cost_burned = computation_units * reference_gas_price.
413 // = (computation_cost / gas_price) * reference_gas_price
414 self.computation_cost * self.reference_gas_price / self.gas_price
415 } else {
416 // when protocol_defined_base_fee is disabled, the entire computation cost is
417 // burned.
418 self.computation_cost
419 };
420 let storage_rebate = self.storage_rebate();
421 let sender_rebate = sender_rebate(storage_rebate, self.rebate_rate);
422 assert!(sender_rebate <= storage_rebate);
423 let non_refundable_storage_fee = storage_rebate - sender_rebate;
424 GasCostSummary {
425 computation_cost: self.computation_cost,
426 computation_cost_burned,
427 storage_cost: self.storage_cost(),
428 storage_rebate: sender_rebate,
429 non_refundable_storage_fee,
430 }
431 }
432
433 fn gas_budget(&self) -> u64 {
434 self.gas_budget
435 }
436
437 fn gas_price(&self) -> u64 {
438 self.gas_price
439 }
440
441 fn storage_gas_units(&self) -> u64 {
442 self.per_object_storage
443 .iter()
444 .map(|(_, per_object)| per_object.storage_cost)
445 .sum()
446 }
447
448 fn storage_rebate(&self) -> u64 {
449 self.per_object_storage
450 .iter()
451 .map(|(_, per_object)| per_object.storage_rebate)
452 .sum()
453 }
454
455 fn unmetered_storage_rebate(&self) -> u64 {
456 self.unmetered_storage_rebate
457 }
458
459 fn gas_used(&self) -> u64 {
460 self.gas_status.gas_used_pre_gas_price()
461 }
462
463 fn reset_storage_cost_and_rebate(&mut self) {
464 self.per_object_storage = Vec::new();
465 self.unmetered_storage_rebate = 0;
466 }
467
468 fn charge_storage_read(&mut self, size: usize) -> Result<(), ExecutionError> {
469 self.gas_status
470 .charge_bytes(size, self.cost_table.object_read_per_byte_cost)
471 .map_err(|e| {
472 debug_assert_eq!(e.major_status(), StatusCode::OUT_OF_GAS);
473 ExecutionErrorKind::InsufficientGas.into()
474 })
475 }
476
477 fn charge_publish_package(&mut self, size: usize) -> Result<(), ExecutionError> {
478 self.gas_status
479 .charge_bytes(size, self.cost_table.package_publish_per_byte_cost)
480 .map_err(|e| {
481 debug_assert_eq!(e.major_status(), StatusCode::OUT_OF_GAS);
482 ExecutionErrorKind::InsufficientGas.into()
483 })
484 }
485
486 /// Update `storage_rebate` and `storage_gas_units` for each object in
487 /// the transaction. There is no charge in this function.
488 /// Charges will all be applied together at the end
489 /// (`track_storage_mutation`).
490 /// Return the new storage rebate (cost of object storage) according to
491 /// `new_size`.
492 fn track_storage_mutation(
493 &mut self,
494 object_id: ObjectID,
495 new_size: usize,
496 storage_rebate: u64,
497 ) -> u64 {
498 if self.is_unmetered() {
499 self.unmetered_storage_rebate += storage_rebate;
500 return 0;
501 }
502
503 // compute and track cost (based on size)
504 let new_size = new_size as u64;
505 let storage_cost =
506 new_size * self.cost_table.storage_per_byte_cost * self.storage_gas_price;
507 // track rebate
508
509 self.per_object_storage.push((
510 object_id,
511 PerObjectStorage {
512 storage_cost,
513 storage_rebate,
514 new_size,
515 },
516 ));
517 // return the new object rebate (object storage cost)
518 storage_cost
519 }
520
521 fn charge_storage_and_rebate(&mut self) -> Result<(), ExecutionError> {
522 let storage_rebate = self.storage_rebate();
523 let storage_cost = self.storage_cost();
524 let sender_rebate = sender_rebate(storage_rebate, self.rebate_rate);
525 assert!(sender_rebate <= storage_rebate);
526 if sender_rebate >= storage_cost {
527 // there is more rebate than cost, when deducting gas we are adding
528 // to whatever is the current amount charged so we are `Ok`
529 Ok(())
530 } else {
531 let gas_left = self.gas_budget - self.computation_cost;
532 // we have to charge for storage and may go out of gas, check
533 if gas_left < storage_cost - sender_rebate {
534 // Running out of gas would cause the temporary store to reset
535 // and zero storage and rebate.
536 // The remaining_gas will be 0 and we will charge all in computation
537 Err(ExecutionErrorKind::InsufficientGas.into())
538 } else {
539 Ok(())
540 }
541 }
542 }
543
544 fn adjust_computation_on_out_of_gas(&mut self) {
545 self.per_object_storage = Vec::new();
546 self.computation_cost = self.gas_budget;
547 }
548 }
549
550 pub fn computation_budget(gas_budget: u64, gas_price: u64, config: &ProtocolConfig) -> u64 {
551 let max_computation_budget = config.max_gas_computation_bucket() * gas_price;
552
553 if gas_budget > max_computation_budget {
554 max_computation_budget
555 } else {
556 gas_budget
557 }
558 }
559}