Skip to content

Commit 43c1676

Browse files
Implement worst case scenario for price algorithm v1 (#2219)
## Linked Issues/PRs Closes #2203 ## Description This PR adds the `worst_case()` calculation to the `V1` price algorithm. ## Checklist - [X] New behavior is reflected in tests ### Before requesting review - [X] I have reviewed the code myself --------- Co-authored-by: Mitchell Turner <[email protected]>
1 parent 79ca0d0 commit 43c1676

File tree

7 files changed

+184
-27
lines changed

7 files changed

+184
-27
lines changed

crates/fuel-gas-price-algorithm/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
#![deny(clippy::cast_possible_truncation)]
33
#![deny(warnings)]
44

5+
mod utils;
56
pub mod v0;
67
pub mod v1;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#[allow(clippy::cast_possible_truncation)]
2+
pub(crate) fn cumulative_percentage_change(
3+
new_exec_price: u64,
4+
for_height: u32,
5+
percentage: u64,
6+
height: u32,
7+
) -> u64 {
8+
let blocks = height.saturating_sub(for_height) as f64;
9+
let percentage_as_decimal = percentage as f64 / 100.0;
10+
let multiple = (1.0f64 + percentage_as_decimal).powf(blocks);
11+
let mut approx = new_exec_price as f64 * multiple;
12+
// account for rounding errors and take a slightly higher value
13+
const ROUNDING_ERROR_CUTOFF: f64 = 16948547188989277.0;
14+
if approx > ROUNDING_ERROR_CUTOFF {
15+
const ROUNDING_ERROR_COMPENSATION: f64 = 2000.0;
16+
approx += ROUNDING_ERROR_COMPENSATION;
17+
}
18+
// `f64` over `u64::MAX` are cast to `u64::MAX`
19+
approx.ceil() as u64
20+
}

crates/fuel-gas-price-algorithm/src/v0.rs

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ use std::{
33
num::NonZeroU64,
44
};
55

6+
use crate::utils::cumulative_percentage_change;
7+
68
#[cfg(test)]
79
mod tests;
810

@@ -27,22 +29,13 @@ impl AlgorithmV0 {
2729
self.new_exec_price
2830
}
2931

30-
#[allow(clippy::cast_possible_truncation)]
3132
pub fn worst_case(&self, height: u32) -> u64 {
32-
let price = self.new_exec_price as f64;
33-
let blocks = height.saturating_sub(self.for_height) as f64;
34-
let percentage = self.percentage as f64;
35-
let percentage_as_decimal = percentage / 100.0;
36-
let multiple = (1.0f64 + percentage_as_decimal).powf(blocks);
37-
let mut approx = price * multiple;
38-
// account for rounding errors and take a slightly higher value
39-
const ARB_CUTOFF: f64 = 16948547188989277.0;
40-
if approx > ARB_CUTOFF {
41-
const ARB_ADDITION: f64 = 2000.0;
42-
approx += ARB_ADDITION;
43-
}
44-
// `f64` over `u64::MAX` are cast to `u64::MAX`
45-
approx.ceil() as u64
33+
cumulative_percentage_change(
34+
self.new_exec_price,
35+
self.for_height,
36+
self.percentage,
37+
height,
38+
)
4639
}
4740
}
4841

crates/fuel-gas-price-algorithm/src/v0/tests/algorithm_v0_tests.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ proptest! {
8383
}
8484

