fuel_gas_price_algorithm/
v0.rs

1use std::{
2    cmp::max,
3    num::NonZeroU64,
4};
5
6use crate::utils::cumulative_percentage_change;
7
8#[cfg(test)]
9mod tests;
10
11#[derive(Debug, thiserror::Error, PartialEq)]
12pub enum Error {
13    #[error("Skipped L2 block update: expected {expected:?}, got {got:?}")]
14    SkippedL2Block { expected: u32, got: u32 },
15}
16
17#[derive(Debug, Clone, PartialEq)]
18pub struct AlgorithmV0 {
19    /// The gas price for to cover the execution of the next block
20    new_exec_price: u64,
21    /// The block height of the next L2 block
22    for_height: u32,
23    /// The change percentage per block
24    percentage: u64,
25}
26
27impl AlgorithmV0 {
28    pub fn calculate(&self) -> u64 {
29        self.new_exec_price
30    }
31
32    pub fn worst_case(&self, height: u32) -> u64 {
33        cumulative_percentage_change(
34            self.new_exec_price,
35            self.for_height,
36            self.percentage,
37            height,
38        )
39    }
40}
41
42/// The state of the algorithm used to update the gas price algorithm for each block
43///
44/// Because there will always be a delay between blocks submitted to the L2 chain and the blocks
45/// being recorded on the DA chain, the updater needs to make "projections" about the cost of
46/// recording any given block to the DA chain. This is done by tracking the cost per byte of recording
47/// for the most recent blocks, and using the known bytes of the unrecorded blocks to estimate
48/// the cost for that block. Every time the DA recording is updated, the projections are recalculated.
49///
50/// This projection will inevitably lead to error in the gas price calculation. Special care should be taken
51/// to account for the worst case scenario when calculating the parameters of the algorithm.
52#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
53pub struct AlgorithmUpdaterV0 {
54    /// The gas price to cover the execution of the next block
55    pub new_exec_price: u64,
56    // Execution
57    /// The lowest the algorithm allows the exec gas price to go
58    pub min_exec_gas_price: u64,
59    /// The Percentage the execution gas price will change in a single block, either increase or decrease
60    /// based on the fullness of the last L2 block
61    pub exec_gas_price_change_percent: u64,
62    /// The height of the next L2 block
63    pub l2_block_height: u32,
64    /// The threshold of gas usage above and below which the gas price will increase or decrease
65    /// This is a percentage of the total capacity of the L2 block
66    pub l2_block_fullness_threshold_percent: u64,
67}
68
69impl AlgorithmUpdaterV0 {
70    pub fn new(
71        new_exec_price: u64,
72        min_exec_gas_price: u64,
73        exec_gas_price_change_percent: u64,
74        l2_block_height: u32,
75        l2_block_fullness_threshold_percent: u64,
76    ) -> Self {
77        let new_exec_price_corrected = max(new_exec_price, min_exec_gas_price);
78        Self {
79            new_exec_price: new_exec_price_corrected,
80            min_exec_gas_price,
81            exec_gas_price_change_percent,
82            l2_block_height,
83            l2_block_fullness_threshold_percent,
84        }
85    }
86
87    pub fn update_l2_block_data(
88        &mut self,
89        height: u32,
90        used: u64,
91        capacity: NonZeroU64,
92    ) -> Result<(), Error> {
93        let expected = self.l2_block_height.saturating_add(1);
94        if height != expected {
95            Err(Error::SkippedL2Block {
96                expected,
97                got: height,
98            })
99        } else {
100            self.l2_block_height = height;
101            self.update_exec_gas_price(used, capacity);
102            Ok(())
103        }
104    }
105
106    fn update_exec_gas_price(&mut self, used: u64, capacity: NonZeroU64) {
107        let mut exec_gas_price = self.new_exec_price;
108        let fullness_percent = used
109            .saturating_mul(100)
110            .checked_div(capacity.into())
111            .unwrap_or(self.l2_block_fullness_threshold_percent);
112
113        match fullness_percent.cmp(&self.l2_block_fullness_threshold_percent) {
114            std::cmp::Ordering::Greater | std::cmp::Ordering::Equal => {
115                let change_amount = self.change_amount(exec_gas_price);
116                exec_gas_price = exec_gas_price.saturating_add(change_amount);
117            }
118            std::cmp::Ordering::Less => {
119                let change_amount = self.change_amount(exec_gas_price);
120                exec_gas_price = exec_gas_price.saturating_sub(change_amount);
121            }
122        }
123        self.new_exec_price = max(self.min_exec_gas_price, exec_gas_price);
124    }
125
126    fn change_amount(&self, principle: u64) -> u64 {
127        principle
128            .saturating_mul(self.exec_gas_price_change_percent)
129            .saturating_div(100)
130    }
131
132    pub fn algorithm(&self) -> AlgorithmV0 {
133        AlgorithmV0 {
134            new_exec_price: self.new_exec_price,
135            for_height: self.l2_block_height,
136            percentage: self.exec_gas_price_change_percent,
137        }
138    }
139}