diff --git a/Cargo.lock b/Cargo.lock index a8059c8f5d0..53b5ec71041 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5070,6 +5070,7 @@ dependencies = [ name = "fuel-gas-price-algorithm" version = "0.47.1" dependencies = [ + "fuel-core-types 0.47.1", "proptest", "rand 0.8.5", "serde", diff --git a/bin/fuel-core/src/cli/run.rs b/bin/fuel-core/src/cli/run.rs index 3775610489f..86690b6d19d 100644 --- a/bin/fuel-core/src/cli/run.rs +++ b/bin/fuel-core/src/cli/run.rs @@ -757,7 +757,9 @@ impl Command { heavy_work: pool_heavy_work_config, service_channel_limits, pending_pool_tx_ttl: tx_pending_pool_ttl.into(), - max_pending_pool_size_percentage: tx_pending_pool_size_percentage, + max_pending_pool_size_percentage: fuel_core_types::ClampedPercentage::new( + tx_pending_pool_size_percentage.min(100) as u8, + ), metrics: metrics.is_enabled(Module::TxPool), }, block_producer: ProducerConfig { 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 26916d741d5..a2f74297f47 100644 --- a/crates/fuel-core/src/service/adapters/gas_price_adapters.rs +++ b/crates/fuel-core/src/service/adapters/gas_price_adapters.rs @@ -24,6 +24,7 @@ use fuel_core_storage::{ transactional::HistoricalView, }; use fuel_core_types::{ + ClampedPercentage, blockchain::{ block::Block, header::ConsensusParametersVersion, @@ -91,7 +92,9 @@ impl From for V1AlgorithmConfig { new_exec_gas_price: starting_exec_gas_price.max(min_exec_gas_price), min_exec_gas_price, exec_gas_price_change_percent, - l2_block_fullness_threshold_percent: exec_gas_price_threshold_percent, + l2_block_fullness_threshold_percent: ClampedPercentage::new( + exec_gas_price_threshold_percent, + ), min_da_gas_price, max_da_gas_price, max_da_gas_price_change_percent, @@ -100,7 +103,7 @@ impl From for V1AlgorithmConfig { normal_range_size: activity_normal_range_size, capped_range_size: activity_capped_range_size, decrease_range_size: activity_decrease_range_size, - block_activity_threshold, + block_activity_threshold: ClampedPercentage::new(block_activity_threshold), da_poll_interval, gas_price_factor: da_gas_price_factor, starting_recorded_height: starting_recorded_height.map(BlockHeight::from), diff --git a/crates/fuel-gas-price-algorithm/Cargo.toml b/crates/fuel-gas-price-algorithm/Cargo.toml index a1928ce7d63..30acd9fe858 100644 --- a/crates/fuel-gas-price-algorithm/Cargo.toml +++ b/crates/fuel-gas-price-algorithm/Cargo.toml @@ -16,6 +16,7 @@ name = "fuel_gas_price_algorithm" path = "src/lib.rs" [dependencies] +fuel-core-types = { path = "../types", features = ["serde"] } serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } tracing = { workspace = true } diff --git a/crates/fuel-gas-price-algorithm/src/v1.rs b/crates/fuel-gas-price-algorithm/src/v1.rs index 7d76ddb70a4..7b47f629915 100644 --- a/crates/fuel-gas-price-algorithm/src/v1.rs +++ b/crates/fuel-gas-price-algorithm/src/v1.rs @@ -1,4 +1,5 @@ use crate::utils::cumulative_percentage_change; +use fuel_core_types::ClampedPercentage; use std::{ cmp::{ max, @@ -321,36 +322,6 @@ impl L2ActivityTracker { } } -/// A value that represents a value between 0 and 100. Higher values are clamped to 100 -#[derive( - serde::Serialize, serde::Deserialize, Debug, Copy, Clone, PartialEq, PartialOrd, -)] -pub struct ClampedPercentage { - value: u8, -} - -impl ClampedPercentage { - pub fn new(maybe_value: u8) -> Self { - Self { - value: maybe_value.min(100), - } - } -} - -impl From for ClampedPercentage { - fn from(value: u8) -> Self { - Self::new(value) - } -} - -impl core::ops::Deref for ClampedPercentage { - type Target = u8; - - fn deref(&self) -> &Self::Target { - &self.value - } -} - impl AlgorithmUpdaterV1 { pub fn update_da_record_data( &mut self, diff --git a/crates/services/gas_price_service/src/v1/metadata.rs b/crates/services/gas_price_service/src/v1/metadata.rs index c583b694e87..4e5b3133b1e 100644 --- a/crates/services/gas_price_service/src/v1/metadata.rs +++ b/crates/services/gas_price_service/src/v1/metadata.rs @@ -1,5 +1,8 @@ use crate::v0::metadata::V0Metadata; -use fuel_core_types::fuel_types::BlockHeight; +use fuel_core_types::{ + ClampedPercentage, + fuel_types::BlockHeight, +}; use fuel_gas_price_algorithm::v1::{ AlgorithmUpdaterV1, L2ActivityTracker, @@ -78,7 +81,7 @@ pub struct V1AlgorithmConfig { pub new_exec_gas_price: u64, pub min_exec_gas_price: u64, pub exec_gas_price_change_percent: u16, - pub l2_block_fullness_threshold_percent: u8, + pub l2_block_fullness_threshold_percent: ClampedPercentage, // TODO:We don't need this after we implement // https://github.com/FuelLabs/fuel-core/issues/2481 pub gas_price_factor: NonZeroU64, @@ -90,7 +93,7 @@ pub struct V1AlgorithmConfig { pub normal_range_size: u16, pub capped_range_size: u16, pub decrease_range_size: u16, - pub block_activity_threshold: u8, + pub block_activity_threshold: ClampedPercentage, /// The interval at which the `DaSourceService` polls for new data pub da_poll_interval: Option, pub starting_recorded_height: Option, @@ -105,7 +108,7 @@ pub fn updater_from_config( value.normal_range_size, value.capped_range_size, value.decrease_range_size, - value.block_activity_threshold.into(), + value.block_activity_threshold, ); let unrecorded_blocks_bytes = 0; AlgorithmUpdaterV1 { @@ -126,9 +129,7 @@ pub fn updater_from_config( l2_activity, min_exec_gas_price: value.min_exec_gas_price, exec_gas_price_change_percent: value.exec_gas_price_change_percent, - l2_block_fullness_threshold_percent: value - .l2_block_fullness_threshold_percent - .into(), + l2_block_fullness_threshold_percent: value.l2_block_fullness_threshold_percent, 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, @@ -163,7 +164,7 @@ pub fn v1_algorithm_from_metadata( config.normal_range_size, config.capped_range_size, config.decrease_range_size, - config.block_activity_threshold.into(), + config.block_activity_threshold, ); let unrecorded_blocks_bytes: u128 = metadata.unrecorded_block_bytes; let projected_portion = @@ -185,9 +186,7 @@ pub fn v1_algorithm_from_metadata( l2_activity, min_exec_gas_price: config.min_exec_gas_price, exec_gas_price_change_percent: config.exec_gas_price_change_percent, - l2_block_fullness_threshold_percent: config - .l2_block_fullness_threshold_percent - .into(), + l2_block_fullness_threshold_percent: config.l2_block_fullness_threshold_percent, 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, diff --git a/crates/services/gas_price_service/src/v1/service.rs b/crates/services/gas_price_service/src/v1/service.rs index 24c96709ca0..29a25121a3b 100644 --- a/crates/services/gas_price_service/src/v1/service.rs +++ b/crates/services/gas_price_service/src/v1/service.rs @@ -713,7 +713,9 @@ mod tests { new_exec_gas_price: 100, min_exec_gas_price: 50, exec_gas_price_change_percent: 20, - l2_block_fullness_threshold_percent: 20, + l2_block_fullness_threshold_percent: fuel_core_types::ClampedPercentage::new( + 20, + ), gas_price_factor: NonZeroU64::new(10).unwrap(), min_da_gas_price: 10, max_da_gas_price: 11, @@ -723,7 +725,7 @@ mod tests { normal_range_size: 10, capped_range_size: 100, decrease_range_size: 4, - block_activity_threshold: 20, + block_activity_threshold: fuel_core_types::ClampedPercentage::new(20), da_poll_interval: None, starting_recorded_height: None, record_metrics: false, @@ -807,7 +809,9 @@ mod tests { new_exec_gas_price: 100, min_exec_gas_price: 50, exec_gas_price_change_percent: 0, - l2_block_fullness_threshold_percent: 20, + l2_block_fullness_threshold_percent: fuel_core_types::ClampedPercentage::new( + 20, + ), gas_price_factor: NonZeroU64::new(10).unwrap(), min_da_gas_price: 0, max_da_gas_price: 1, @@ -817,7 +821,7 @@ mod tests { normal_range_size: 10, capped_range_size: 100, decrease_range_size: 4, - block_activity_threshold: 20, + block_activity_threshold: fuel_core_types::ClampedPercentage::new(20), da_poll_interval: None, starting_recorded_height: None, record_metrics: false, @@ -896,7 +900,9 @@ mod tests { new_exec_gas_price: 100, min_exec_gas_price: 50, exec_gas_price_change_percent: 0, - l2_block_fullness_threshold_percent: 20, + l2_block_fullness_threshold_percent: fuel_core_types::ClampedPercentage::new( + 20, + ), gas_price_factor: NonZeroU64::new(10).unwrap(), min_da_gas_price: 0, max_da_gas_price: 1, @@ -906,7 +912,7 @@ mod tests { normal_range_size: 10, capped_range_size: 100, decrease_range_size: 4, - block_activity_threshold: 20, + block_activity_threshold: fuel_core_types::ClampedPercentage::new(20), da_poll_interval: None, starting_recorded_height: None, record_metrics: false, @@ -1108,7 +1114,9 @@ mod tests { new_exec_gas_price: 100, min_exec_gas_price: 50, exec_gas_price_change_percent: 20, - l2_block_fullness_threshold_percent: 20, + l2_block_fullness_threshold_percent: fuel_core_types::ClampedPercentage::new( + 20, + ), gas_price_factor: NonZeroU64::new(10).unwrap(), min_da_gas_price: 10, max_da_gas_price: 11, @@ -1118,7 +1126,7 @@ mod tests { normal_range_size: 10, capped_range_size: 100, decrease_range_size: 4, - block_activity_threshold: 20, + block_activity_threshold: fuel_core_types::ClampedPercentage::new(20), da_poll_interval: None, starting_recorded_height: None, record_metrics: false, diff --git a/crates/services/gas_price_service/src/v1/tests.rs b/crates/services/gas_price_service/src/v1/tests.rs index 32be5a83def..37b39d773d7 100644 --- a/crates/services/gas_price_service/src/v1/tests.rs +++ b/crates/services/gas_price_service/src/v1/tests.rs @@ -77,6 +77,7 @@ use fuel_core_storage::{ }, }; use fuel_core_types::{ + ClampedPercentage, blockchain::{ block::Block, header::ConsensusParametersVersion, @@ -290,7 +291,7 @@ fn zero_threshold_arbitrary_config() -> V1AlgorithmConfig { new_exec_gas_price: 100, min_exec_gas_price: 0, exec_gas_price_change_percent: 10, - l2_block_fullness_threshold_percent: 0, + l2_block_fullness_threshold_percent: ClampedPercentage::new(0), gas_price_factor: NonZeroU64::new(100).unwrap(), min_da_gas_price: 0, max_da_gas_price: 1, @@ -300,7 +301,7 @@ fn zero_threshold_arbitrary_config() -> V1AlgorithmConfig { normal_range_size: 0, capped_range_size: 0, decrease_range_size: 0, - block_activity_threshold: 0, + block_activity_threshold: ClampedPercentage::new(0), da_poll_interval: None, starting_recorded_height: None, record_metrics: false, @@ -327,7 +328,7 @@ fn different_arb_config() -> V1AlgorithmConfig { new_exec_gas_price: 200, min_exec_gas_price: 0, exec_gas_price_change_percent: 20, - l2_block_fullness_threshold_percent: 0, + l2_block_fullness_threshold_percent: ClampedPercentage::new(0), gas_price_factor: NonZeroU64::new(100).unwrap(), min_da_gas_price: 0, max_da_gas_price: 1, @@ -337,7 +338,7 @@ fn different_arb_config() -> V1AlgorithmConfig { normal_range_size: 0, capped_range_size: 0, decrease_range_size: 0, - block_activity_threshold: 0, + block_activity_threshold: ClampedPercentage::new(0), da_poll_interval: None, starting_recorded_height: None, record_metrics: false, @@ -816,7 +817,7 @@ fn algo_updater_override_values_match( ); assert_eq!( algo_updater.l2_block_fullness_threshold_percent, - config.l2_block_fullness_threshold_percent.into() + config.l2_block_fullness_threshold_percent ); assert_eq!(algo_updater.gas_price_factor, config.gas_price_factor); assert_eq!(algo_updater.min_da_gas_price, config.min_da_gas_price); diff --git a/crates/services/txpool_v2/src/config.rs b/crates/services/txpool_v2/src/config.rs index bdeb831e556..0caded9dbf0 100644 --- a/crates/services/txpool_v2/src/config.rs +++ b/crates/services/txpool_v2/src/config.rs @@ -4,6 +4,7 @@ use std::{ }; use fuel_core_types::{ + ClampedPercentage, fuel_tx::{ Address, ContractId, @@ -143,7 +144,7 @@ pub struct Config { /// TTL for transactions inside the pending pool. pub pending_pool_tx_ttl: Duration, /// Maximum percentage of the pool size to be used for the pending pool. - pub max_pending_pool_size_percentage: u16, + pub max_pending_pool_size_percentage: ClampedPercentage, /// Enable metrics when set to true pub metrics: bool, } @@ -206,7 +207,7 @@ impl Default for Config { max_pending_read_pool_requests: 1000, }, pending_pool_tx_ttl: Duration::from_secs(3), - max_pending_pool_size_percentage: 50, + max_pending_pool_size_percentage: ClampedPercentage::new(50), metrics: false, } } diff --git a/crates/services/txpool_v2/src/pool_worker.rs b/crates/services/txpool_v2/src/pool_worker.rs index a50de4c507b..40e82edd96e 100644 --- a/crates/services/txpool_v2/src/pool_worker.rs +++ b/crates/services/txpool_v2/src/pool_worker.rs @@ -624,21 +624,21 @@ where .config .pool_limits .max_gas - .saturating_mul(self.pool.config.max_pending_pool_size_percentage as u64) + .saturating_mul(*self.pool.config.max_pending_pool_size_percentage as u64) .saturating_div(100); let max_bytes = self .pool .config .pool_limits .max_bytes_size - .saturating_mul(self.pool.config.max_pending_pool_size_percentage as usize) + .saturating_mul(*self.pool.config.max_pending_pool_size_percentage as usize) .saturating_div(100); let max_txs = self .pool .config .pool_limits .max_txs - .saturating_mul(self.pool.config.max_pending_pool_size_percentage as usize) + .saturating_mul(*self.pool.config.max_pending_pool_size_percentage as usize) .saturating_div(100); if gas_used > max_gas || bytes_used > max_bytes || txs_used > max_txs { diff --git a/crates/services/txpool_v2/src/tests/tests_pending_pool.rs b/crates/services/txpool_v2/src/tests/tests_pending_pool.rs index 097cc0ebd5e..413f8b05c93 100644 --- a/crates/services/txpool_v2/src/tests/tests_pending_pool.rs +++ b/crates/services/txpool_v2/src/tests/tests_pending_pool.rs @@ -1,5 +1,6 @@ use fuel_core_services::Service; use fuel_core_types::{ + ClampedPercentage, fuel_tx::{ UniqueIdentifier, UtxoId, @@ -94,7 +95,7 @@ async fn test_tx__return_error_expired() { async fn test_tx__directly_removed_not_enough_space() { let mut universe = TestPoolUniverse::default(); universe.config.utxo_validation = true; - universe.config.max_pending_pool_size_percentage = 1; + universe.config.max_pending_pool_size_percentage = ClampedPercentage::new(1); universe.config.pool_limits.max_txs = 1; let (_, unset_input) = universe.create_output_and_input(); diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 6d763e42112..4f21be1a086 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -32,9 +32,12 @@ pub use tai64; pub mod blockchain; pub mod entities; +pub mod primitives; pub mod services; pub mod signer; +pub use primitives::ClampedPercentage; + /// Re-export of some fuel-vm types pub mod fuel_vm { #[doc(no_inline)] diff --git a/crates/types/src/primitives.rs b/crates/types/src/primitives.rs new file mode 100644 index 00000000000..d206bfa45ab --- /dev/null +++ b/crates/types/src/primitives.rs @@ -0,0 +1,162 @@ +//! Common primitive types used across fuel-core. + +/// A value that represents a value between 0 and 100. Higher values are clamped to 100 +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +pub struct ClampedPercentage { + value: u8, +} + +impl ClampedPercentage { + /// Creates a new `ClampedPercentage` from a `u8` value. + /// Values greater than 100 are clamped to 100. + pub fn new(maybe_value: u8) -> Self { + Self { + value: maybe_value.min(100), + } + } +} + +impl From for ClampedPercentage { + fn from(value: u8) -> Self { + Self::new(value) + } +} + +impl core::ops::Deref for ClampedPercentage { + type Target = u8; + + fn deref(&self) -> &Self::Target { + &self.value + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for ClampedPercentage { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + // Deserialize the struct, but ensure the value is clamped + // We use a temporary struct to deserialize, then reconstruct with clamping + #[derive(serde::Deserialize)] + struct ClampedPercentageHelper { + value: u8, + } + + let helper = ClampedPercentageHelper::deserialize(deserializer)?; + Ok(Self::new(helper.value)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new_clamps_values_greater_than_100() { + assert_eq!(ClampedPercentage::new(101).value, 100); + assert_eq!(ClampedPercentage::new(150).value, 100); + assert_eq!(ClampedPercentage::new(255).value, 100); + } + + #[test] + fn new_preserves_values_less_than_or_equal_to_100() { + assert_eq!(ClampedPercentage::new(0).value, 0); + assert_eq!(ClampedPercentage::new(50).value, 50); + assert_eq!(ClampedPercentage::new(100).value, 100); + } + + #[test] + fn from_u8_works_correctly() { + assert_eq!(ClampedPercentage::from(0).value, 0); + assert_eq!(ClampedPercentage::from(50).value, 50); + assert_eq!(ClampedPercentage::from(100).value, 100); + assert_eq!(ClampedPercentage::from(101).value, 100); + assert_eq!(ClampedPercentage::from(255).value, 100); + } + + #[test] + fn deref_works_correctly() { + let percentage = ClampedPercentage::new(50); + assert_eq!(*percentage, 50); + assert_eq!(percentage.value, 50); + } + + #[test] + fn default_is_zero() { + let default = ClampedPercentage::default(); + assert_eq!(default.value, 0); + assert_eq!(*default, 0); + } + + #[test] + fn partial_eq_works() { + assert_eq!(ClampedPercentage::new(50), ClampedPercentage::new(50)); + assert_ne!(ClampedPercentage::new(50), ClampedPercentage::new(51)); + } + + #[test] + fn partial_ord_works() { + assert!(ClampedPercentage::new(50) < ClampedPercentage::new(51)); + assert!(ClampedPercentage::new(100) > ClampedPercentage::new(50)); + assert!(ClampedPercentage::new(50) <= ClampedPercentage::new(50)); + assert!(ClampedPercentage::new(50) >= ClampedPercentage::new(50)); + } + + #[test] + fn clone_and_copy_work() { + let original = ClampedPercentage::new(75); + let cloned = original; + let copied = original.clone(); + assert_eq!(original, cloned); + assert_eq!(original, copied); + } + + #[cfg(feature = "serde")] + #[test] + fn serde_round_trip_with_postcard() { + use postcard; + + let percentage = ClampedPercentage::new(50); + let serialized = postcard::to_allocvec(&percentage).unwrap(); + let deserialized: ClampedPercentage = postcard::from_bytes(&serialized).unwrap(); + assert_eq!(percentage, deserialized); + } + + #[cfg(feature = "serde")] + #[test] + fn serde_clamps_on_deserialize() { + use postcard; + + // Test that deserializing a struct with value > 100 gets clamped + // We create a helper struct with value > 100, serialize it, + // then deserialize as ClampedPercentage to ensure clamping works + #[derive(serde::Serialize)] + struct TestHelper { + value: u8, + } + + let helper = TestHelper { value: 150 }; + let serialized = postcard::to_allocvec(&helper).unwrap(); + + // Deserialize should clamp the value + let deserialized: ClampedPercentage = postcard::from_bytes(&serialized).unwrap(); + assert_eq!(deserialized.value, 100, "Deserialized value > 100 should be clamped to 100"); + } + + #[cfg(feature = "serde")] + #[test] + fn serde_preserves_clamping() { + use postcard; + + // Create a percentage with value > 100 (should be clamped) + let percentage = ClampedPercentage::new(150); + assert_eq!(percentage.value, 100); + + // Serialize and deserialize + let serialized = postcard::to_allocvec(&percentage).unwrap(); + let deserialized: ClampedPercentage = postcard::from_bytes(&serialized).unwrap(); + assert_eq!(deserialized.value, 100); + } +}