Skip to content

Commit 0e92fb3

Browse files
committed
feat: strongly typed gas units
This adds a new gas type to avoid things like: 1. Multiplying gas by gas (not possible now). 2. Swapping gas/milligas. 3. Not using saturating arithmetic. This change also caught and fixed a bug where the "extra" consensus fault fee from lotus was being charged as milligas, not gas. Motivation: the previous commit that fixed a bug in window post cost scaling.
1 parent 53deec5 commit 0e92fb3

File tree

12 files changed

+400
-329
lines changed

12 files changed

+400
-329
lines changed

fvm/src/call_manager/default.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use num_traits::Zero;
1111
use super::{Backtrace, CallManager, InvocationResult, NO_DATA_BLOCK_ID};
1212
use crate::call_manager::backtrace::Frame;
1313
use crate::call_manager::FinishRet;
14-
use crate::gas::GasTracker;
14+
use crate::gas::{Gas, GasTracker};
1515
use crate::kernel::{ExecutionError, Kernel, Result, SyscallError};
1616
use crate::machine::Machine;
1717
use crate::syscalls::error::Abort;
@@ -71,7 +71,7 @@ where
7171
fn new(machine: M, gas_limit: i64, origin: Address, nonce: u64) -> Self {
7272
DefaultCallManager(Some(Box::new(InnerDefaultCallManager {
7373
machine,
74-
gas_tracker: GasTracker::new(gas_limit, 0),
74+
gas_tracker: GasTracker::new(Gas::new(gas_limit), Gas::zero()),
7575
origin,
7676
nonce,
7777
num_actors_created: 0,
@@ -154,7 +154,7 @@ where
154154
}
155155

156156
fn finish(mut self) -> (FinishRet, Self::Machine) {
157-
let gas_used = self.gas_tracker.gas_used().max(0);
157+
let gas_used = self.gas_tracker.gas_used().max(Gas::zero()).round_up();
158158

159159
let inner = self.0.take().expect("call manager is poisoned");
160160
// TODO: Having to check against zero here is fishy, but this is what lotus does.

fvm/src/executor/default.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use num_traits::Zero;
1515

1616
use super::{ApplyFailure, ApplyKind, ApplyRet, Executor};
1717
use crate::call_manager::{backtrace, CallManager, InvocationResult};
18-
use crate::gas::{milligas_to_gas, GasCharge, GasOutputs};
18+
use crate::gas::{Gas, GasCharge, GasOutputs};
1919
use crate::kernel::{ClassifyResult, Context as _, ExecutionError, Kernel};
2020
use crate::machine::{Machine, BURNT_FUNDS_ACTOR_ADDR, REWARD_ACTOR_ADDR};
2121

@@ -231,10 +231,13 @@ where
231231
let pl = &self.context().price_list;
232232

233233
let (inclusion_cost, miner_penalty_amount) = match apply_kind {
234-
ApplyKind::Implicit => (GasCharge::new("none", 0, 0), Default::default()),
234+
ApplyKind::Implicit => (
235+
GasCharge::new("none", Gas::zero(), Gas::zero()),
236+
Default::default(),
237+
),
235238
ApplyKind::Explicit => {
236239
let inclusion_cost = pl.on_chain_message(raw_length);
237-
let inclusion_total = milligas_to_gas(inclusion_cost.total(), true);
240+
let inclusion_total = inclusion_cost.total().round_up();
238241

239242
// Verify the cost of the message is not over the message gas limit.
240243
if inclusion_total > msg.gas_limit {

fvm/src/gas/charge.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
// Copyright 2019-2022 ChainSafe Systems
22
// SPDX-License-Identifier: Apache-2.0, MIT
33

4-
use crate::gas::Milligas;
4+
use super::Gas;
55

66
/// Single gas charge in the VM. Contains information about what gas was for, as well
77
/// as the amount of gas needed for computation and storage respectively.
88
pub struct GasCharge<'a> {
99
pub name: &'a str,
10-
/// Compute costs in milligas.
11-
pub compute_gas: Milligas,
12-
/// Storage costs in milligas.
13-
pub storage_gas: Milligas,
10+
/// Compute costs
11+
pub compute_gas: Gas,
12+
/// Storage costs
13+
pub storage_gas: Gas,
1414
}
1515

1616
impl<'a> GasCharge<'a> {
17-
pub fn new(name: &'a str, compute_gas: Milligas, storage_gas: Milligas) -> Self {
17+
pub fn new(name: &'a str, compute_gas: Gas, storage_gas: Gas) -> Self {
1818
Self {
1919
name,
2020
compute_gas,
@@ -24,7 +24,7 @@ impl<'a> GasCharge<'a> {
2424

2525
/// Calculates total gas charge (in milligas) by summing compute and
2626
/// storage gas associated with this charge.
27-
pub fn total(&self) -> Milligas {
27+
pub fn total(&self) -> Gas {
2828
self.compute_gas + self.storage_gas
2929
}
3030
}

fvm/src/gas/mod.rs

Lines changed: 157 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
// Copyright 2019-2022 ChainSafe Systems
22
// SPDX-License-Identifier: Apache-2.0, MIT
33

4+
use std::fmt::{Debug, Display};
5+
use std::ops::{Add, AddAssign, Mul, Sub, SubAssign};
6+
47
pub use self::charge::GasCharge;
58
pub(crate) use self::outputs::GasOutputs;
69
pub use self::price_list::{price_list_by_network_version, PriceList, WasmGasPrices};
@@ -12,88 +15,184 @@ mod price_list;
1215

1316
pub const MILLIGAS_PRECISION: i64 = 1000;
1417

15-
// Type aliases to disambiguate units in interfaces.
16-
pub type Gas = i64;
17-
pub type Milligas = i64;
18+
/// A typesafe representation of gas (internally stored as milligas).
19+
///
20+
/// - All math operations are _saturating_ and never overflow.
21+
/// - Enforces correct units by making it impossible to, e.g., get gas squared (by multiplying gas
22+
/// by gas).
23+
/// - Makes it harder to confuse gas and milligas.
24+
#[derive(Hash, Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Default)]
25+
pub struct Gas(i64 /* milligas */);
26+
27+
impl Debug for Gas {
28+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29+
if self.0 == 0 {
30+
f.debug_tuple("Gas").field(&0 as &dyn Debug).finish()
31+
} else {
32+
let integral = self.0 / MILLIGAS_PRECISION;
33+
let fractional = self.0 % MILLIGAS_PRECISION;
34+
f.debug_tuple("Gas")
35+
.field(&format_args!("{integral}.{fractional:03}"))
36+
.finish()
37+
}
38+
}
39+
}
40+
41+
impl Display for Gas {
42+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43+
if self.0 == 0 {
44+
f.write_str("0")
45+
} else {
46+
let integral = self.0 / MILLIGAS_PRECISION;
47+
let fractional = self.0 % MILLIGAS_PRECISION;
48+
write!(f, "{integral}.{fractional:03}")
49+
}
50+
}
51+
}
52+
53+
impl Gas {
54+
#[inline]
55+
pub fn from_milligas(milligas: i64) -> Gas {
56+
Gas(milligas)
57+
}
58+
59+
#[inline]
60+
pub fn new(gas: i64) -> Gas {
61+
Gas(gas.saturating_mul(MILLIGAS_PRECISION))
62+
}
63+
64+
#[inline]
65+
pub fn is_saturated(&self) -> bool {
66+
self.0 == i64::MAX
67+
}
68+
69+
#[inline]
70+
pub fn round_up(&self) -> i64 {
71+
milligas_to_gas(self.0, true)
72+
}
73+
74+
#[inline]
75+
pub fn round_down(&self) -> i64 {
76+
milligas_to_gas(self.0, false)
77+
}
78+
79+
#[inline]
80+
pub fn as_milligas(&self) -> i64 {
81+
self.0
82+
}
83+
}
84+
85+
impl num_traits::Zero for Gas {
86+
fn zero() -> Self {
87+
return Gas::default();
88+
}
89+
90+
fn is_zero(&self) -> bool {
91+
self.0 == 0
92+
}
93+
}
94+
95+
impl Add for Gas {
96+
type Output = Gas;
97+
98+
#[inline]
99+
fn add(self, rhs: Self) -> Self::Output {
100+
Self(self.0.saturating_add(rhs.0))
101+
}
102+
}
103+
104+
impl AddAssign for Gas {
105+
#[inline]
106+
fn add_assign(&mut self, rhs: Self) {
107+
self.0 = self.0.saturating_add(rhs.0)
108+
}
109+
}
110+
111+
impl SubAssign for Gas {
112+
#[inline]
113+
fn sub_assign(&mut self, rhs: Self) {
114+
self.0 = self.0.saturating_sub(rhs.0)
115+
}
116+
}
117+
118+
impl Sub for Gas {
119+
type Output = Gas;
120+
121+
#[inline]
122+
fn sub(self, rhs: Self) -> Self::Output {
123+
Self(self.0.saturating_sub(rhs.0))
124+
}
125+
}
126+
127+
impl Mul<i64> for Gas {
128+
type Output = Gas;
129+
130+
#[inline]
131+
fn mul(self, rhs: i64) -> Self::Output {
132+
Self(self.0.saturating_mul(rhs))
133+
}
134+
}
135+
136+
impl Mul<i32> for Gas {
137+
type Output = Gas;
138+
139+
#[inline]
140+
fn mul(self, rhs: i32) -> Self::Output {
141+
Self(self.0.saturating_mul(rhs.into()))
142+
}
143+
}
18144

19145
pub struct GasTracker {
20-
milligas_limit: i64,
21-
milligas_used: i64,
146+
gas_limit: Gas,
147+
gas_used: Gas,
22148
}
23149

24150
impl GasTracker {
25151
/// Gas limit and gas used are provided in protocol units (i.e. full units).
26152
/// They are converted to milligas for internal canonical accounting.
27153
pub fn new(gas_limit: Gas, gas_used: Gas) -> Self {
28154
Self {
29-
milligas_limit: gas_to_milligas(gas_limit),
30-
milligas_used: gas_to_milligas(gas_used),
155+
gas_limit,
156+
gas_used,
31157
}
32158
}
33159

34160
/// Safely consumes gas and returns an out of gas error if there is not sufficient
35161
/// enough gas remaining for charge.
36-
pub fn charge_milligas(&mut self, name: &str, to_use: Milligas) -> Result<()> {
37-
match self.milligas_used.checked_add(to_use) {
38-
None => {
39-
log::trace!("gas overflow: {}", name);
40-
self.milligas_used = self.milligas_limit;
41-
Err(ExecutionError::OutOfGas)
42-
}
43-
Some(used) => {
44-
log::trace!("charged {} gas: {}", to_use, name);
45-
if used > self.milligas_limit {
46-
log::trace!("out of gas: {}", name);
47-
self.milligas_used = self.milligas_limit;
48-
Err(ExecutionError::OutOfGas)
49-
} else {
50-
self.milligas_used = used;
51-
Ok(())
52-
}
53-
}
162+
pub fn charge_gas(&mut self, name: &str, to_use: Gas) -> Result<()> {
163+
log::trace!("charging gas: {} {}", name, to_use);
164+
// The gas type uses saturating math.
165+
self.gas_used += to_use;
166+
if self.gas_used > self.gas_limit {
167+
log::trace!("gas limit reached");
168+
self.gas_used = self.gas_limit;
169+
Err(ExecutionError::OutOfGas)
170+
} else {
171+
Ok(())
54172
}
55173
}
56174

57175
/// Applies the specified gas charge, where quantities are supplied in milligas.
58176
pub fn apply_charge(&mut self, charge: GasCharge) -> Result<()> {
59-
self.charge_milligas(charge.name, charge.total())
177+
self.charge_gas(charge.name, charge.total())
60178
}
61179

62-
/// Getter for gas available.
180+
/// Getter for the maximum gas usable by this message.
63181
pub fn gas_limit(&self) -> Gas {
64-
milligas_to_gas(self.milligas_limit, false)
65-
}
66-
67-
/// Getter for milligas available.
68-
pub fn milligas_limit(&self) -> Milligas {
69-
self.milligas_limit
182+
self.gas_limit
70183
}
71184

72185
/// Getter for gas used.
73186
pub fn gas_used(&self) -> Gas {
74-
milligas_to_gas(self.milligas_used, true)
75-
}
76-
77-
/// Getter for milligas used.
78-
pub fn milligas_used(&self) -> Milligas {
79-
self.milligas_used
187+
self.gas_used
80188
}
81189

190+
/// Getter for gas available.
82191
pub fn gas_available(&self) -> Gas {
83-
milligas_to_gas(self.milligas_available(), false)
84-
}
85-
86-
pub fn milligas_available(&self) -> Milligas {
87-
self.milligas_limit.saturating_sub(self.milligas_used)
192+
self.gas_limit - self.gas_used
88193
}
89194
}
90195

91-
/// Converts the specified gas into equivalent fractional gas units
92-
#[inline]
93-
pub(crate) fn gas_to_milligas(gas: i64) -> i64 {
94-
gas.saturating_mul(MILLIGAS_PRECISION)
95-
}
96-
97196
/// Converts the specified fractional gas units into gas units
98197
#[inline]
99198
pub(crate) fn milligas_to_gas(milligas: i64, round_up: bool) -> i64 {
@@ -108,18 +207,20 @@ pub(crate) fn milligas_to_gas(milligas: i64, round_up: bool) -> i64 {
108207

109208
#[cfg(test)]
110209
mod tests {
210+
use num_traits::Zero;
211+
111212
use super::*;
112213

113214
#[test]
114215
#[allow(clippy::identity_op)]
115216
fn basic_gas_tracker() -> Result<()> {
116-
let mut t = GasTracker::new(20, 10);
117-
t.apply_charge(GasCharge::new("", 5 * MILLIGAS_PRECISION, 0))?;
118-
assert_eq!(t.gas_used(), 15);
119-
t.apply_charge(GasCharge::new("", 5 * MILLIGAS_PRECISION, 0))?;
120-
assert_eq!(t.gas_used(), 20);
217+
let mut t = GasTracker::new(Gas::new(20), Gas::new(10));
218+
t.apply_charge(GasCharge::new("", Gas::new(5), Gas::zero()))?;
219+
assert_eq!(t.gas_used(), Gas::new(15));
220+
t.apply_charge(GasCharge::new("", Gas::new(5), Gas::zero()))?;
221+
assert_eq!(t.gas_used(), Gas::new(20));
121222
assert!(t
122-
.apply_charge(GasCharge::new("", 1 * MILLIGAS_PRECISION, 0))
223+
.apply_charge(GasCharge::new("", Gas::new(1), Gas::zero()))
123224
.is_err());
124225
Ok(())
125226
}

fvm/src/gas/outputs.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ use std::convert::TryFrom;
33
use fvm_shared::bigint::BigInt;
44
use fvm_shared::econ::TokenAmount;
55

6-
use crate::gas::Gas;
7-
86
#[derive(Clone, Default)]
97
pub(crate) struct GasOutputs {
108
pub base_fee_burn: TokenAmount,
@@ -13,14 +11,16 @@ pub(crate) struct GasOutputs {
1311
pub miner_tip: TokenAmount,
1412
pub refund: TokenAmount,
1513

16-
pub gas_refund: Gas,
17-
pub gas_burned: Gas,
14+
// In whole gas units.
15+
pub gas_refund: i64,
16+
pub gas_burned: i64,
1817
}
1918

2019
impl GasOutputs {
2120
pub fn compute(
22-
gas_used: Gas,
23-
gas_limit: Gas,
21+
// In whole gas units.
22+
gas_used: i64,
23+
gas_limit: i64,
2424
base_fee: &TokenAmount,
2525
fee_cap: &TokenAmount,
2626
gas_premium: &TokenAmount,
@@ -59,7 +59,7 @@ impl GasOutputs {
5959
}
6060
}
6161

62-
fn compute_gas_overestimation_burn(gas_used: Gas, gas_limit: Gas) -> (Gas, Gas) {
62+
fn compute_gas_overestimation_burn(gas_used: i64, gas_limit: i64) -> (i64, i64) {
6363
const GAS_OVERUSE_NUM: i64 = 11;
6464
const GAS_OVERUSE_DENOM: i64 = 10;
6565

0 commit comments

Comments
 (0)