diff --git a/CHANGELOG.md b/CHANGELOG.md index a67e169d9a7..82018148012 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added - [2321](https://github.com/FuelLabs/fuel-core/pull/2321): New metrics for the txpool: "The size of transactions in the txpool" (`txpool_tx_size`), "The time spent by a transaction in the txpool in seconds" (`txpool_tx_time_in_txpool_seconds`), The number of transactions in the txpool (`txpool_number_of_transactions`), "The number of transactions pending verification before entering the txpool" (`txpool_number_of_transactions_pending_verification`), "The number of executable transactions in the txpool" (`txpool_number_of_executable_transactions`), "The time it took to select transactions for inclusion in a block in nanoseconds" (`txpool_select_transaction_time_nanoseconds`), The time it took to insert a transaction in the txpool in milliseconds (`txpool_insert_transaction_time_milliseconds`). +- [2347](https://github.com/FuelLabs/fuel-core/pull/2364): Add activity concept in order to protect against infinitely increasing DA gas price scenarios - [2362](https://github.com/FuelLabs/fuel-core/pull/2362): Added a new request_response protocol version `/fuel/req_res/0.0.2`. In comparison with `/fuel/req/0.0.1`, which returns an empty response when a request cannot be fulfilled, this version returns more meaningful error codes. Nodes still support the version `0.0.1` of the protocol to guarantee backward compatibility with fuel-core nodes. Empty responses received from nodes using the old protocol `/fuel/req/0.0.1` are automatically converted into an error `ProtocolV1EmptyResponse` with error code 0, which is also the only error code implemented. More specific error codes will be added in the future. ## [Version 0.40.0] diff --git a/crates/fuel-gas-price-algorithm/gas-price-analysis/src/simulation.rs b/crates/fuel-gas-price-algorithm/gas-price-analysis/src/simulation.rs index 94d7c06faa3..590952207ba 100644 --- a/crates/fuel-gas-price-algorithm/gas-price-analysis/src/simulation.rs +++ b/crates/fuel-gas-price-algorithm/gas-price-analysis/src/simulation.rs @@ -1,5 +1,8 @@ use super::*; -use fuel_gas_price_algorithm::v1::AlgorithmUpdaterV1; +use fuel_gas_price_algorithm::v1::{ + AlgorithmUpdaterV1, + L2ActivityTracker, +}; use std::{ collections::BTreeMap, num::NonZeroU64, @@ -83,6 +86,7 @@ impl Simulator { ) -> AlgorithmUpdaterV1 { // Scales the gas price internally, value is arbitrary let gas_price_factor = 100; + let always_normal_activity = L2ActivityTracker::new_always_normal(); let updater = AlgorithmUpdaterV1 { min_exec_gas_price: 10, min_da_gas_price: 10, @@ -109,6 +113,7 @@ impl Simulator { da_d_component, last_profit: 0, second_to_last_profit: 0, + l2_activity: always_normal_activity, }; updater } diff --git a/crates/fuel-gas-price-algorithm/src/v1.rs b/crates/fuel-gas-price-algorithm/src/v1.rs index a1a5f7dd536..9a9da066fee 100644 --- a/crates/fuel-gas-price-algorithm/src/v1.rs +++ b/crates/fuel-gas-price-algorithm/src/v1.rs @@ -26,6 +26,8 @@ pub enum Error { L2BlockExpectedNotFound(u32), } +// TODO: separate exec gas price and DA gas price into newtypes for clarity +// https://github.com/FuelLabs/fuel-core/issues/2382 #[derive(Debug, Clone, PartialEq)] pub struct AlgorithmV1 { /// The gas price for to cover the execution of the next block @@ -118,13 +120,8 @@ pub struct AlgorithmUpdaterV1 { /// This is a percentage of the total capacity of the L2 block pub l2_block_fullness_threshold_percent: ClampedPercentage, // DA - /// The gas price for the DA portion of the last block. This can be used to calculate - /// the DA portion of the next block - // pub last_da_gas_price: u64, - /// The gas price (scaled by the `gas_price_factor`) to cover the DA commitment of the next block pub new_scaled_da_gas_price: u64, - /// Scale factor for the gas price. pub gas_price_factor: NonZeroU64, /// The lowest the algorithm allows the da gas price to go @@ -151,13 +148,136 @@ pub struct AlgorithmUpdaterV1 { pub second_to_last_profit: i128, /// The latest known cost per byte for recording blocks on the DA chain pub latest_da_cost_per_byte: u128, - + /// Activity of L2 + pub l2_activity: L2ActivityTracker, /// The unrecorded blocks that are used to calculate the projected cost of recording blocks pub unrecorded_blocks: BTreeMap, } -/// A value that represents a value between 0 and 100. Higher values are clamped to 100 +/// The `L2ActivityTracker` tracks the chain activity to determine a safety mode for setting the DA price. +/// +/// Because the DA gas price can increase even when no-one is using the network, there is a potential +/// for a negative feedback loop to occur where the gas price increases, further decreasing activity +/// and increasing the gas price. The `L2ActivityTracker` is used to moderate changes to the DA +/// gas price based on the activity of the L2 chain. +/// +/// The chain activity is a cumulative measure, updated whenever a new block is processed. +/// For each L2 block, the block usage is a percentage of the block capacity used. If the +/// block usage is below a certain threshold, the chain activity is decreased, if above the threshold, +/// the activity is increased The chain activity exists on a scale +/// between 0 and the sum of the normal, capped, and decrease buffers. +/// +/// e.g. if the decrease activity threshold is 20, the capped activity threshold is 80, and the max activity is 120, +/// we'd have the following ranges: +/// +/// 0 <-- decrease range -->20<-- capped range -->80<-- normal range -->120 +/// +/// The current chain activity determines the behavior of the DA gas price. +/// +/// For healthy behavior, the activity should be in the `normal` range. #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)] +pub struct L2ActivityTracker { + /// The maximum value the chain activity can hit + max_activity: u16, + /// The threshold if the block activity is below, the DA gas price will be held when it would otherwise be increased + capped_activity_threshold: u16, + /// If the chain activity falls below this value, the DA gas price will be decreased when it would otherwise be increased + decrease_activity_threshold: u16, + /// The current activity of the L2 chain + chain_activity: u16, + /// The threshold of block activity below which the chain activity will be decreased, + /// above or equal it will always increase + block_activity_threshold: ClampedPercentage, +} + +/// Designates the intended behavior of the DA gas price based on the activity of the L2 chain +pub enum DAGasPriceSafetyMode { + /// Should increase DA gas price freely + Normal, + /// Should not increase the DA gas price + Capped, + /// Should decrease the DA gas price always + AlwaysDecrease, +} + +impl L2ActivityTracker { + pub fn new_full( + normal_range_size: u16, + capped_range_size: u16, + decrease_range_size: u16, + block_activity_threshold: ClampedPercentage, + ) -> Self { + let decrease_activity_threshold = decrease_range_size; + let capped_activity_threshold = + decrease_range_size.saturating_add(capped_range_size); + let max_activity = capped_activity_threshold.saturating_add(normal_range_size); + let chain_activity = max_activity; + Self { + max_activity, + capped_activity_threshold, + decrease_activity_threshold, + chain_activity, + block_activity_threshold, + } + } + + pub fn new( + normal_range_size: u16, + capped_range_size: u16, + decrease_range_size: u16, + activity: u16, + block_activity_threshold: ClampedPercentage, + ) -> Self { + let mut tracker = Self::new_full( + normal_range_size, + capped_range_size, + decrease_range_size, + block_activity_threshold, + ); + tracker.chain_activity = activity.min(tracker.max_activity); + tracker + } + + pub fn new_always_normal() -> Self { + let normal_range_size = 100; + let capped_range_size = 0; + let decrease_range_size = 0; + let percentage = ClampedPercentage::new(0); + Self::new( + normal_range_size, + capped_range_size, + decrease_range_size, + 100, + percentage, + ) + } + + pub fn safety_mode(&self) -> DAGasPriceSafetyMode { + if self.chain_activity > self.capped_activity_threshold { + DAGasPriceSafetyMode::Normal + } else if self.chain_activity > self.decrease_activity_threshold { + DAGasPriceSafetyMode::Capped + } else { + DAGasPriceSafetyMode::AlwaysDecrease + } + } + + pub fn update(&mut self, block_usage: ClampedPercentage) { + if block_usage < self.block_activity_threshold { + self.chain_activity = self.chain_activity.saturating_sub(1); + } else { + self.chain_activity = + self.chain_activity.saturating_add(1).min(self.max_activity); + } + } + + pub fn current_activity(&self) -> u16 { + self.chain_activity + } +} + +/// A value that represents a value between 0 and 100. Higher values are clamped to 100 +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, PartialOrd)] pub struct ClampedPercentage { value: u8, } @@ -226,6 +346,9 @@ impl AlgorithmUpdaterV1 { let last_profit = rewards.saturating_sub(projected_total_da_cost); self.update_last_profit(last_profit); + // activity + self.update_activity(used, capacity); + // gas prices self.update_exec_gas_price(used, capacity); self.update_da_gas_price(); @@ -236,6 +359,12 @@ impl AlgorithmUpdaterV1 { } } + fn update_activity(&mut self, used: u64, capacity: NonZeroU64) { + let block_activity = used.saturating_mul(100).div(capacity); + let usage = ClampedPercentage::new(block_activity.try_into().unwrap_or(100)); + self.l2_activity.update(usage); + } + fn update_da_rewards(&mut self, fee_wei: u128) { let block_da_reward = self.da_portion_of_fee(fee_wei); self.total_da_rewards_excess = @@ -309,7 +438,8 @@ impl AlgorithmUpdaterV1 { fn update_da_gas_price(&mut self) { let p = self.p(); let d = self.d(); - let da_change = self.da_change(p, d); + let maybe_da_change = self.da_change(p, d); + let da_change = self.da_change_accounting_for_activity(maybe_da_change); let maybe_new_scaled_da_gas_price = i128::from(self.new_scaled_da_gas_price) .checked_add(da_change) .and_then(|x| u64::try_from(x).ok()) @@ -326,6 +456,20 @@ impl AlgorithmUpdaterV1 { ); } + fn da_change_accounting_for_activity(&self, maybe_da_change: i128) -> i128 { + if maybe_da_change > 0 { + match self.l2_activity.safety_mode() { + DAGasPriceSafetyMode::Normal => maybe_da_change, + DAGasPriceSafetyMode::Capped => 0, + DAGasPriceSafetyMode::AlwaysDecrease => { + self.max_change().saturating_mul(-1) + } + } + } else { + maybe_da_change + } + } + fn min_scaled_da_gas_price(&self) -> u64 { self.min_da_gas_price .saturating_mul(self.gas_price_factor.into()) @@ -348,14 +492,18 @@ impl AlgorithmUpdaterV1 { fn da_change(&self, p: i128, d: i128) -> i128 { let pd_change = p.saturating_add(d); + let max_change = self.max_change(); + let clamped_change = pd_change.saturating_abs().min(max_change); + pd_change.signum().saturating_mul(clamped_change) + } + + // Should always be positive + fn max_change(&self) -> i128 { let upcast_percent = self.max_da_gas_price_change_percent.into(); - let max_change = self - .new_scaled_da_gas_price + self.new_scaled_da_gas_price .saturating_mul(upcast_percent) .saturating_div(100) - .into(); - let clamped_change = pd_change.saturating_abs().min(max_change); - pd_change.signum().saturating_mul(clamped_change) + .into() } fn exec_change(&self, principle: u64) -> u64 { diff --git a/crates/fuel-gas-price-algorithm/src/v1/tests.rs b/crates/fuel-gas-price-algorithm/src/v1/tests.rs index 0aca738a3f0..e7150378428 100644 --- a/crates/fuel-gas-price-algorithm/src/v1/tests.rs +++ b/crates/fuel-gas-price-algorithm/src/v1/tests.rs @@ -2,7 +2,10 @@ #![allow(clippy::arithmetic_side_effects)] #![allow(clippy::cast_possible_truncation)] -use crate::v1::AlgorithmUpdaterV1; +use crate::v1::{ + AlgorithmUpdaterV1, + L2ActivityTracker, +}; #[cfg(test)] mod algorithm_v1_tests; @@ -40,6 +43,7 @@ pub struct UpdaterBuilder { last_profit: i128, second_to_last_profit: i128, da_gas_price_factor: u64, + l2_activity: L2ActivityTracker, } impl UpdaterBuilder { @@ -67,6 +71,7 @@ impl UpdaterBuilder { last_profit: 0, second_to_last_profit: 0, da_gas_price_factor: 1, + l2_activity: L2ActivityTracker::new_always_normal(), } } @@ -159,6 +164,11 @@ impl UpdaterBuilder { self } + fn with_activity(mut self, l2_activity: L2ActivityTracker) -> Self { + self.l2_activity = l2_activity; + self + } + fn build(self) -> AlgorithmUpdaterV1 { AlgorithmUpdaterV1 { min_exec_gas_price: self.min_exec_gas_price, @@ -190,6 +200,7 @@ impl UpdaterBuilder { .da_gas_price_factor .try_into() .expect("Should never be non-zero"), + l2_activity: self.l2_activity, } } } diff --git a/crates/fuel-gas-price-algorithm/src/v1/tests/update_l2_block_data_tests.rs b/crates/fuel-gas-price-algorithm/src/v1/tests/update_l2_block_data_tests.rs index 6d5e4cf2ce7..5cb56bbde2d 100644 --- a/crates/fuel-gas-price-algorithm/src/v1/tests/update_l2_block_data_tests.rs +++ b/crates/fuel-gas-price-algorithm/src/v1/tests/update_l2_block_data_tests.rs @@ -4,8 +4,64 @@ use crate::v1::{ UpdaterBuilder, }, Error, + L2ActivityTracker, }; +fn decrease_l2_activity() -> L2ActivityTracker { + let normal = 1; + let capped = 1; + let decrease = 100; + let activity = 50; + let threshold = 50.into(); + L2ActivityTracker::new(normal, capped, decrease, activity, threshold) +} + +fn negative_profit_updater_builder() -> UpdaterBuilder { + let starting_exec_gas_price = 100; + let starting_da_gas_price = 100; + let starting_cost = u128::MAX; + let latest_gas_per_byte = i32::MAX; // DA is very expensive + let da_p_component = 100; + let da_d_component = 10; + let last_profit = i128::MIN; + let last_last_profit = 0; + let smaller_starting_reward = 0; + UpdaterBuilder::new() + .with_starting_exec_gas_price(starting_exec_gas_price) + .with_starting_da_gas_price(starting_da_gas_price) + .with_da_p_component(da_p_component) + .with_da_d_component(da_d_component) + .with_total_rewards(smaller_starting_reward) + .with_known_total_cost(starting_cost) + .with_projected_total_cost(starting_cost) + .with_da_cost_per_byte(latest_gas_per_byte as u128) + .with_last_profit(last_profit, last_last_profit) +} + +fn positive_profit_updater_builder() -> UpdaterBuilder { + let starting_exec_gas_price = 100; + let last_da_gas_price = 100; + let starting_cost = 500; + let latest_gas_per_byte = 0; // DA is free + let da_p_component = 100; + let da_d_component = 10; + let last_profit = i128::MAX; + let last_last_profit = 0; + let large_reward = i128::MAX; + UpdaterBuilder::new() + .with_starting_exec_gas_price(starting_exec_gas_price) + .with_da_p_component(da_p_component) + .with_da_d_component(da_d_component) + .with_starting_da_gas_price(last_da_gas_price) + .with_total_rewards(large_reward as u128) + .with_known_total_cost(starting_cost as u128) + .with_projected_total_cost(starting_cost as u128) + .with_da_cost_per_byte(latest_gas_per_byte as u128) + .with_last_profit(last_profit, last_last_profit) + .with_da_max_change_percent(u16::MAX) + .with_exec_gas_price_change_percent(0) +} + #[test] fn update_l2_block_data__updates_l2_block() { // given @@ -268,32 +324,11 @@ fn update_l2_block_data__updates_last_and_last_last_profit() { #[test] fn update_l2_block_data__positive_profit_decrease_gas_price() { // given - let starting_exec_gas_price = 100; - let last_da_gas_price = 100; - let starting_cost = 500; - let latest_gas_per_byte = 0; // DA is free - let da_p_component = 100; - let da_d_component = 10; - let block_bytes = 500u64; - let last_profit = i128::MAX; - let last_last_profit = 0; - let large_reward = i128::MAX; - let mut updater = UpdaterBuilder::new() - .with_starting_exec_gas_price(starting_exec_gas_price) - .with_da_p_component(da_p_component) - .with_da_d_component(da_d_component) - .with_starting_da_gas_price(last_da_gas_price) - .with_total_rewards(large_reward as u128) - .with_known_total_cost(starting_cost as u128) - .with_projected_total_cost(starting_cost as u128) - .with_da_cost_per_byte(latest_gas_per_byte as u128) - .with_last_profit(last_profit, last_last_profit) - .with_da_max_change_percent(u16::MAX) - .with_exec_gas_price_change_percent(0) - .build(); + let mut updater = positive_profit_updater_builder().build(); let old_gas_price = updater.algorithm().calculate(); // when + let block_bytes = 500u64; updater .update_l2_block_data( updater.l2_block_height + 1, @@ -496,26 +531,7 @@ fn update_l2_block_data__even_profit_maintains_price() { #[test] fn update_l2_block_data__negative_profit_increase_gas_price() { // given - let starting_exec_gas_price = 100; - let starting_da_gas_price = 100; - let starting_cost = u128::MAX; - let latest_gas_per_byte = i32::MAX; // DA is very expensive - let da_p_component = 100; - let da_d_component = 10; - let last_profit = i128::MIN; - let last_last_profit = 0; - let smaller_starting_reward = 0; - let mut updater = UpdaterBuilder::new() - .with_starting_exec_gas_price(starting_exec_gas_price) - .with_starting_da_gas_price(starting_da_gas_price) - .with_da_p_component(da_p_component) - .with_da_d_component(da_d_component) - .with_total_rewards(smaller_starting_reward) - .with_known_total_cost(starting_cost) - .with_projected_total_cost(starting_cost) - .with_da_cost_per_byte(latest_gas_per_byte as u128) - .with_last_profit(last_profit, last_last_profit) - .build(); + let mut updater = negative_profit_updater_builder().build(); let algo = updater.algorithm(); let old_gas_price = algo.calculate(); @@ -610,3 +626,272 @@ fn update_l2_block_data__retains_existing_blocks_and_adds_l2_block_to_unrecorded .contains_key(&preexisting_block.height); assert!(contains_preexisting_block_bytes); } + +fn capped_l2_activity_tracker() -> L2ActivityTracker { + let normal = 1; + let capped = 100; + let decrease = 1; + let activity = 50; + let threshold = 50.into(); + L2ActivityTracker::new(normal, capped, decrease, activity, threshold) +} + +#[test] +fn update_l2_block_data__da_gas_price_wants_to_increase_will_hold_if_activity_in_hold_range( +) { + // given + let capped_activity = capped_l2_activity_tracker(); + let mut updater = negative_profit_updater_builder() + .with_activity(capped_activity) + .build(); + let algo = updater.algorithm(); + let old_gas_price = algo.calculate(); + + // when + let height = updater.l2_block_height + 1; + let used = 50; + let capacity = 100u64.try_into().unwrap(); + let block_bytes = 500u64; + let fee = 0; + updater + .update_l2_block_data(height, used, capacity, block_bytes, fee) + .unwrap(); + + // then + let algo = updater.algorithm(); + let new_gas_price = algo.calculate(); + assert_eq!(new_gas_price, old_gas_price,); +} + +#[test] +fn update_l2_block_data__da_gas_price_wants_to_decrease_will_decrease_if_activity_in_hold_range( +) { + // given + let capped_activity = capped_l2_activity_tracker(); + let mut updater = positive_profit_updater_builder() + .with_activity(capped_activity) + .build(); + let old_gas_price = updater.algorithm().calculate(); + + // when + let block_bytes = 500u64; + updater + .update_l2_block_data( + updater.l2_block_height + 1, + 50, + 100.try_into().unwrap(), + block_bytes, + 200, + ) + .unwrap(); + + // then + let new_gas_price = updater.algorithm().calculate(); + assert!( + new_gas_price < old_gas_price, + "{} !< {}", + old_gas_price, + new_gas_price + ); +} + +#[test] +fn update_l2_block_data__da_gas_price_wants_to_increase_will_decrease_if_activity_in_decrease_range( +) { + // given + let decrease_activity = decrease_l2_activity(); + let mut updater = negative_profit_updater_builder() + .with_activity(decrease_activity) + .build(); + let algo = updater.algorithm(); + let old_gas_price = algo.calculate(); + + // when + let height = updater.l2_block_height + 1; + let used = 50; + let capacity = 100u64.try_into().unwrap(); + let block_bytes = 500u64; + let fee = 0; + updater + .update_l2_block_data(height, used, capacity, block_bytes, fee) + .unwrap(); + + // then + let algo = updater.algorithm(); + let new_gas_price = algo.calculate(); + assert!( + new_gas_price < old_gas_price, + "{} !> {}", + new_gas_price, + old_gas_price + ); +} + +#[test] +fn update_l2_block_data__da_gas_price_wants_to_decrease_will_decrease_if_activity_in_decrease_range( +) { + // given + let decrease_activity = decrease_l2_activity(); + let mut updater = positive_profit_updater_builder() + .with_activity(decrease_activity) + .build(); + let old_gas_price = updater.algorithm().calculate(); + + // when + let block_bytes = 500u64; + updater + .update_l2_block_data( + updater.l2_block_height + 1, + 50, + 100.try_into().unwrap(), + block_bytes, + 200, + ) + .unwrap(); + + // then + let new_gas_price = updater.algorithm().calculate(); + assert!( + new_gas_price < old_gas_price, + "{} !< {}", + old_gas_price, + new_gas_price + ); +} + +#[test] +fn update_l2_block_data__above_threshold_increase_activity() { + // given + let starting_exec_gas_price = 100; + let exec_gas_price_increase_percent = 10; + let threshold = 50; + let starting_activity = 2; + let activity = L2ActivityTracker::new(1, 1, 1, starting_activity, 50.into()); + let mut updater = UpdaterBuilder::new() + .with_starting_exec_gas_price(starting_exec_gas_price) + .with_exec_gas_price_change_percent(exec_gas_price_increase_percent) + .with_l2_block_capacity_threshold(threshold) + .with_activity(activity) + .build(); + + let height = 1; + let used = 60; + let capacity = 100.try_into().unwrap(); + let block_bytes = 1000; + let fee = 200; + + // when + updater + .update_l2_block_data(height, used, capacity, block_bytes, fee) + .unwrap(); + + // then + let expected = starting_activity + 1; + let actual = updater.l2_activity.current_activity(); + assert_eq!(actual, expected); +} + +#[test] +fn update_l2_block_data__below_threshold_decrease_activity() { + // given + let starting_exec_gas_price = 100; + let exec_gas_price_increase_percent = 10; + let threshold = 50; + let starting_activity = 2; + let activity = L2ActivityTracker::new(1, 1, 1, starting_activity, 50.into()); + let mut updater = UpdaterBuilder::new() + .with_starting_exec_gas_price(starting_exec_gas_price) + .with_exec_gas_price_change_percent(exec_gas_price_increase_percent) + .with_l2_block_capacity_threshold(threshold) + .with_activity(activity) + .build(); + + let height = 1; + let used = 40; + let capacity = 100.try_into().unwrap(); + let block_bytes = 1000; + let fee = 200; + + // when + updater + .update_l2_block_data(height, used, capacity, block_bytes, fee) + .unwrap(); + + // then + let expected = starting_activity - 1; + let actual = updater.l2_activity.current_activity(); + assert_eq!(actual, expected); +} + +#[test] +fn update_l2_block_data__if_activity_at_max_will_stop_increasing() { + // given + let starting_exec_gas_price = 100; + let exec_gas_price_increase_percent = 10; + let threshold = 50; + let normal_range = 1; + let capped_range = 1; + let decrease_range = 1; + let starting_activity = normal_range + capped_range + decrease_range; + let activity = L2ActivityTracker::new( + normal_range, + capped_range, + decrease_range, + starting_activity, + 50.into(), + ); + let mut updater = UpdaterBuilder::new() + .with_starting_exec_gas_price(starting_exec_gas_price) + .with_exec_gas_price_change_percent(exec_gas_price_increase_percent) + .with_l2_block_capacity_threshold(threshold) + .with_activity(activity) + .build(); + + let height = 1; + let used = 60; + let capacity = 100.try_into().unwrap(); + let block_bytes = 1000; + let fee = 200; + + // when + updater + .update_l2_block_data(height, used, capacity, block_bytes, fee) + .unwrap(); + + // then + let expected = starting_activity; + let actual = updater.l2_activity.current_activity(); + assert_eq!(actual, expected); +} + +#[test] +fn update_l2_block_data__if_activity_is_zero_will_stop_decreasing() { + // given + let starting_exec_gas_price = 100; + let exec_gas_price_increase_percent = 10; + let threshold = 50; + let starting_activity = 0; + let activity = L2ActivityTracker::new(1, 1, 1, starting_activity, 50.into()); + let mut updater = UpdaterBuilder::new() + .with_starting_exec_gas_price(starting_exec_gas_price) + .with_exec_gas_price_change_percent(exec_gas_price_increase_percent) + .with_l2_block_capacity_threshold(threshold) + .with_activity(activity) + .build(); + + let height = 1; + let used = 40; + let capacity = 100.try_into().unwrap(); + let block_bytes = 1000; + let fee = 200; + + // when + updater + .update_l2_block_data(height, used, capacity, block_bytes, fee) + .unwrap(); + + // then + let expected = starting_activity; + let actual = updater.l2_activity.current_activity(); + assert_eq!(actual, expected); +}