fuel_gas_price_algorithm/
v0.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
use std::{
    cmp::max,
    num::NonZeroU64,
};

use crate::utils::cumulative_percentage_change;

#[cfg(test)]
mod tests;

#[derive(Debug, thiserror::Error, PartialEq)]
pub enum Error {
    #[error("Skipped L2 block update: expected {expected:?}, got {got:?}")]
    SkippedL2Block { expected: u32, got: u32 },
}

#[derive(Debug, Clone, PartialEq)]
pub struct AlgorithmV0 {
    /// The gas price for to cover the execution of the next block
    new_exec_price: u64,
    /// The block height of the next L2 block
    for_height: u32,
    /// The change percentage per block
    percentage: u64,
}

impl AlgorithmV0 {
    pub fn calculate(&self) -> u64 {
        self.new_exec_price
    }

    pub fn worst_case(&self, height: u32) -> u64 {
        cumulative_percentage_change(
            self.new_exec_price,
            self.for_height,
            self.percentage,
            height,
        )
    }
}

/// The state of the algorithm used to update the gas price algorithm for each block
///
/// Because there will always be a delay between blocks submitted to the L2 chain and the blocks
/// being recorded on the DA chain, the updater needs to make "projections" about the cost of
/// recording any given block to the DA chain. This is done by tracking the cost per byte of recording
/// for the most recent blocks, and using the known bytes of the unrecorded blocks to estimate
/// the cost for that block. Every time the DA recording is updated, the projections are recalculated.
///
/// This projection will inevitably lead to error in the gas price calculation. Special care should be taken
/// to account for the worst case scenario when calculating the parameters of the algorithm.
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
pub struct AlgorithmUpdaterV0 {
    /// The gas price to cover the execution of the next block
    pub new_exec_price: u64,
    // Execution
    /// The lowest the algorithm allows the exec gas price to go
    pub min_exec_gas_price: u64,
    /// The Percentage the execution gas price will change in a single block, either increase or decrease
    /// based on the fullness of the last L2 block
    pub exec_gas_price_change_percent: u64,
    /// The height of the next L2 block
    pub l2_block_height: u32,
    /// The threshold of gas usage above and below which the gas price will increase or decrease
    /// This is a percentage of the total capacity of the L2 block
    pub l2_block_fullness_threshold_percent: u64,
}

impl AlgorithmUpdaterV0 {
    pub fn new(
        new_exec_price: u64,
        min_exec_gas_price: u64,
        exec_gas_price_change_percent: u64,
        l2_block_height: u32,
        l2_block_fullness_threshold_percent: u64,
    ) -> Self {
        Self {
            new_exec_price,
            min_exec_gas_price,
            exec_gas_price_change_percent,
            l2_block_height,
            l2_block_fullness_threshold_percent,
        }
    }

    pub fn update_l2_block_data(
        &mut self,
        height: u32,
        used: u64,
        capacity: NonZeroU64,
    ) -> Result<(), Error> {
        let expected = self.l2_block_height.saturating_add(1);
        if height != expected {
            Err(Error::SkippedL2Block {
                expected,
                got: height,
            })
        } else {
            self.l2_block_height = height;
            self.update_exec_gas_price(used, capacity);
            Ok(())
        }
    }

    fn update_exec_gas_price(&mut self, used: u64, capacity: NonZeroU64) {
        let mut exec_gas_price = self.new_exec_price;
        let fullness_percent = used
            .saturating_mul(100)
            .checked_div(capacity.into())
            .unwrap_or(self.l2_block_fullness_threshold_percent);

        match fullness_percent.cmp(&self.l2_block_fullness_threshold_percent) {
            std::cmp::Ordering::Greater | std::cmp::Ordering::Equal => {
                let change_amount = self.change_amount(exec_gas_price);
                exec_gas_price = exec_gas_price.saturating_add(change_amount);
            }
            std::cmp::Ordering::Less => {
                let change_amount = self.change_amount(exec_gas_price);
                exec_gas_price = exec_gas_price.saturating_sub(change_amount);
            }
        }
        self.new_exec_price = max(self.min_exec_gas_price, exec_gas_price);
    }

    fn change_amount(&self, principle: u64) -> u64 {
        principle
            .saturating_mul(self.exec_gas_price_change_percent)
            .saturating_div(100)
    }

    pub fn algorithm(&self) -> AlgorithmV0 {
        AlgorithmV0 {
            new_exec_price: self.new_exec_price,
            for_height: self.l2_block_height,
            percentage: self.exec_gas_price_change_percent,
        }
    }
}