Skip to content

Commit 5d3a7b1

Browse files
authored
Merge pull request #562 from filecoin-project/fix/price-list-snafu
Strongly typed gas units, and fix two related bugs
2 parents f930a1d + 5baa787 commit 5d3a7b1

File tree

12 files changed

+406
-329
lines changed

12 files changed

+406
-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: 163 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,190 @@ 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+
/// Construct a `Gas` from milligas.
55+
#[inline]
56+
pub fn from_milligas(milligas: i64) -> Gas {
57+
Gas(milligas)
58+
}
59+
60+
/// Construct a `Gas` from gas, scaling up. If this exceeds the width of an i64, it saturates at
61+
/// `i64::MAX` milligas.
62+
#[inline]
63+
pub fn new(gas: i64) -> Gas {
64+
Gas(gas.saturating_mul(MILLIGAS_PRECISION))
65+
}
66+
67+
#[inline]
68+
pub fn is_saturated(&self) -> bool {
69+
self.0 == i64::MAX
70+
}
71+
72+
/// Returns the gas value as an integer, rounding the fractional part up.
73+
#[inline]
74+
pub fn round_up(&self) -> i64 {
75+
milligas_to_gas(self.0, true)
76+
}
77+
78+
/// Returns the gas value as an integer, truncating the fractional part.
79+
#[inline]
80+
pub fn round_down(&self) -> i64 {
81+
milligas_to_gas(self.0, false)
82+
}
83+
84+
/// Returns the gas value as milligas, without loss of precision.
85+
#[inline]
86+
pub fn as_milligas(&self) -> i64 {
87+
self.0
88+
}
89+
}
90+
91+
impl num_traits::Zero for Gas {
92+
fn zero() -> Self {
93+
Gas(0)
94+
}
95+
96+
fn is_zero(&self) -> bool {
97+
self.0 == 0
98+
}
99+
}
100+
101+
impl Add for Gas {
102+
type Output = Gas;
103+
104+
#[inline]
105+
fn add(self, rhs: Self) -> Self::Output {
106+
Self(self.0.saturating_add(rhs.0))
107+
}
108+
}
109+
110+
impl AddAssign for Gas {
111+
#[inline]
112+
fn add_assign(&mut self, rhs: Self) {
113+
self.0 = self.0.saturating_add(rhs.0)
114+
}
115+
}
116+
117+
impl SubAssign for Gas {
118+
#[inline]
119+
fn sub_assign(&mut self, rhs: Self) {
120+
self.0 = self.0.saturating_sub(rhs.0)
121+
}
122+
}
123+
124+
impl Sub for Gas {
125+
type Output = Gas;
126+
127+
#[inline]
128+
fn sub(self, rhs: Self) -> Self::Output {
129+
Self(self.0.saturating_sub(rhs.0))
130+
}
131+
}
132+
133+
impl Mul<i64> for Gas {
134+
type Output = Gas;
135+
136+
#[inline]
137+
fn mul(self, rhs: i64) -> Self::Output {
138+
Self(self.0.saturating_mul(rhs))
139+
}
140+
}
141+
142+
impl Mul<i32> for Gas {
143+
type Output = Gas;
144+
145+
#[inline]
146+
fn mul(self, rhs: i32) -> Self::Output {
147+
Self(self.0.saturating_mul(rhs.into()))
148+
}
149+
}
18150

19151
pub struct GasTracker {
20-
milligas_limit: i64,
21-
milligas_used: i64,
152+
gas_limit: Gas,
153+
gas_used: Gas,
22154
}
23155

24156
impl GasTracker {
25157
/// Gas limit and gas used are provided in protocol units (i.e. full units).
26158
/// They are converted to milligas for internal canonical accounting.
27159
pub fn new(gas_limit: Gas, gas_used: Gas) -> Self {
28160
Self {
29-
milligas_limit: gas_to_milligas(gas_limit),
30-
milligas_used: gas_to_milligas(gas_used),
161+
gas_limit,
162+
gas_used,
31163
}
32164
}
33165

34166
/// Safely consumes gas and returns an out of gas error if there is not sufficient
35167
/// 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-
}
168+
pub fn charge_gas(&mut self, name: &str, to_use: Gas) -> Result<()> {
169+
log::trace!("charging gas: {} {}", name, to_use);
170+
// The gas type uses saturating math.
171+
self.gas_used += to_use;
172+
if self.gas_used > self.gas_limit {
173+
log::trace!("gas limit reached");
174+
self.gas_used = self.gas_limit;
175+
Err(ExecutionError::OutOfGas)
176+
} else {
177+
Ok(())
54178
}
55179
}
56180

57181
/// Applies the specified gas charge, where quantities are supplied in milligas.
58182
pub fn apply_charge(&mut self, charge: GasCharge) -> Result<()> {
59-
self.charge_milligas(charge.name, charge.total())
183+
self.charge_gas(charge.name, charge.total())
60184
}
61185

62-
/// Getter for gas available.
186+
/// Getter for the maximum gas usable by this message.
63187
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
188+
self.gas_limit
70189
}
71190

72191
/// Getter for gas used.
73192
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
193+
self.gas_used
80194
}
81195

196+
/// Getter for gas available.
82197
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)
198+
self.gas_limit - self.gas_used
88199
}
89200
}
90201

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-
97202
/// Converts the specified fractional gas units into gas units
98203
#[inline]
99204
pub(crate) fn milligas_to_gas(milligas: i64, round_up: bool) -> i64 {
@@ -108,18 +213,20 @@ pub(crate) fn milligas_to_gas(milligas: i64, round_up: bool) -> i64 {
108213

109214
#[cfg(test)]
110215
mod tests {
216+
use num_traits::Zero;
217+
111218
use super::*;
112219

113220
#[test]
114221
#[allow(clippy::identity_op)]
115222
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);
223+
let mut t = GasTracker::new(Gas::new(20), Gas::new(10));
224+
t.apply_charge(GasCharge::new("", Gas::new(5), Gas::zero()))?;
225+
assert_eq!(t.gas_used(), Gas::new(15));
226+
t.apply_charge(GasCharge::new("", Gas::new(5), Gas::zero()))?;
227+
assert_eq!(t.gas_used(), Gas::new(20));
121228
assert!(t
122-
.apply_charge(GasCharge::new("", 1 * MILLIGAS_PRECISION, 0))
229+
.apply_charge(GasCharge::new("", Gas::new(1), Gas::zero()))
123230
.is_err());
124231
Ok(())
125232
}

0 commit comments

Comments
 (0)