fuel_gas_price_algorithm/
v1.rs

1use crate::utils::cumulative_percentage_change;
2use std::{
3    cmp::{
4        max,
5        min,
6    },
7    collections::BTreeMap,
8    num::NonZeroU64,
9    ops::{
10        Div,
11        RangeInclusive,
12    },
13};
14
15#[cfg(test)]
16mod tests;
17
18#[derive(Debug, thiserror::Error, PartialEq)]
19pub enum Error {
20    #[error("Skipped L2 block update: expected {expected:?}, got {got:?}")]
21    SkippedL2Block { expected: u32, got: u32 },
22    #[error("Could not calculate cost per byte: {bytes:?} bytes, {cost:?} cost")]
23    CouldNotCalculateCostPerByte { bytes: u128, cost: u128 },
24    #[error("Failed to include L2 block data: {0}")]
25    FailedToIncludeL2BlockData(String),
26    #[error("L2 block expected but not found in unrecorded blocks: {height}")]
27    L2BlockExpectedNotFound { height: u32 },
28    #[error("Could not insert unrecorded block: {0}")]
29    CouldNotInsertUnrecordedBlock(String),
30    #[error("Could not remove unrecorded block: {0}")]
31    CouldNotRemoveUnrecordedBlock(String),
32}
33
34// TODO: separate exec gas price and DA gas price into newtypes for clarity
35//   https://github.com/FuelLabs/fuel-core/issues/2382
36#[derive(Debug, Clone, PartialEq)]
37pub struct AlgorithmV1 {
38    /// The gas price for to cover the execution of the next block
39    new_exec_price: u64,
40    /// The change percentage per block
41    exec_price_percentage: u64,
42    /// The gas price for to cover DA commitment
43    new_da_gas_price: u64,
44    /// The change percentage per block
45    da_gas_price_percentage: u64,
46    /// The block height of the next L2 block
47    for_height: u32,
48}
49
50impl AlgorithmV1 {
51    pub fn calculate(&self) -> u64 {
52        self.new_exec_price.saturating_add(self.new_da_gas_price)
53    }
54
55    pub fn worst_case(&self, height: u32) -> u64 {
56        let exec = cumulative_percentage_change(
57            self.new_exec_price,
58            self.for_height,
59            self.exec_price_percentage,
60            height,
61        );
62        let da = cumulative_percentage_change(
63            self.new_da_gas_price,
64            self.for_height,
65            self.da_gas_price_percentage,
66            height,
67        );
68        exec.saturating_add(da)
69    }
70}
71
72pub type Height = u32;
73pub type Bytes = u64;
74
75pub trait UnrecordedBlocks {
76    fn insert(&mut self, height: Height, bytes: Bytes) -> Result<(), String>;
77
78    fn remove(&mut self, height: &Height) -> Result<Option<Bytes>, String>;
79}
80
81impl UnrecordedBlocks for BTreeMap<Height, Bytes> {
82    fn insert(&mut self, height: Height, bytes: Bytes) -> Result<(), String> {
83        self.insert(height, bytes);
84        Ok(())
85    }
86
87    fn remove(&mut self, height: &Height) -> Result<Option<Bytes>, String> {
88        let value = self.remove(height);
89        Ok(value)
90    }
91}
92
93/// The state of the algorithm used to update the gas price algorithm for each block
94///
95/// Because there will always be a delay between blocks submitted to the L2 chain and the blocks
96/// being recorded on the DA chain, the updater needs to make "projections" about the cost of
97/// recording any given block to the DA chain. This is done by tracking the cost per byte of recording
98/// for the most recent blocks, and using the known bytes of the unrecorded blocks to estimate
99/// the cost for that block. Every time the DA recording is updated, the projections are recalculated.
100///
101/// This projection will inevitably lead to error in the gas price calculation. Special care should be taken
102/// to account for the worst case scenario when calculating the parameters of the algorithm.
103///
104/// An algorithm for calculating the gas price for the next block
105///
106/// The algorithm breaks up the gas price into two components:
107/// - The execution gas price, which is used to cover the cost of executing the next block as well
108///   as moderating the congestion of the network by increasing the price when traffic is high.
109/// - The data availability (DA) gas price, which is used to cover the cost of recording the block on the DA chain
110///
111/// The execution gas price is calculated based on the fullness of the last received l2 block. Each
112/// block has a capacity threshold, and if the block is above this threshold, the gas price is increased. If
113/// it is below the threshold, the gas price is decreased.
114/// The gas price can only change by a fixed amount each block.
115///
116/// The DA gas price is calculated based on the profit of previous blocks. The profit is the
117/// difference between the rewards from the DA portion of the gas price and the cost of recording the blocks on the DA chain.
118/// The algorithm uses a naive PID controller to calculate the change in the DA gas price. The "P" portion
119/// of the new gas price is "proportional" to the profit, either negative or positive. The "D" portion is derived
120/// from the slope or change in the profits since the last block.
121///
122/// if p > 0 and dp/db > 0, decrease
123/// if p > 0 and dp/db < 0, hold/moderate
124/// if p < 0 and dp/db < 0, increase
125/// if p < 0 and dp/db > 0, hold/moderate
126///
127/// The DA portion also uses a moving average of the profits over the last `avg_window` blocks
128/// instead of the actual profit. Setting the `avg_window` to 1 will effectively disable the
129/// moving average.
130#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
131pub struct AlgorithmUpdaterV1 {
132    // Execution
133    /// The gas price (scaled by the `gas_price_factor`) to cover the execution of the next block
134    pub new_scaled_exec_price: u64,
135    /// The lowest the algorithm allows the exec gas price to go
136    pub min_exec_gas_price: u64,
137    /// The Percentage the execution gas price will change in a single block, either increase or decrease
138    /// based on the fullness of the last L2 block. Using `u16` because it can go above 100% and
139    /// possibly over 255%
140    pub exec_gas_price_change_percent: u16,
141    /// The height of the next L2 block
142    pub l2_block_height: u32,
143    /// The threshold of gas usage above and below which the gas price will increase or decrease
144    /// This is a percentage of the total capacity of the L2 block
145    pub l2_block_fullness_threshold_percent: ClampedPercentage,
146    // DA
147    /// The gas price (scaled by the `gas_price_factor`) to cover the DA commitment of the next block
148    pub new_scaled_da_gas_price: u64,
149    /// Scale factor for the gas price.
150    pub gas_price_factor: NonZeroU64,
151    /// The lowest the algorithm allows the da gas price to go
152    pub min_da_gas_price: u64,
153    /// The highest the algorithm allows the da gas price to go
154    pub max_da_gas_price: u64,
155    /// The maximum percentage that the DA portion of the gas price can change in a single block
156    ///   Using `u16` because it can go above 100% and possibly over 255%
157    pub max_da_gas_price_change_percent: u16,
158    /// The cumulative reward from the DA portion of the gas price
159    pub total_da_rewards: u128,
160    /// The cumulative cost of recording L2 blocks on the DA chain as of the last recorded block
161    pub latest_known_total_da_cost: u128,
162    /// The predicted cost of recording L2 blocks on the DA chain as of the last L2 block
163    /// (This value is added on top of the `latest_known_total_da_cost` if the L2 height is higher)
164    pub projected_total_da_cost: u128,
165    /// The P component of the PID control for the DA gas price
166    pub da_p_component: i64,
167    /// The D component of the PID control for the DA gas price
168    pub da_d_component: i64,
169    /// The last profit
170    pub last_profit: i128,
171    /// The profit before last
172    pub second_to_last_profit: i128,
173    /// The latest known cost per byte for recording blocks on the DA chain
174    pub latest_da_cost_per_byte: u128,
175    /// Activity of L2
176    pub l2_activity: L2ActivityTracker,
177    /// Total unrecorded block bytes
178    pub unrecorded_blocks_bytes: u128,
179}
180
181/// The `L2ActivityTracker` tracks the chain activity to determine a safety mode for setting the DA price.
182///
183/// Because the DA gas price can increase even when no-one is using the network, there is a potential
184/// for a negative feedback loop to occur where the gas price increases, further decreasing activity
185/// and increasing the gas price. The `L2ActivityTracker` is used to moderate changes to the DA
186/// gas price based on the activity of the L2 chain.
187///
188/// The chain activity is a cumulative measure, updated whenever a new block is processed.
189/// For each L2 block, the block usage is a percentage of the block capacity used. If the
190/// block usage is below a certain threshold, the chain activity is decreased, if above the threshold,
191/// the activity is increased The chain activity exists on a scale
192/// between 0 and the sum of the normal, capped, and decrease buffers.
193///
194/// e.g. if the decrease activity threshold is 20, the capped activity threshold is 80, and the max activity is 120,
195/// we'd have the following ranges:
196///
197/// 0 <-- decrease range -->20<-- capped range -->80<-- normal range -->120
198///
199/// The current chain activity determines the behavior of the DA gas price.
200///
201/// For healthy behavior, the activity should be in the `normal` range.
202#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
203pub struct L2ActivityTracker {
204    /// The maximum value the chain activity can hit
205    max_activity: u16,
206    /// The threshold if the block activity is below, the DA gas price will be held when it would otherwise be increased
207    capped_activity_threshold: u16,
208    /// If the chain activity falls below this value, the DA gas price will be decreased when it would otherwise be increased
209    decrease_activity_threshold: u16,
210    /// The current activity of the L2 chain
211    chain_activity: u16,
212    /// The threshold of block activity below which the chain activity will be decreased,
213    /// above or equal it will always increase
214    block_activity_threshold: ClampedPercentage,
215}
216
217/// Designates the intended behavior of the DA gas price based on the activity of the L2 chain
218pub enum DAGasPriceSafetyMode {
219    /// Should increase DA gas price freely
220    Normal,
221    /// Should not increase the DA gas price
222    Capped,
223    /// Should decrease the DA gas price always
224    AlwaysDecrease,
225}
226
227impl L2ActivityTracker {
228    pub fn new_full(
229        normal_range_size: u16,
230        capped_range_size: u16,
231        decrease_range_size: u16,
232        block_activity_threshold: ClampedPercentage,
233    ) -> Self {
234        let decrease_activity_threshold = decrease_range_size;
235        let capped_activity_threshold =
236            decrease_range_size.saturating_add(capped_range_size);
237        let max_activity = capped_activity_threshold.saturating_add(normal_range_size);
238        let chain_activity = max_activity;
239        Self {
240            max_activity,
241            capped_activity_threshold,
242            decrease_activity_threshold,
243            chain_activity,
244            block_activity_threshold,
245        }
246    }
247
248    pub fn new(
249        normal_range_size: u16,
250        capped_range_size: u16,
251        decrease_range_size: u16,
252        activity: u16,
253        block_activity_threshold: ClampedPercentage,
254    ) -> Self {
255        let mut tracker = Self::new_full(
256            normal_range_size,
257            capped_range_size,
258            decrease_range_size,
259            block_activity_threshold,
260        );
261        tracker.chain_activity = activity.min(tracker.max_activity);
262        tracker
263    }
264
265    pub fn new_always_normal() -> Self {
266        let normal_range_size = 100;
267        let capped_range_size = 0;
268        let decrease_range_size = 0;
269        let percentage = ClampedPercentage::new(0);
270        Self::new(
271            normal_range_size,
272            capped_range_size,
273            decrease_range_size,
274            100,
275            percentage,
276        )
277    }
278
279    pub fn safety_mode(&self) -> DAGasPriceSafetyMode {
280        if self.chain_activity >= self.capped_activity_threshold {
281            DAGasPriceSafetyMode::Normal
282        } else if self.chain_activity >= self.decrease_activity_threshold {
283            DAGasPriceSafetyMode::Capped
284        } else {
285            DAGasPriceSafetyMode::AlwaysDecrease
286        }
287    }
288
289    pub fn update(&mut self, block_usage: ClampedPercentage) {
290        if block_usage < self.block_activity_threshold {
291            tracing::debug!(
292                "Decreasing activity {:?} < {:?}",
293                block_usage,
294                self.block_activity_threshold
295            );
296            self.chain_activity = self.chain_activity.saturating_sub(1);
297        } else {
298            self.chain_activity =
299                self.chain_activity.saturating_add(1).min(self.max_activity);
300        }
301    }
302
303    pub fn current_activity(&self) -> u16 {
304        self.chain_activity
305    }
306
307    pub fn max_activity(&self) -> u16 {
308        self.max_activity
309    }
310
311    pub fn capped_activity_threshold(&self) -> u16 {
312        self.capped_activity_threshold
313    }
314
315    pub fn decrease_activity_threshold(&self) -> u16 {
316        self.decrease_activity_threshold
317    }
318
319    pub fn block_activity_threshold(&self) -> ClampedPercentage {
320        self.block_activity_threshold
321    }
322}
323
324/// A value that represents a value between 0 and 100. Higher values are clamped to 100
325#[derive(
326    serde::Serialize, serde::Deserialize, Debug, Copy, Clone, PartialEq, PartialOrd,
327)]
328pub struct ClampedPercentage {
329    value: u8,
330}
331
332impl ClampedPercentage {
333    pub fn new(maybe_value: u8) -> Self {
334        Self {
335            value: maybe_value.min(100),
336        }
337    }
338}
339
340impl From<u8> for ClampedPercentage {
341    fn from(value: u8) -> Self {
342        Self::new(value)
343    }
344}
345
346impl core::ops::Deref for ClampedPercentage {
347    type Target = u8;
348
349    fn deref(&self) -> &Self::Target {
350        &self.value
351    }
352}
353
354impl AlgorithmUpdaterV1 {
355    pub fn update_da_record_data<U: UnrecordedBlocks>(
356        &mut self,
357        heights: RangeInclusive<u32>,
358        recorded_bytes: u32,
359        recording_cost: u128,
360        unrecorded_blocks: &mut U,
361    ) -> Result<(), Error> {
362        if !heights.is_empty() {
363            self.da_block_update(
364                heights,
365                recorded_bytes as u128,
366                recording_cost,
367                unrecorded_blocks,
368            )?;
369            self.recalculate_projected_cost();
370            self.update_da_gas_price();
371        }
372        Ok(())
373    }
374
375    pub fn update_l2_block_data<U: UnrecordedBlocks>(
376        &mut self,
377        height: u32,
378        used: u64,
379        capacity: NonZeroU64,
380        block_bytes: u64,
381        fee_wei: u128,
382        unrecorded_blocks: &mut U,
383    ) -> Result<(), Error> {
384        let expected = self.l2_block_height.saturating_add(1);
385        if height != expected {
386            Err(Error::SkippedL2Block {
387                expected,
388                got: height,
389            })
390        } else {
391            self.l2_block_height = height;
392
393            // rewards
394            self.update_da_rewards(fee_wei);
395            let rewards = self.clamped_rewards_as_i128();
396
397            // costs
398            self.update_projected_da_cost(block_bytes);
399            let projected_total_da_cost = self.clamped_projected_cost_as_i128();
400
401            // profit
402            let last_profit = rewards.saturating_sub(projected_total_da_cost);
403            self.update_last_profit(last_profit);
404
405            // activity
406            self.update_activity(used, capacity);
407
408            // gas prices
409            self.update_exec_gas_price(used, capacity);
410            self.update_da_gas_price();
411
412            // metadata
413            unrecorded_blocks
414                .insert(height, block_bytes)
415                .map_err(Error::CouldNotInsertUnrecordedBlock)?;
416            self.unrecorded_blocks_bytes = self
417                .unrecorded_blocks_bytes
418                .saturating_add(block_bytes as u128);
419            Ok(())
420        }
421    }
422
423    fn update_activity(&mut self, used: u64, capacity: NonZeroU64) {
424        let block_activity = used.saturating_mul(100).div(capacity);
425        let usage = ClampedPercentage::new(block_activity.try_into().unwrap_or(100));
426        self.l2_activity.update(usage);
427    }
428
429    fn update_da_rewards(&mut self, fee_wei: u128) {
430        let block_da_reward = self.da_portion_of_fee(fee_wei);
431        self.total_da_rewards = self.total_da_rewards.saturating_add(block_da_reward);
432    }
433
434    fn update_projected_da_cost(&mut self, block_bytes: u64) {
435        let block_projected_da_cost =
436            (block_bytes as u128).saturating_mul(self.latest_da_cost_per_byte);
437        self.projected_total_da_cost = self
438            .projected_total_da_cost
439            .saturating_add(block_projected_da_cost);
440    }
441
442    // Take the `fee_wei` and return the portion of the fee that should be used for paying DA costs
443    fn da_portion_of_fee(&self, fee_wei: u128) -> u128 {
444        // fee_wei * (da_price / (exec_price + da_price))
445        let numerator = fee_wei.saturating_mul(self.descaled_da_price() as u128);
446        let denominator = (self.descaled_exec_price() as u128)
447            .saturating_add(self.descaled_da_price() as u128);
448        if denominator == 0 {
449            0
450        } else {
451            numerator.div_ceil(denominator)
452        }
453    }
454
455    fn clamped_projected_cost_as_i128(&self) -> i128 {
456        i128::try_from(self.projected_total_da_cost).unwrap_or(i128::MAX)
457    }
458
459    fn clamped_rewards_as_i128(&self) -> i128 {
460        i128::try_from(self.total_da_rewards).unwrap_or(i128::MAX)
461    }
462
463    fn update_last_profit(&mut self, new_profit: i128) {
464        self.second_to_last_profit = self.last_profit;
465        self.last_profit = new_profit;
466    }
467
468    fn update_exec_gas_price(&mut self, used: u64, capacity: NonZeroU64) {
469        let threshold = *self.l2_block_fullness_threshold_percent as u64;
470        let mut scaled_exec_gas_price = self.new_scaled_exec_price;
471        let fullness_percent = used
472            .saturating_mul(100)
473            .checked_div(capacity.into())
474            .unwrap_or(threshold);
475
476        match fullness_percent.cmp(&threshold) {
477            std::cmp::Ordering::Greater | std::cmp::Ordering::Equal => {
478                let change_amount = self.exec_change(scaled_exec_gas_price);
479                scaled_exec_gas_price =
480                    scaled_exec_gas_price.saturating_add(change_amount);
481            }
482            std::cmp::Ordering::Less => {
483                let change_amount = self.exec_change(scaled_exec_gas_price);
484                scaled_exec_gas_price =
485                    scaled_exec_gas_price.saturating_sub(change_amount);
486            }
487        }
488        self.new_scaled_exec_price =
489            max(self.min_scaled_exec_gas_price(), scaled_exec_gas_price);
490    }
491
492    fn min_scaled_exec_gas_price(&self) -> u64 {
493        self.min_exec_gas_price
494            .saturating_mul(self.gas_price_factor.into())
495    }
496
497    fn update_da_gas_price(&mut self) {
498        let p = self.p();
499        let d = self.d();
500        let maybe_scaled_da_change = self.da_change(p, d);
501        let scaled_da_change =
502            self.da_change_accounting_for_activity(maybe_scaled_da_change);
503        let maybe_new_scaled_da_gas_price = i128::from(self.new_scaled_da_gas_price)
504            .checked_add(scaled_da_change)
505            .and_then(|x| u64::try_from(x).ok())
506            .unwrap_or_else(|| {
507                if scaled_da_change.is_positive() {
508                    u64::MAX
509                } else {
510                    0u64
511                }
512            });
513        tracing::debug!("Profit: {}", self.last_profit);
514        tracing::debug!(
515            "DA gas price change: p: {}, d: {}, change: {}, new: {}",
516            p,
517            d,
518            scaled_da_change,
519            maybe_new_scaled_da_gas_price
520        );
521        self.new_scaled_da_gas_price = min(
522            max(
523                self.min_scaled_da_gas_price(),
524                maybe_new_scaled_da_gas_price,
525            ),
526            self.max_scaled_da_gas_price(),
527        );
528    }
529
530    fn da_change_accounting_for_activity(&self, maybe_da_change: i128) -> i128 {
531        if maybe_da_change > 0 {
532            match self.l2_activity.safety_mode() {
533                DAGasPriceSafetyMode::Normal => maybe_da_change,
534                DAGasPriceSafetyMode::Capped => 0,
535                DAGasPriceSafetyMode::AlwaysDecrease => {
536                    tracing::info!("Activity is low, decreasing DA gas price");
537                    self.max_change().saturating_mul(-1)
538                }
539            }
540        } else {
541            maybe_da_change
542        }
543    }
544
545    fn min_scaled_da_gas_price(&self) -> u64 {
546        self.min_da_gas_price
547            .saturating_mul(self.gas_price_factor.into())
548    }
549
550    fn max_scaled_da_gas_price(&self) -> u64 {
551        // note: here we make sure that the correct maximum is used
552        max(self.max_da_gas_price, self.min_da_gas_price)
553            .saturating_mul(self.gas_price_factor.into())
554    }
555
556    fn p(&self) -> i128 {
557        let upcast_p = i128::from(self.da_p_component);
558        let checked_p = self.last_profit.checked_div(upcast_p);
559        // If the profit is positive, we want to decrease the gas price
560        checked_p.unwrap_or(0).saturating_mul(-1)
561    }
562
563    fn d(&self) -> i128 {
564        let upcast_d = i128::from(self.da_d_component);
565        let slope = self.last_profit.saturating_sub(self.second_to_last_profit);
566        let checked_d = slope.checked_div(upcast_d);
567        // if the slope is positive, we want to decrease the gas price
568        checked_d.unwrap_or(0).saturating_mul(-1)
569    }
570
571    fn da_change(&self, p: i128, d: i128) -> i128 {
572        let scaled_pd_change = p
573            .saturating_add(d)
574            .saturating_mul(self.gas_price_factor.get() as i128);
575        let max_change = self.max_change();
576        let clamped_change = scaled_pd_change.saturating_abs().min(max_change);
577        scaled_pd_change.signum().saturating_mul(clamped_change)
578    }
579
580    // Should always be positive
581    fn max_change(&self) -> i128 {
582        let upcast_percent = self.max_da_gas_price_change_percent.into();
583        self.new_scaled_da_gas_price
584            .saturating_mul(upcast_percent)
585            .saturating_div(100)
586            .into()
587    }
588
589    fn exec_change(&self, principle: u64) -> u64 {
590        principle
591            .saturating_mul(self.exec_gas_price_change_percent as u64)
592            .saturating_div(100)
593    }
594
595    fn da_block_update<U: UnrecordedBlocks>(
596        &mut self,
597        heights: RangeInclusive<u32>,
598        recorded_bytes: u128,
599        recording_cost: u128,
600        unrecorded_blocks: &mut U,
601    ) -> Result<(), Error> {
602        self.update_unrecorded_block_bytes(heights, unrecorded_blocks)?;
603
604        let new_da_block_cost = self
605            .latest_known_total_da_cost
606            .saturating_add(recording_cost);
607        self.latest_known_total_da_cost = new_da_block_cost;
608
609        let compressed_cost_per_bytes = recording_cost
610            .checked_div(recorded_bytes)
611            .ok_or(Error::CouldNotCalculateCostPerByte {
612                bytes: recorded_bytes,
613                cost: recording_cost,
614            })?;
615
616        // This is often "pessimistic" in the sense that we are charging for the compressed blocks
617        // and we will use it to calculate base on the uncompressed blocks
618        self.latest_da_cost_per_byte = compressed_cost_per_bytes;
619        Ok(())
620    }
621
622    // Get the bytes for all specified heights, or get none of them.
623    // Always remove the blocks from the unrecorded blocks so they don't build up indefinitely
624    fn update_unrecorded_block_bytes<U: UnrecordedBlocks>(
625        &mut self,
626        heights: RangeInclusive<u32>,
627        unrecorded_blocks: &mut U,
628    ) -> Result<(), Error> {
629        let mut total: u128 = 0;
630        for expected_height in heights {
631            let maybe_bytes = unrecorded_blocks
632                .remove(&expected_height)
633                .map_err(Error::CouldNotRemoveUnrecordedBlock)?;
634
635            if let Some(bytes) = maybe_bytes {
636                total = total.saturating_add(bytes as u128);
637            } else {
638                tracing::warn!(
639                    "L2 block expected but not found in unrecorded blocks: {}",
640                    expected_height,
641                );
642            }
643        }
644        self.unrecorded_blocks_bytes = self.unrecorded_blocks_bytes.saturating_sub(total);
645
646        Ok(())
647    }
648
649    fn recalculate_projected_cost(&mut self) {
650        let projection_portion = self
651            .unrecorded_blocks_bytes
652            .saturating_mul(self.latest_da_cost_per_byte);
653        self.projected_total_da_cost = self
654            .latest_known_total_da_cost
655            .saturating_add(projection_portion);
656    }
657
658    fn descaled_exec_price(&self) -> u64 {
659        self.new_scaled_exec_price.div(self.gas_price_factor)
660    }
661
662    fn descaled_da_price(&self) -> u64 {
663        self.new_scaled_da_gas_price.div(self.gas_price_factor)
664    }
665
666    pub fn algorithm(&self) -> AlgorithmV1 {
667        AlgorithmV1 {
668            new_exec_price: self.descaled_exec_price(),
669            exec_price_percentage: self.exec_gas_price_change_percent as u64,
670            new_da_gas_price: self.descaled_da_price(),
671            da_gas_price_percentage: self.max_da_gas_price_change_percent as u64,
672            for_height: self.l2_block_height,
673        }
674    }
675}