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