Skip to content

Commit

Permalink
Implement worst case scenario for price algorithm v1 (#2219)
Browse files Browse the repository at this point in the history
## 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 <james.mitchell.turner@gmail.com>
  • Loading branch information
rafal-ch and MitchTurner authored Sep 20, 2024
1 parent 79ca0d0 commit 43c1676
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 27 deletions.
1 change: 1 addition & 0 deletions crates/fuel-gas-price-algorithm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
#![deny(clippy::cast_possible_truncation)]
#![deny(warnings)]

mod utils;
pub mod v0;
pub mod v1;
20 changes: 20 additions & 0 deletions crates/fuel-gas-price-algorithm/src/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#[allow(clippy::cast_possible_truncation)]
pub(crate) fn cumulative_percentage_change(
new_exec_price: u64,
for_height: u32,
percentage: u64,
height: u32,
) -> u64 {
let blocks = height.saturating_sub(for_height) as f64;
let percentage_as_decimal = percentage as f64 / 100.0;
let multiple = (1.0f64 + percentage_as_decimal).powf(blocks);
let mut approx = new_exec_price as f64 * multiple;
// account for rounding errors and take a slightly higher value
const ROUNDING_ERROR_CUTOFF: f64 = 16948547188989277.0;
if approx > ROUNDING_ERROR_CUTOFF {
const ROUNDING_ERROR_COMPENSATION: f64 = 2000.0;
approx += ROUNDING_ERROR_COMPENSATION;
}
// `f64` over `u64::MAX` are cast to `u64::MAX`
approx.ceil() as u64
}
23 changes: 8 additions & 15 deletions crates/fuel-gas-price-algorithm/src/v0.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use std::{
num::NonZeroU64,
};

use crate::utils::cumulative_percentage_change;

#[cfg(test)]
mod tests;

Expand All @@ -27,22 +29,13 @@ impl AlgorithmV0 {
self.new_exec_price
}

#[allow(clippy::cast_possible_truncation)]
pub fn worst_case(&self, height: u32) -> u64 {
let price = self.new_exec_price as f64;
let blocks = height.saturating_sub(self.for_height) as f64;
let percentage = self.percentage as f64;
let percentage_as_decimal = percentage / 100.0;
let multiple = (1.0f64 + percentage_as_decimal).powf(blocks);
let mut approx = price * multiple;
// account for rounding errors and take a slightly higher value
const ARB_CUTOFF: f64 = 16948547188989277.0;
if approx > ARB_CUTOFF {
const ARB_ADDITION: f64 = 2000.0;
approx += ARB_ADDITION;
}
// `f64` over `u64::MAX` are cast to `u64::MAX`
approx.ceil() as u64
cumulative_percentage_change(
self.new_exec_price,
self.for_height,
self.percentage,
height,
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ proptest! {
}

#[test]
fn worst_case__same_block_gives_new_exec_price() {
fn worst_case__same_block_gives_the_same_value_as_calculate() {
// given
let new_exec_price = 1000;
let for_height = 10;
Expand All @@ -100,6 +100,6 @@ fn worst_case__same_block_gives_new_exec_price() {
let actual = algorithm.worst_case(target_height);

// then
let expected = new_exec_price;
let expected = algorithm.calculate();
assert_eq!(expected, actual);
}
43 changes: 36 additions & 7 deletions crates/fuel-gas-price-algorithm/src/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ use std::{
ops::Div,
};

use crate::utils::cumulative_percentage_change;

#[cfg(test)]
mod tests;

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

#[derive(Debug, Clone, PartialEq)]
pub struct AlgorithmV1 {
/// the combined Execution and DA gas prices
combined_gas_price: u64,
/// The gas price for to cover the execution of the next block
new_exec_price: u64,
/// The change percentage per block
exec_price_percentage: u64,
/// The gas price for to cover DA commitment
new_da_gas_price: u64,
/// The change percentage per block
da_gas_price_percentage: u64,
/// The block height of the next L2 block
for_height: u32,
}

impl AlgorithmV1 {
pub fn calculate(&self) -> u64 {
self.combined_gas_price
self.new_exec_price.saturating_add(self.new_da_gas_price)
}

pub fn worst_case(&self, height: u32) -> u64 {
let exec = cumulative_percentage_change(
self.new_exec_price,
self.for_height,
self.exec_price_percentage,
height,
);
let da = cumulative_percentage_change(
self.new_da_gas_price,
self.for_height,
self.da_gas_price_percentage,
height,
);
exec.saturating_add(da)
}
}

Expand Down Expand Up @@ -370,10 +396,13 @@ impl AlgorithmUpdaterV1 {
}

pub fn algorithm(&self) -> AlgorithmV1 {
let combined_gas_price = self
.descaled_exec_price()
.saturating_add(self.descaled_da_price());
AlgorithmV1 { combined_gas_price }
AlgorithmV1 {
new_exec_price: self.descaled_exec_price(),
exec_price_percentage: self.exec_gas_price_change_percent as u64,
new_da_gas_price: self.descaled_da_price(),
da_gas_price_percentage: self.max_da_gas_price_change_percent as u64,
for_height: self.l2_block_height,
}
}

// We only need to track the difference between the rewards and costs after we have true DA data
Expand Down
117 changes: 116 additions & 1 deletion crates/fuel-gas-price-algorithm/src/v1/tests/algorithm_v1_tests.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use crate::v1::tests::UpdaterBuilder;
use crate::v1::{
tests::UpdaterBuilder,
AlgorithmV1,
};
use proptest::prelude::*;

#[test]
fn calculate__returns_sum_of_da_and_exec_gas_prices() {
Expand Down Expand Up @@ -28,3 +32,114 @@ fn calculate__returns_sum_of_da_and_exec_gas_prices() {
let expected = starting_exec_gas_price + starting_da_gas_price;
assert_eq!(expected, actual);
}

fn _worst_case__correctly_calculates_value(
new_exec_price: u64,
new_da_gas_price: u64,
for_height: u32,
block_horizon: u32,
exec_price_percentage: u64,
da_gas_price_percentage: u64,
) {
// given
let algorithm = AlgorithmV1 {
new_exec_price,
exec_price_percentage,
new_da_gas_price,
da_gas_price_percentage,
for_height,
};

// when
let target_height = for_height.saturating_add(block_horizon);
let actual = algorithm.worst_case(target_height);

// then
let mut expected_exec_price = new_exec_price;
let mut expected_da_gas_price = new_da_gas_price;

for _ in 0..block_horizon {
let change_amount = expected_exec_price
.saturating_mul(exec_price_percentage)
.saturating_div(100);
expected_exec_price = expected_exec_price.saturating_add(change_amount);

let change_amount = expected_da_gas_price
.saturating_mul(da_gas_price_percentage)
.saturating_div(100);
expected_da_gas_price = expected_da_gas_price.saturating_add(change_amount);
}

let expected = expected_exec_price.saturating_add(expected_da_gas_price);

dbg!(actual, expected);
assert!(actual >= expected);
}

proptest! {
#[test]
fn worst_case__correctly_calculates_value(
exec_price: u64,
da_price: u64,
starting_height: u32,
block_horizon in 0..10_000u32,
exec_percentage: u64,
da_percentage: u64,
) {
_worst_case__correctly_calculates_value(exec_price, da_price, starting_height, block_horizon, exec_percentage, da_percentage);
}
}

proptest! {
#[test]
fn worst_case__never_overflows(
exec_price: u64,
da_price: u64,
starting_height: u32,
block_horizon in 0..10_000u32,
exec_percentage: u64,
da_percentage: u64,
) {
// given
let algorithm = AlgorithmV1 {
new_exec_price: exec_price,
exec_price_percentage: exec_percentage,
new_da_gas_price: da_price,
da_gas_price_percentage: da_percentage,
for_height: starting_height,
};

// when
let target_height = starting_height.saturating_add(block_horizon);
let _ = algorithm.worst_case(target_height);

// then
// doesn't panic with an overflow
}
}

#[test]
fn worst_case__same_block_gives_the_same_value_as_calculate() {
// given
let new_exec_price = 1000;
let exec_price_percentage = 10;
let new_da_gas_price = 1000;
let da_gas_price_percentage = 10;
let for_height = 10;
let algorithm = AlgorithmV1 {
new_exec_price,
exec_price_percentage,
new_da_gas_price,
da_gas_price_percentage,
for_height,
};

// when
let delta = 0;
let target_height = for_height + delta;
let actual = algorithm.worst_case(target_height);

// then
let expected = algorithm.calculate();
assert_eq!(expected, actual);
}
Original file line number Diff line number Diff line change
Expand Up @@ -350,8 +350,7 @@ impl GasPriceAlgorithm for Algorithm {
fn worst_case_gas_price(&self, height: BlockHeight) -> u64 {
match self {
Algorithm::V0(v0) => v0.worst_case(height.into()),
// TODO: Add worst_case calculation https://github.com/FuelLabs/fuel-core/issues/2203
Algorithm::V1(v1) => v1.calculate(),
Algorithm::V1(v1) => v1.worst_case(height.into()),
}
}
}

0 comments on commit 43c1676

Please sign in to comment.