From ae40ee4826e4007a5e3d771ecfa8152ce98ae611 Mon Sep 17 00:00:00 2001 From: Aaryamann Challani <43716372+rymnc@users.noreply.github.com> Date: Thu, 9 Jan 2025 23:22:06 +0530 Subject: [PATCH] feat(gas_price_service_v1): include `max_da_gas_price` to control volatility (#2541) ## Linked Issues/PRs - #2469 ## Description Adds a new config param `max_da_gas_price`, which `new_scaled_da_gas_price` depends upon while mutating it. ## Checklist - [ ] Breaking changes are clearly marked as such in the PR description and changelog - [ ] New behavior is reflected in tests - [ ] [The specification](https://github.com/FuelLabs/fuel-specs/) matches the implemented behavior (link update PR if changes are needed) ### Before requesting review - [ ] I have reviewed the code myself - [ ] I have created follow-up issues caused by this PR and linked them here ### After merging, notify other teams [Add or remove entries as needed] - [ ] [Rust SDK](https://github.com/FuelLabs/fuels-rs/) - [ ] [Sway compiler](https://github.com/FuelLabs/sway/) - [ ] [Platform documentation](https://github.com/FuelLabs/devrel-requests/issues/new?assignees=&labels=new+request&projects=&template=NEW-REQUEST.yml&title=%5BRequest%5D%3A+) (for out-of-organization contributors, the person merging the PR will do this) - [ ] Someone else? --------- Co-authored-by: Brandon Kite Co-authored-by: Mitchell Turner --- bin/fuel-core/src/cli/run.rs | 7 + .../service/adapters/gas_price_adapters.rs | 1 + crates/fuel-core/src/service/config.rs | 2 + crates/fuel-gas-price-algorithm/src/v1.rs | 22 +++- .../fuel-gas-price-algorithm/src/v1/tests.rs | 8 ++ .../v1/tests/update_da_record_data_tests.rs | 122 ++++++++++++++++++ .../v1/tests/update_l2_block_data_tests.rs | 10 ++ .../services/gas_price_service/src/ports.rs | 2 + .../gas_price_service/src/v1/metadata.rs | 3 + .../gas_price_service/src/v1/service.rs | 3 + .../gas_price_service/src/v1/tests.rs | 2 + tests/tests/gas_price.rs | 3 + 12 files changed, 181 insertions(+), 4 deletions(-) diff --git a/bin/fuel-core/src/cli/run.rs b/bin/fuel-core/src/cli/run.rs index 94c1afe0213..93c9fc95b65 100644 --- a/bin/fuel-core/src/cli/run.rs +++ b/bin/fuel-core/src/cli/run.rs @@ -212,6 +212,11 @@ pub struct Command { #[arg(long = "min-da-gas-price", default_value = "10000000", env)] pub min_da_gas_price: u64, + /// Maximum DA gas price + // DEV: ensure that the max_da_gas_price default is > then the min_da_gas_price default + #[arg(long = "max-da-gas-price", default_value = "10000001", env)] + pub max_da_gas_price: u64, + /// P component of DA gas price calculation /// **NOTE**: This is the **inverse** gain of a typical P controller. /// Increasing this value will reduce gas price fluctuations. @@ -339,6 +344,7 @@ impl Command { min_gas_price, gas_price_threshold_percent, min_da_gas_price, + max_da_gas_price, da_p_component, da_d_component, max_da_gas_price_change_percent, @@ -660,6 +666,7 @@ impl Command { memory_pool_size, da_gas_price_factor: NonZeroU64::new(100).expect("100 is not zero"), min_da_gas_price, + max_da_gas_price, max_da_gas_price_change_percent, da_p_component, da_d_component, diff --git a/crates/fuel-core/src/service/adapters/gas_price_adapters.rs b/crates/fuel-core/src/service/adapters/gas_price_adapters.rs index fe131ddadbf..d5eb0c71cb9 100644 --- a/crates/fuel-core/src/service/adapters/gas_price_adapters.rs +++ b/crates/fuel-core/src/service/adapters/gas_price_adapters.rs @@ -71,6 +71,7 @@ impl From for GasPriceServiceConfig { value.exec_gas_price_threshold_percent, value.da_gas_price_factor, value.min_da_gas_price, + value.max_da_gas_price, value.max_da_gas_price_change_percent, value.da_p_component, value.da_d_component, diff --git a/crates/fuel-core/src/service/config.rs b/crates/fuel-core/src/service/config.rs index ace51098781..d3acbe58d55 100644 --- a/crates/fuel-core/src/service/config.rs +++ b/crates/fuel-core/src/service/config.rs @@ -86,6 +86,7 @@ pub struct Config { pub memory_pool_size: usize, pub da_gas_price_factor: NonZeroU64, pub min_da_gas_price: u64, + pub max_da_gas_price: u64, pub max_da_gas_price_change_percent: u16, pub da_p_component: i64, pub da_d_component: i64, @@ -216,6 +217,7 @@ impl Config { memory_pool_size: 4, da_gas_price_factor: NonZeroU64::new(100).expect("100 is not zero"), min_da_gas_price: 0, + max_da_gas_price: 1, max_da_gas_price_change_percent: 0, da_p_component: 0, da_d_component: 0, diff --git a/crates/fuel-gas-price-algorithm/src/v1.rs b/crates/fuel-gas-price-algorithm/src/v1.rs index 628ef7d0254..bef07066275 100644 --- a/crates/fuel-gas-price-algorithm/src/v1.rs +++ b/crates/fuel-gas-price-algorithm/src/v1.rs @@ -1,6 +1,9 @@ use crate::utils::cumulative_percentage_change; use std::{ - cmp::max, + cmp::{ + max, + min, + }, collections::BTreeMap, num::NonZeroU64, ops::{ @@ -147,6 +150,8 @@ pub struct AlgorithmUpdaterV1 { pub gas_price_factor: NonZeroU64, /// The lowest the algorithm allows the da gas price to go pub min_da_gas_price: u64, + /// The highest the algorithm allows the da gas price to go + pub max_da_gas_price: u64, /// The maximum percentage that the DA portion of the gas price can change in a single block /// Using `u16` because it can go above 100% and possibly over 255% pub max_da_gas_price_change_percent: u16, @@ -514,9 +519,12 @@ impl AlgorithmUpdaterV1 { scaled_da_change, maybe_new_scaled_da_gas_price ); - self.new_scaled_da_gas_price = max( - self.min_scaled_da_gas_price(), - maybe_new_scaled_da_gas_price, + self.new_scaled_da_gas_price = min( + max( + self.min_scaled_da_gas_price(), + maybe_new_scaled_da_gas_price, + ), + self.max_scaled_da_gas_price(), ); } @@ -540,6 +548,12 @@ impl AlgorithmUpdaterV1 { .saturating_mul(self.gas_price_factor.into()) } + fn max_scaled_da_gas_price(&self) -> u64 { + // note: here we make sure that a correct maximum is used; hacky :( + max(self.max_da_gas_price, self.min_da_gas_price) + .saturating_mul(self.gas_price_factor.into()) + } + fn p(&self) -> i128 { let upcast_p = i128::from(self.da_p_component); let checked_p = self.last_profit.checked_div(upcast_p); diff --git a/crates/fuel-gas-price-algorithm/src/v1/tests.rs b/crates/fuel-gas-price-algorithm/src/v1/tests.rs index c2dcaba36fa..535ff23b3e5 100644 --- a/crates/fuel-gas-price-algorithm/src/v1/tests.rs +++ b/crates/fuel-gas-price-algorithm/src/v1/tests.rs @@ -26,6 +26,7 @@ pub struct BlockBytes { pub struct UpdaterBuilder { min_exec_gas_price: u64, min_da_gas_price: u64, + max_da_gas_price: u64, starting_exec_gas_price: u64, starting_da_gas_price: u64, exec_gas_price_change_percent: u16, @@ -53,6 +54,7 @@ impl UpdaterBuilder { Self { min_exec_gas_price: 0, min_da_gas_price: 0, + max_da_gas_price: 1, starting_exec_gas_price: 1, starting_da_gas_price: 1, exec_gas_price_change_percent: 0, @@ -86,6 +88,11 @@ impl UpdaterBuilder { self } + fn with_max_da_gas_price(mut self, max_price: u64) -> Self { + self.max_da_gas_price = max_price; + self + } + fn with_starting_exec_gas_price(mut self, starting_da_gas_price: u64) -> Self { self.starting_exec_gas_price = starting_da_gas_price; self @@ -192,6 +199,7 @@ impl UpdaterBuilder { last_profit: self.last_profit, second_to_last_profit: self.second_to_last_profit, min_da_gas_price: self.min_da_gas_price, + max_da_gas_price: self.max_da_gas_price, gas_price_factor: self .da_gas_price_factor .try_into() diff --git a/crates/fuel-gas-price-algorithm/src/v1/tests/update_da_record_data_tests.rs b/crates/fuel-gas-price-algorithm/src/v1/tests/update_da_record_data_tests.rs index da6812cce2f..5817227de8a 100644 --- a/crates/fuel-gas-price-algorithm/src/v1/tests/update_da_record_data_tests.rs +++ b/crates/fuel-gas-price-algorithm/src/v1/tests/update_da_record_data_tests.rs @@ -302,6 +302,7 @@ fn update_da_record_data__da_block_lowers_da_gas_price() { fn update_da_record_data__da_block_increases_da_gas_price() { // given let da_cost_per_byte = 40; + let max_da_gas_price = u64::MAX; let l2_block_height = 11; let original_known_total_cost = 150; let mut unrecorded_blocks: BTreeMap<_, _> = [(11, 3000)].into_iter().collect(); @@ -320,6 +321,7 @@ fn update_da_record_data__da_block_increases_da_gas_price() { .with_projected_total_cost(projected_total_cost as u128) .with_known_total_cost(original_known_total_cost as u128) .with_unrecorded_blocks(&unrecorded_blocks) + .with_max_da_gas_price(max_da_gas_price) .build(); let new_cost_per_byte = 100; @@ -355,6 +357,126 @@ fn update_da_record_data__da_block_increases_da_gas_price() { assert_ne!(old_da_gas_price, new_da_gas_price); } +#[test] +fn update_da_record_data__da_block_increases_da_gas_price_within_the_min_max_range() { + // given + let min_da_gas_price = 0; + let max_da_gas_price = 5; + let da_cost_per_byte = 40; + let l2_block_height = 11; + let original_known_total_cost = 150; + let mut unrecorded_blocks: BTreeMap<_, _> = [(11, 3000)].into_iter().collect(); + let da_p_component = 2; + let guessed_cost: u64 = unrecorded_blocks + .values() + .map(|bytes| bytes * da_cost_per_byte) + .sum(); + let projected_total_cost = original_known_total_cost + guessed_cost; + + let mut updater = UpdaterBuilder::new() + .with_da_cost_per_byte(da_cost_per_byte as u128) + .with_da_p_component(da_p_component) + .with_last_profit(-10, 0) + .with_l2_block_height(l2_block_height) + .with_projected_total_cost(projected_total_cost as u128) + .with_known_total_cost(original_known_total_cost as u128) + .with_unrecorded_blocks(&unrecorded_blocks) + .with_min_da_gas_price(min_da_gas_price) + .with_max_da_gas_price(max_da_gas_price) + .build(); + + let new_cost_per_byte = 100; + let (recorded_heights, recorded_cost) = unrecorded_blocks.iter().fold( + (vec![], 0), + |(mut range, cost), (height, bytes)| { + range.push(height); + (range, cost + bytes * new_cost_per_byte) + }, + ); + + let min = *recorded_heights.iter().min().unwrap(); + let max = *recorded_heights.iter().max().unwrap(); + let recorded_range = *min..=*max; + let recorded_bytes = 500; + + let old_da_gas_price = updater.new_scaled_da_gas_price; + + // when + updater + .update_da_record_data( + recorded_range, + recorded_bytes, + recorded_cost as u128, + &mut unrecorded_blocks, + ) + .unwrap(); + + // then + let new_da_gas_price = updater.new_scaled_da_gas_price; + // because the profit is -10 and the da_p_component is 2, the new da gas price should be greater than the previous one. + assert_eq!(new_da_gas_price, max_da_gas_price); + assert_ne!(old_da_gas_price, new_da_gas_price); +} + +#[test] +fn update_da_record_data__sets_da_gas_price_to_min_da_gas_price_when_max_lt_min() { + // given + let min_da_gas_price = 1; + let max_da_gas_price = 0; + let da_cost_per_byte = 40; + let l2_block_height = 11; + let original_known_total_cost = 150; + let mut unrecorded_blocks: BTreeMap<_, _> = [(11, 3000)].into_iter().collect(); + let da_p_component = 2; + let guessed_cost: u64 = unrecorded_blocks + .values() + .map(|bytes| bytes * da_cost_per_byte) + .sum(); + let projected_total_cost = original_known_total_cost + guessed_cost; + + let mut updater = UpdaterBuilder::new() + .with_da_cost_per_byte(da_cost_per_byte as u128) + .with_da_p_component(da_p_component) + .with_last_profit(-10, 0) + .with_l2_block_height(l2_block_height) + .with_projected_total_cost(projected_total_cost as u128) + .with_known_total_cost(original_known_total_cost as u128) + .with_unrecorded_blocks(&unrecorded_blocks) + .with_min_da_gas_price(min_da_gas_price) + .with_max_da_gas_price(max_da_gas_price) + .build(); + + let new_cost_per_byte = 100; + let (recorded_heights, recorded_cost) = unrecorded_blocks.iter().fold( + (vec![], 0), + |(mut range, cost), (height, bytes)| { + range.push(height); + (range, cost + bytes * new_cost_per_byte) + }, + ); + + let min = *recorded_heights.iter().min().unwrap(); + let max = *recorded_heights.iter().max().unwrap(); + let recorded_range = *min..=*max; + let recorded_bytes = 500; + + // when + updater + .update_da_record_data( + recorded_range, + recorded_bytes, + recorded_cost as u128, + &mut unrecorded_blocks, + ) + .unwrap(); + + // then + let new_da_gas_price = updater.new_scaled_da_gas_price; + + // because max_da_gas_price = 0 and < min_da_gas_price = 1, the new da gas price should be min_da_gas_price + assert_eq!(new_da_gas_price, min_da_gas_price); +} + #[test] fn update_da_record_data__da_block_will_not_change_da_gas_price() { // given 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 f4555551579..994318172d9 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 @@ -24,6 +24,7 @@ fn negative_profit_updater_builder() -> UpdaterBuilder { let starting_da_gas_price = 100; let starting_cost = u128::MAX; let latest_gas_per_byte = i32::MAX; // DA is very expensive + let max_da_gas_price = u64::MAX; let da_p_component = 100; let da_d_component = 10; let last_profit = i128::MIN; @@ -39,6 +40,7 @@ fn negative_profit_updater_builder() -> UpdaterBuilder { .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) + .with_max_da_gas_price(max_da_gas_price) } fn positive_profit_updater_builder() -> UpdaterBuilder { @@ -399,6 +401,7 @@ fn update_l2_block_data__price_does_not_decrease_more_than_max_percent() { let last_profit = i128::MAX; // Large, positive profit to decrease da price let last_last_profit = 0; let max_da_change_percent = 5; + let max_da_gas_price = u64::MAX; let large_starting_reward = i128::MAX; let mut updater = UpdaterBuilder::new() .with_starting_exec_gas_price(starting_exec_gas_price) @@ -411,6 +414,7 @@ fn update_l2_block_data__price_does_not_decrease_more_than_max_percent() { .with_da_cost_per_byte(latest_gas_per_byte as u128) .with_last_profit(last_profit, last_last_profit) .with_da_max_change_percent(max_da_change_percent) + .with_max_da_gas_price(max_da_gas_price) .build(); let unrecorded_blocks = &mut empty_unrecorded_blocks(); @@ -446,6 +450,7 @@ fn update_l2_block_data__da_price_does_not_increase_more_than_max_percent() { let last_profit = i128::MIN; // Large, negative profit to increase da price let last_last_profit = 0; let max_da_change_percent = 5; + let max_da_gas_price = u64::MAX; let large_starting_reward = 0; let unrecorded_blocks = &mut empty_unrecorded_blocks(); let mut updater = UpdaterBuilder::new() @@ -459,6 +464,7 @@ fn update_l2_block_data__da_price_does_not_increase_more_than_max_percent() { .with_da_cost_per_byte(latest_gas_per_byte) .with_last_profit(last_profit, last_last_profit) .with_da_max_change_percent(max_da_change_percent) + .with_max_da_gas_price(max_da_gas_price) .build(); // when @@ -487,6 +493,7 @@ fn update_l2_block_data__never_drops_below_minimum_da_gas_price() { let starting_exec_gas_price = 0; let last_da_gas_price = 100; let min_da_gas_price = 100; + let max_da_gas_price = min_da_gas_price + 1; let starting_cost = 0; let latest_gas_per_byte = 0; // DA is free let da_p_component = 100; @@ -507,6 +514,7 @@ fn update_l2_block_data__never_drops_below_minimum_da_gas_price() { .with_da_cost_per_byte(latest_gas_per_byte as u128) .with_last_profit(last_profit, avg_window) .with_min_da_gas_price(min_da_gas_price) + .with_max_da_gas_price(max_da_gas_price) .build(); // when @@ -534,6 +542,7 @@ fn update_l2_block_data__even_profit_maintains_price() { // given let starting_exec_gas_price = 100; let starting_da_gas_price = 100; + let max_da_gas_price = u64::MAX; let starting_cost = 500; let latest_cost_per_byte = 10; let da_gas_price_denominator = 1; @@ -548,6 +557,7 @@ fn update_l2_block_data__even_profit_maintains_price() { .with_known_total_cost(starting_cost as u128) .with_projected_total_cost(starting_cost as u128) .with_da_cost_per_byte(latest_cost_per_byte as u128) + .with_max_da_gas_price(max_da_gas_price) .build(); // when diff --git a/crates/services/gas_price_service/src/ports.rs b/crates/services/gas_price_service/src/ports.rs index 7670b3e0eab..80ec23e20e4 100644 --- a/crates/services/gas_price_service/src/ports.rs +++ b/crates/services/gas_price_service/src/ports.rs @@ -98,6 +98,7 @@ impl GasPriceServiceConfig { l2_block_fullness_threshold_percent: u8, gas_price_factor: NonZeroU64, min_da_gas_price: u64, + max_da_gas_price: u64, max_da_gas_price_change_percent: u16, da_p_component: i64, da_d_component: i64, @@ -114,6 +115,7 @@ impl GasPriceServiceConfig { l2_block_fullness_threshold_percent, gas_price_factor, min_da_gas_price, + max_da_gas_price, max_da_gas_price_change_percent, da_p_component, da_d_component, diff --git a/crates/services/gas_price_service/src/v1/metadata.rs b/crates/services/gas_price_service/src/v1/metadata.rs index 76f886dde39..f182818eee5 100644 --- a/crates/services/gas_price_service/src/v1/metadata.rs +++ b/crates/services/gas_price_service/src/v1/metadata.rs @@ -67,6 +67,7 @@ pub struct V1AlgorithmConfig { // https://github.com/FuelLabs/fuel-core/issues/2481 pub gas_price_factor: NonZeroU64, pub min_da_gas_price: u64, + pub max_da_gas_price: u64, pub max_da_gas_price_change_percent: u16, pub da_p_component: i64, pub da_d_component: i64, @@ -108,6 +109,7 @@ pub fn updater_from_config(value: &V1AlgorithmConfig) -> AlgorithmUpdaterV1 { .l2_block_fullness_threshold_percent .into(), min_da_gas_price: value.min_da_gas_price, + max_da_gas_price: value.max_da_gas_price, max_da_gas_price_change_percent: value.max_da_gas_price_change_percent, da_p_component: value.da_p_component, da_d_component: value.da_d_component, @@ -166,6 +168,7 @@ pub fn v1_algorithm_from_metadata( .l2_block_fullness_threshold_percent .into(), min_da_gas_price: config.min_da_gas_price, + max_da_gas_price: config.max_da_gas_price, max_da_gas_price_change_percent: config.max_da_gas_price_change_percent, da_p_component: config.da_p_component, da_d_component: config.da_d_component, diff --git a/crates/services/gas_price_service/src/v1/service.rs b/crates/services/gas_price_service/src/v1/service.rs index 87bb46445df..619d55642a4 100644 --- a/crates/services/gas_price_service/src/v1/service.rs +++ b/crates/services/gas_price_service/src/v1/service.rs @@ -556,6 +556,7 @@ mod tests { l2_block_fullness_threshold_percent: 20, gas_price_factor: NonZeroU64::new(10).unwrap(), min_da_gas_price: 10, + max_da_gas_price: 11, max_da_gas_price_change_percent: 20, da_p_component: 4, da_d_component: 2, @@ -635,6 +636,7 @@ mod tests { l2_block_fullness_threshold_percent: 20, gas_price_factor: NonZeroU64::new(10).unwrap(), min_da_gas_price: 0, + max_da_gas_price: 1, max_da_gas_price_change_percent: 100, da_p_component: 4, da_d_component: 2, @@ -714,6 +716,7 @@ mod tests { l2_block_fullness_threshold_percent: 20, gas_price_factor: NonZeroU64::new(10).unwrap(), min_da_gas_price: 0, + max_da_gas_price: 1, max_da_gas_price_change_percent: 100, da_p_component: 4, da_d_component: 2, diff --git a/crates/services/gas_price_service/src/v1/tests.rs b/crates/services/gas_price_service/src/v1/tests.rs index fd29fb9e1b1..c8024db81cd 100644 --- a/crates/services/gas_price_service/src/v1/tests.rs +++ b/crates/services/gas_price_service/src/v1/tests.rs @@ -293,6 +293,7 @@ fn zero_threshold_arbitrary_config() -> V1AlgorithmConfig { l2_block_fullness_threshold_percent: 0, gas_price_factor: NonZeroU64::new(100).unwrap(), min_da_gas_price: 0, + max_da_gas_price: 1, max_da_gas_price_change_percent: 0, da_p_component: 0, da_d_component: 0, @@ -327,6 +328,7 @@ fn different_arb_config() -> V1AlgorithmConfig { l2_block_fullness_threshold_percent: 0, gas_price_factor: NonZeroU64::new(100).unwrap(), min_da_gas_price: 0, + max_da_gas_price: 1, max_da_gas_price_change_percent: 0, da_p_component: 0, da_d_component: 0, diff --git a/tests/tests/gas_price.rs b/tests/tests/gas_price.rs index a09586d055f..5908bb7c270 100644 --- a/tests/tests/gas_price.rs +++ b/tests/tests/gas_price.rs @@ -208,6 +208,7 @@ async fn produce_block__raises_gas_price() { node_config.da_d_component = 0; node_config.max_da_gas_price_change_percent = 0; node_config.min_da_gas_price = 0; + node_config.max_da_gas_price = 1; let srv = FuelService::new_node(node_config.clone()).await.unwrap(); let client = FuelClient::from(srv.bound_address); @@ -257,6 +258,7 @@ async fn produce_block__lowers_gas_price() { node_config.da_d_component = 0; node_config.max_da_gas_price_change_percent = 0; node_config.min_da_gas_price = 0; + node_config.max_da_gas_price = 1; let srv = FuelService::new_node(node_config.clone()).await.unwrap(); let client = FuelClient::from(srv.bound_address); @@ -705,6 +707,7 @@ fn node_config_with_da_committer_url(url: &str) -> Config { let starting_gas_price = 10_000_000; node_config.block_producer.coinbase_recipient = Some([5; 32].into()); node_config.min_da_gas_price = starting_gas_price; + node_config.max_da_gas_price = u64::MAX; node_config.max_da_gas_price_change_percent = 15; node_config.block_production = Trigger::Never; node_config.da_committer_url = Some(url.to_string());