8585
#[test]
86-
fn worst_case__same_block_gives_new_exec_price() {
86+
fn worst_case__same_block_gives_the_same_value_as_calculate() {
8787
// given
8888
let new_exec_price = 1000;
8989
let for_height = 10;
@@ -100,6 +100,6 @@ fn worst_case__same_block_gives_new_exec_price() {
100100
let actual = algorithm.worst_case(target_height);
101101

102102
// then
103-
let expected = new_exec_price;
103+
let expected = algorithm.calculate();
104104
assert_eq!(expected, actual);
105105
}

crates/fuel-gas-price-algorithm/src/v1.rs

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use std::{
44
ops::Div,
55
};
66

7+
use crate::utils::cumulative_percentage_change;
8+
79
#[cfg(test)]
810
mod tests;
911

@@ -21,13 +23,37 @@ pub enum Error {
2123

2224
#[derive(Debug, Clone, PartialEq)]
2325
pub struct AlgorithmV1 {
24-
/// the combined Execution and DA gas prices
25-
combined_gas_price: u64,
26+
/// The gas price for to cover the execution of the next block
27+
new_exec_price: u64,
28+
/// The change percentage per block
29+
exec_price_percentage: u64,
30+
/// The gas price for to cover DA commitment
31+
new_da_gas_price: u64,
32+
/// The change percentage per block
33+
da_gas_price_percentage: u64,
34+
/// The block height of the next L2 block
35+
for_height: u32,
2636
}
2737

2838
impl AlgorithmV1 {
2939
pub fn calculate(&self) -> u64 {
30-
self.combined_gas_price
40+
self.new_exec_price.saturating_add(self.new_da_gas_price)
41+
}
42+
43+
pub fn worst_case(&self, height: u32) -> u64 {
44+
let exec = cumulative_percentage_change(
45+
self.new_exec_price,
46+
self.for_height,
47+
self.exec_price_percentage,
48+
height,
49+
);
50+
let da = cumulative_percentage_change(
51+
self.new_da_gas_price,
52+
self.for_height,
53+
self.da_gas_price_percentage,
54+
height,
55+
);
56+
exec.saturating_add(da)
3157
}
3258
}
3359

@@ -370,10 +396,13 @@ impl AlgorithmUpdaterV1 {
370396
}
371397

372398
pub fn algorithm(&self) -> AlgorithmV1 {
373-
let combined_gas_price = self
374-
.descaled_exec_price()
375-
.saturating_add(self.descaled_da_price());
376-
AlgorithmV1 { combined_gas_price }
399+
AlgorithmV1 {
400+
new_exec_price: self.descaled_exec_price(),
401+
exec_price_percentage: self.exec_gas_price_change_percent as u64,
402+
new_da_gas_price: self.descaled_da_price(),
403+
da_gas_price_percentage: self.max_da_gas_price_change_percent as u64,
404+
for_height: self.l2_block_height,
405+
}
377406
}
378407

379408
// We only need to track the difference between the rewards and costs after we have true DA data

crates/fuel-gas-price-algorithm/src/v1/tests/algorithm_v1_tests.rs

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
use crate::v1::tests::UpdaterBuilder;
1+
use crate::v1::{
2+
tests::UpdaterBuilder,
3+
AlgorithmV1,
4+
};
5+
use proptest::prelude::*;
26

37
#[test]
48
fn calculate__returns_sum_of_da_and_exec_gas_prices() {
@@ -28,3 +32,114 @@ fn calculate__returns_sum_of_da_and_exec_gas_prices() {
2832
let expected = starting_exec_gas_price + starting_da_gas_price;
2933
assert_eq!(expected, actual);
3034
}
35+
36+
fn _worst_case__correctly_calculates_value(
37+
new_exec_price: u64,
38+
new_da_gas_price: u64,
39+
for_height: u32,
40+
block_horizon: u32,
41+
exec_price_percentage: u64,
42+
da_gas_price_percentage: u64,
43+
) {
44+
// given
45+
let algorithm = AlgorithmV1 {
46+
new_exec_price,
47+
exec_price_percentage,
48+
new_da_gas_price,
49+
da_gas_price_percentage,
50+
for_height,
51+
};
52+
53+
// when
54+
let target_height = for_height.saturating_add(block_horizon);
55+
let actual = algorithm.worst_case(target_height);
56+
57+
// then
58+
let mut expected_exec_price = new_exec_price;
59+
let mut expected_da_gas_price = new_da_gas_price;
60+
61+
for _ in 0..block_horizon {
62+
let change_amount = expected_exec_price
63+
.saturating_mul(exec_price_percentage)
64+
.saturating_div(100);
65+
expected_exec_price = expected_exec_price.saturating_add(change_amount);
66+
67+
let change_amount = expected_da_gas_price
68+
.saturating_mul(da_gas_price_percentage)
69+
.saturating_div(100);
70+
expected_da_gas_price = expected_da_gas_price.saturating_add(change_amount);
71+
}
72+
73+
let expected = expected_exec_price.saturating_add(expected_da_gas_price);
74+
75+
dbg!(actual, expected);
76+
assert!(actual >= expected);
77+
}
78+
79+
proptest! {
80+
#[test]
81+
fn worst_case__correctly_calculates_value(
82+
exec_price: u64,
83+
da_price: u64,
84+
starting_height: u32,
85+
block_horizon in 0..10_000u32,
86+
exec_percentage: u64,
87+
da_percentage: u64,
88+
) {
89+
_worst_case__correctly_calculates_value(exec_price, da_price, starting_height, block_horizon, exec_percentage, da_percentage);
90+
}
91+
}
92+
93+
proptest! {
94+
#[test]
95+
fn worst_case__never_overflows(
96+
exec_price: u64,
97+
da_price: u64,
98+
starting_height: u32,
99+
block_horizon in 0..10_000u32,
100+
exec_percentage: u64,
101+
da_percentage: u64,
102+
) {
103+
// given
104+
let algorithm = AlgorithmV1 {
105+
new_exec_price: exec_price,
106+
exec_price_percentage: exec_percentage,
107+
new_da_gas_price: da_price,
108+
da_gas_price_percentage: da_percentage,
109+
for_height: starting_height,
110+
};
111+
112+
// when
113+
let target_height = starting_height.saturating_add(block_horizon);
114+
let _ = algorithm.worst_case(target_height);
115+
116+
// then
117+
// doesn't panic with an overflow
118+
}
119+
}
120+
121+
#[test]
122+
fn worst_case__same_block_gives_the_same_value_as_calculate() {
123+
// given
124+
let new_exec_price = 1000;
125+
let exec_price_percentage = 10;
126+
let new_da_gas_price = 1000;
127+
let da_gas_price_percentage = 10;
128+
let for_height = 10;
129+
let algorithm = AlgorithmV1 {
130+
new_exec_price,
131+
exec_price_percentage,
132+
new_da_gas_price,
133+
da_gas_price_percentage,
134+
for_height,
135+
};
136+
137+
// when
138+
let delta = 0;
139+
let target_height = for_height + delta;
140+
let actual = algorithm.worst_case(target_height);
141+
142+
// then
143+
let expected = algorithm.calculate();
144+
assert_eq!(expected, actual);
145+
}

crates/services/gas_price_service/src/fuel_gas_price_updater.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -350,8 +350,7 @@ impl GasPriceAlgorithm for Algorithm {
350350
fn worst_case_gas_price(&self, height: BlockHeight) -> u64 {
351351
match self {
352352
Algorithm::V0(v0) => v0.worst_case(height.into()),
353-
// TODO: Add worst_case calculation https://github.com/FuelLabs/fuel-core/issues/2203
354-
Algorithm::V1(v1) => v1.calculate(),
353+
Algorithm::V1(v1) => v1.worst_case(height.into()),
355354
}
356355
}
357356
}

0 commit comments

Comments
 (0)