diff --git a/CHANGELOG.md b/CHANGELOG.md index 07e2a1aa04b..87c510b898c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - [2511](https://github.com/FuelLabs/fuel-core/pull/2511): Fix backward compatibility of V0Metadata in gas price db. ### Changed +- [2501](https://github.com/FuelLabs/fuel-core/pull/2501): Use gas price from block for estimating future gas prices - [2468](https://github.com/FuelLabs/fuel-core/pull/2468): Abstract unrecorded blocks concept for V1 algorithm, create new storage impl. Introduce `TransactionableStorage` trait to allow atomic changes to the storage. - [2295](https://github.com/FuelLabs/fuel-core/pull/2295): `CombinedDb::from_config` now respects `state_rewind_policy` with tmp RocksDB. - [2378](https://github.com/FuelLabs/fuel-core/pull/2378): Use cached hash of the topic instead of calculating it on each publishing gossip message. diff --git a/crates/fuel-core/proptest-regressions/service/adapters.txt b/crates/fuel-core/proptest-regressions/service/adapters.txt new file mode 100644 index 00000000000..b83038b4a5f --- /dev/null +++ b/crates/fuel-core/proptest-regressions/service/adapters.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 87eb4d9c7c90c2a0ee11edef3ee1b01303db0a6ba1fd34d7fc6146a2c36387f4 # shrinks to gas_price = 1, starting_height = 0, block_horizon = 1, percentage = 100 diff --git a/crates/fuel-core/src/service/adapters.rs b/crates/fuel-core/src/service/adapters.rs index f55d4572335..0f9b55cb370 100644 --- a/crates/fuel-core/src/service/adapters.rs +++ b/crates/fuel-core/src/service/adapters.rs @@ -1,8 +1,23 @@ +use crate::{ + database::{ + database_description::relayer::Relayer, + Database, + }, + fuel_core_graphql_api::ports::GasPriceEstimate, + service::{ + sub_services::{ + BlockProducerService, + TxPoolSharedState, + }, + vm_pool::MemoryPool, + }, +}; use fuel_core_consensus_module::{ block_verifier::Verifier, RelayerConsensusConfig, }; use fuel_core_executor::executor::OnceTransactionsSource; +use fuel_core_gas_price_service::v1::service::LatestGasPrice; use fuel_core_importer::ImporterResult; use fuel_core_poa::{ ports::BlockSigner, @@ -19,6 +34,7 @@ use fuel_core_types::{ consensus::Consensus, }, fuel_tx::Transaction, + fuel_types::BlockHeight, services::{ block_importer::SharedImportResult, block_producer::Components, @@ -32,20 +48,6 @@ use fuel_core_types::{ use fuel_core_upgradable_executor::executor::Executor; use std::sync::Arc; -use crate::{ - database::{ - database_description::relayer::Relayer, - Database, - }, - service::{ - sub_services::{ - BlockProducerService, - TxPoolSharedState, - }, - vm_pool::MemoryPool, - }, -}; - pub mod block_importer; pub mod consensus_module; pub mod consensus_parameters_provider; @@ -85,6 +87,162 @@ impl StaticGasPrice { } } +#[cfg(test)] +mod arc_gas_price_estimate_tests { + #![allow(non_snake_case)] + + use super::*; + use proptest::proptest; + + async fn _worst_case__correctly_calculates_value( + gas_price: u64, + starting_height: u32, + block_horizon: u32, + percentage: u16, + ) { + // given + let subject = ArcGasPriceEstimate::new(starting_height, gas_price, percentage); + + // when + let target_height = starting_height.saturating_add(block_horizon); + let estimated = subject + .worst_case_gas_price(target_height.into()) + .await + .unwrap(); + + // then + let mut actual = gas_price; + + for _ in 0..block_horizon { + let change_amount = + actual.saturating_mul(percentage as u64).saturating_div(100); + actual = actual.saturating_add(change_amount); + } + + assert!(estimated >= actual); + } + + proptest! { + #[test] + fn worst_case_gas_price__correctly_calculates_value( + gas_price: u64, + starting_height: u32, + block_horizon in 0..10_000u32, + percentage: u16, + ) { + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(_worst_case__correctly_calculates_value( + gas_price, + starting_height, + block_horizon, + percentage, + )); + } + } + + proptest! { + #[test] + fn worst_case_gas_price__never_overflows( + gas_price: u64, + starting_height: u32, + block_horizon in 0..10_000u32, + percentage: u16 + ) { + let rt = tokio::runtime::Runtime::new().unwrap(); + + // given + let subject = ArcGasPriceEstimate::new(starting_height, gas_price, percentage); + + // when + let target_height = starting_height.saturating_add(block_horizon); + + let _ = rt.block_on(subject.worst_case_gas_price(target_height.into())); + + // then + // doesn't panic with an overflow + } + } +} + +/// Allows communication from other service with more recent gas price data +/// `Height` refers to the height of the block at which the gas price was last updated +/// `GasPrice` refers to the gas price at the last updated block +#[allow(dead_code)] +pub struct ArcGasPriceEstimate { + /// Shared state of latest gas price data + latest_gas_price: LatestGasPrice, + /// The max percentage the gas price can increase per block + percentage: u16, +} + +impl ArcGasPriceEstimate { + #[cfg(test)] + pub fn new(height: Height, price: GasPrice, percentage: u16) -> Self { + let latest_gas_price = LatestGasPrice::new(height, price); + Self { + latest_gas_price, + percentage, + } + } + + pub fn new_from_inner( + inner: LatestGasPrice, + percentage: u16, + ) -> Self { + Self { + latest_gas_price: inner, + percentage, + } + } +} + +impl ArcGasPriceEstimate { + fn get_height_and_gas_price(&self) -> (Height, GasPrice) { + self.latest_gas_price.get() + } +} + +#[async_trait::async_trait] +impl GasPriceEstimate for ArcGasPriceEstimate { + async fn worst_case_gas_price(&self, height: BlockHeight) -> Option { + let (best_height, best_gas_price) = self.get_height_and_gas_price(); + let percentage = self.percentage; + + let worst = cumulative_percentage_change( + best_gas_price, + best_height, + percentage as u64, + height.into(), + ); + Some(worst) + } +} + +#[allow(clippy::cast_possible_truncation)] +pub(crate) fn cumulative_percentage_change( + start_gas_price: u64, + best_height: u32, + percentage: u64, + target_height: u32, +) -> u64 { + let blocks = target_height.saturating_sub(best_height) as f64; + let percentage_as_decimal = percentage as f64 / 100.0; + let multiple = (1.0f64 + percentage_as_decimal).powf(blocks); + let mut approx = start_gas_price as f64 * multiple; + // Account for rounding errors and take a slightly higher value + // Around the `ROUNDING_ERROR_CUTOFF` the rounding errors will cause the estimate to be too low. + // We increase by `ROUNDING_ERROR_COMPENSATION` to account for this. + // This is an unlikely situation in practice, but we want to guarantee that the actual + // gas price is always equal or less than the estimate given here + const ROUNDING_ERROR_CUTOFF: f64 = 16948547188989277.0; + if approx > ROUNDING_ERROR_CUTOFF { + const ROUNDING_ERROR_COMPENSATION: f64 = 2000.0; + approx += ROUNDING_ERROR_COMPENSATION; + } + // `f64` over `u64::MAX` are cast to `u64::MAX` + approx.ceil() as u64 +} + #[derive(Clone)] pub struct PoAAdapter { shared_state: Option, diff --git a/crates/services/gas_price_service/src/common/fuel_core_storage_adapter.rs b/crates/services/gas_price_service/src/common/fuel_core_storage_adapter.rs index 74bec1207d2..506885fe78b 100644 --- a/crates/services/gas_price_service/src/common/fuel_core_storage_adapter.rs +++ b/crates/services/gas_price_service/src/common/fuel_core_storage_adapter.rs @@ -178,6 +178,7 @@ pub fn get_block_info( block_gas_capacity: block_gas_limit, block_bytes: Postcard::encode(block).len() as u64, block_fees: fee, + gas_price, }; Ok(info) } diff --git a/crates/services/gas_price_service/src/common/utils.rs b/crates/services/gas_price_service/src/common/utils.rs index b57fc2608a2..5c944bd9fb1 100644 --- a/crates/services/gas_price_service/src/common/utils.rs +++ b/crates/services/gas_price_service/src/common/utils.rs @@ -42,5 +42,7 @@ pub enum BlockInfo { block_bytes: u64, // The fees the block has collected block_fees: u64, + // The gas price used in the block + gas_price: u64, }, } diff --git a/crates/services/gas_price_service/src/v0/service.rs b/crates/services/gas_price_service/src/v0/service.rs index c4237b1141e..e0a794c0da9 100644 --- a/crates/services/gas_price_service/src/v0/service.rs +++ b/crates/services/gas_price_service/src/v0/service.rs @@ -250,6 +250,7 @@ mod tests { block_gas_capacity: 100, block_bytes: 100, block_fees: 100, + gas_price: 100, }; let (l2_block_sender, l2_block_receiver) = mpsc::channel(1); diff --git a/crates/services/gas_price_service/src/v0/tests.rs b/crates/services/gas_price_service/src/v0/tests.rs index 492016d37eb..85aeb4fe36a 100644 --- a/crates/services/gas_price_service/src/v0/tests.rs +++ b/crates/services/gas_price_service/src/v0/tests.rs @@ -157,6 +157,7 @@ async fn next_gas_price__affected_by_new_l2_block() { block_gas_capacity: 100, block_bytes: 100, block_fees: 100, + gas_price: 100, }; let (l2_block_sender, l2_block_receiver) = tokio::sync::mpsc::channel(1); let l2_block_source = FakeL2BlockSource { @@ -200,6 +201,7 @@ async fn next__new_l2_block_saves_old_metadata() { block_gas_capacity: 100, block_bytes: 100, block_fees: 100, + gas_price: 100, }; let (l2_block_sender, l2_block_receiver) = tokio::sync::mpsc::channel(1); let l2_block_source = FakeL2BlockSource { diff --git a/crates/services/gas_price_service/src/v1/service.rs b/crates/services/gas_price_service/src/v1/service.rs index df0c91a5c93..0e9b4c2f741 100644 --- a/crates/services/gas_price_service/src/v1/service.rs +++ b/crates/services/gas_price_service/src/v1/service.rs @@ -1,5 +1,3 @@ -use std::num::NonZeroU64; - use crate::{ common::{ gas_price_algorithm::SharedGasPriceAlgo, @@ -58,12 +56,41 @@ use fuel_gas_price_algorithm::{ }, }; use futures::FutureExt; +use std::{ + num::NonZeroU64, + sync::Arc, +}; use tokio::sync::broadcast::Receiver; +#[derive(Debug, Clone)] +pub struct LatestGasPrice { + inner: Arc>, +} + +impl LatestGasPrice { + pub fn new(height: Height, price: GasPrice) -> Self { + let pair = (height, price); + let inner = Arc::new(parking_lot::RwLock::new(pair)); + Self { inner } + } + + pub fn set(&mut self, height: Height, price: GasPrice) { + *self.inner.write() = (height, price); + } +} + +impl LatestGasPrice { + pub fn get(&self) -> (Height, GasPrice) { + *self.inner.read() + } +} + /// The service that updates the gas price algorithm. pub struct GasPriceServiceV1 { /// The algorithm that can be used in the next block shared_algo: SharedV1Algorithm, + /// The latest gas price + latest_gas_price: LatestGasPrice, /// The L2 block source l2_block_source: L2, /// The algorithm updater @@ -78,6 +105,21 @@ pub struct GasPriceServiceV1 { storage_tx_provider: StorageTxProvider, } +impl GasPriceServiceV1 { + pub(crate) fn update_latest_gas_price(&mut self, block_info: &BlockInfo) { + match block_info { + BlockInfo::GenesisBlock => { + // do nothing + } + BlockInfo::Block { + height, gas_price, .. + } => { + self.latest_gas_price.set(*height, *gas_price); + } + } + } +} + impl GasPriceServiceV1 where L2: L2BlockSource, @@ -91,6 +133,7 @@ where tracing::info!("Received L2 block result: {:?}", l2_block_res); let block = l2_block_res?; + self.update_latest_gas_price(&block); tracing::debug!("Updating gas price algorithm"); self.apply_block_info_to_gas_algorithm(block).await?; Ok(()) @@ -105,6 +148,7 @@ where pub fn new( l2_block_source: L2, shared_algo: SharedV1Algorithm, + latest_gas_price: LatestGasPrice, algorithm_updater: AlgorithmUpdaterV1, da_source_adapter_handle: DaSourceService, storage_tx_provider: AtomicStorage, @@ -113,6 +157,7 @@ where da_source_adapter_handle.shared_data().clone().subscribe(); Self { shared_algo, + latest_gas_price, l2_block_source, algorithm_updater, da_source_adapter_handle, @@ -218,6 +263,7 @@ where block_gas_capacity, block_bytes, block_fees, + .. } => { self.handle_normal_block( height, @@ -344,6 +390,7 @@ mod tests { }; use fuel_core_storage::{ structured_storage::test::InMemoryStorage, + tables::merkle::DenseMetadataKey::Latest, transactional::{ IntoTransaction, StorageTransaction, @@ -386,6 +433,7 @@ mod tests { service::{ initialize_algorithm, GasPriceServiceV1, + LatestGasPrice, }, uninitialized_task::fuel_storage_unrecorded_blocks::FuelStorageUnrecordedBlocks, }, @@ -445,6 +493,7 @@ mod tests { block_gas_capacity: 100, block_bytes: 100, block_fees: 100, + gas_price: 100, }; let (l2_block_sender, l2_block_receiver) = mpsc::channel(1); @@ -481,10 +530,12 @@ mod tests { ), None, ); + let latest_gas_price = LatestGasPrice::new(0, 0); let mut service = GasPriceServiceV1::new( l2_block_source, shared_algo, + latest_gas_price, algo_updater, dummy_da_source, inner, @@ -513,6 +564,7 @@ mod tests { block_gas_capacity: 100, block_bytes: 100, block_fees: 100, + gas_price: 100, }; let (l2_block_sender, l2_block_receiver) = mpsc::channel(1); @@ -564,10 +616,12 @@ mod tests { Some(Duration::from_millis(1)), ); let mut watcher = StateWatcher::started(); + let latest_gas_price = LatestGasPrice::new(0, 0); let mut service = GasPriceServiceV1::new( l2_block_source, shared_algo, + latest_gas_price, algo_updater, da_source, inner, @@ -627,6 +681,7 @@ mod tests { block_gas_capacity: 100, block_bytes: 100, block_fees: 100, + gas_price: 100, }; let (l2_block_sender, l2_block_receiver) = mpsc::channel(1); @@ -664,10 +719,12 @@ mod tests { Some(Duration::from_millis(1)), ); let mut watcher = StateWatcher::started(); + let latest_gas_price = LatestGasPrice::new(0, 0); let mut service = GasPriceServiceV1::new( l2_block_source, shared_algo, + latest_gas_price, algo_updater, da_source, inner, diff --git a/crates/services/gas_price_service/src/v1/tests.rs b/crates/services/gas_price_service/src/v1/tests.rs index f5524508edc..edbbbe9d6b8 100644 --- a/crates/services/gas_price_service/src/v1/tests.rs +++ b/crates/services/gas_price_service/src/v1/tests.rs @@ -43,6 +43,7 @@ use crate::{ service::{ initialize_algorithm, GasPriceServiceV1, + LatestGasPrice, }, uninitialized_task::{ fuel_storage_unrecorded_blocks::AsUnrecordedBlocks, @@ -355,6 +356,7 @@ async fn next_gas_price__affected_by_new_l2_block() { block_gas_capacity: 100, block_bytes: 100, block_fees: 100, + gas_price: 100, }; let (l2_block_sender, l2_block_receiver) = tokio::sync::mpsc::channel(1); let l2_block_source = FakeL2BlockSource { @@ -369,9 +371,11 @@ async fn next_gas_price__affected_by_new_l2_block() { initialize_algorithm(&config, height, &metadata_storage).unwrap(); let da_source = FakeDABlockCost::never_returns(); let da_source_service = DaSourceService::new(da_source, None); + let latest_gas_price = LatestGasPrice::new(0, 0); let mut service = GasPriceServiceV1::new( l2_block_source, shared_algo, + latest_gas_price, algo_updater, da_source_service, inner, @@ -401,6 +405,7 @@ async fn run__new_l2_block_saves_old_metadata() { block_gas_capacity: 100, block_bytes: 100, block_fees: 100, + gas_price: 100, }; let (l2_block_sender, l2_block_receiver) = tokio::sync::mpsc::channel(1); let l2_block_source = FakeL2BlockSource { @@ -413,9 +418,11 @@ async fn run__new_l2_block_saves_old_metadata() { let shared_algo = SharedV1Algorithm::new_with_algorithm(algo_updater.algorithm()); let da_source = FakeDABlockCost::never_returns(); let da_source_service = DaSourceService::new(da_source, None); + let latest_gas_price = LatestGasPrice::new(0, 0); let mut service = GasPriceServiceV1::new( l2_block_source, shared_algo, + latest_gas_price, algo_updater, da_source_service, inner, @@ -438,6 +445,54 @@ async fn run__new_l2_block_saves_old_metadata() { service.shutdown().await.unwrap(); } +#[tokio::test] +async fn run__new_l2_block_updates_latest_gas_price_arc() { + // given + let height = 1; + let gas_price = 40; + let l2_block = BlockInfo::Block { + height, + gas_used: 60, + block_gas_capacity: 100, + block_bytes: 100, + block_fees: 100, + gas_price, + }; + let (l2_block_sender, l2_block_receiver) = tokio::sync::mpsc::channel(1); + let l2_block_source = FakeL2BlockSource { + l2_block: l2_block_receiver, + }; + + let config = zero_threshold_arbitrary_config(); + let inner = database(); + let algo_updater = updater_from_config(&config); + let shared_algo = SharedV1Algorithm::new_with_algorithm(algo_updater.algorithm()); + let da_source = FakeDABlockCost::never_returns(); + let da_source_service = DaSourceService::new(da_source, None); + let latest_gas_price = LatestGasPrice::new(0, 0); + let mut service = GasPriceServiceV1::new( + l2_block_source, + shared_algo, + latest_gas_price.clone(), + algo_updater, + da_source_service, + inner, + ); + let mut watcher = StateWatcher::started(); + + // when + l2_block_sender.send(l2_block).await.unwrap(); + service.run(&mut watcher).await; + + // then + let expected = (height, gas_price); + let actual = latest_gas_price.get(); + assert_eq!(expected, actual); + + // cleanup + service.shutdown().await.unwrap(); +} + #[derive(Clone)] struct FakeSettings; @@ -506,7 +561,7 @@ impl L2Data for FakeL2Data { &self, _height: &BlockHeight, ) -> StorageResult>> { - unimplemented!() + Ok(None) } } impl AtomicView for FakeOnChainDb { diff --git a/crates/services/gas_price_service/src/v1/uninitialized_task.rs b/crates/services/gas_price_service/src/v1/uninitialized_task.rs index 96da25427d4..571f3e9f84c 100644 --- a/crates/services/gas_price_service/src/v1/uninitialized_task.rs +++ b/crates/services/gas_price_service/src/v1/uninitialized_task.rs @@ -43,6 +43,7 @@ use crate::{ service::{ initialize_algorithm, GasPriceServiceV1, + LatestGasPrice, }, uninitialized_task::fuel_storage_unrecorded_blocks::{ AsUnrecordedBlocks, @@ -78,6 +79,7 @@ use fuel_gas_price_algorithm::v1::{ AlgorithmUpdaterV1, UnrecordedBlocks, }; +use std::sync::Arc; pub mod fuel_storage_unrecorded_blocks; @@ -90,6 +92,7 @@ pub struct UninitializedTask, pub(crate) shared_algo: SharedV1Algorithm, + pub(crate) latest_gas_price: LatestGasPrice, pub(crate) algo_updater: AlgorithmUpdaterV1, pub(crate) da_source: DA, } @@ -119,10 +122,20 @@ where .latest_height() .unwrap_or(genesis_block_height) .into(); + let latest_gas_price = on_chain_db + .latest_view()? + .get_block(&latest_block_height.into())? + .and_then(|block| { + let (_, gas_price) = mint_values(&block).ok()?; + Some(gas_price) + }) + .unwrap_or(0); let (algo_updater, shared_algo) = initialize_algorithm(&config, latest_block_height, &gas_price_db)?; + let latest_gas_price = LatestGasPrice::new(latest_block_height, latest_gas_price); + let task = Self { config, gas_metadata_height, @@ -132,6 +145,7 @@ where on_chain_db, block_stream, algo_updater, + latest_gas_price, shared_algo, da_source, }; @@ -181,6 +195,7 @@ where let service = GasPriceServiceV1::new( l2_block_source, self.shared_algo, + self.latest_gas_price, self.algo_updater, da_service, self.gas_price_db, @@ -201,6 +216,7 @@ where let service = GasPriceServiceV1::new( l2_block_source, self.shared_algo, + self.latest_gas_price, self.algo_updater, da_service, self.gas_price_db, @@ -221,12 +237,12 @@ where SettingsProvider: GasPriceSettingsProvider + 'static, { const NAME: &'static str = "GasPriceServiceV1"; - type SharedData = SharedV1Algorithm; + type SharedData = (SharedV1Algorithm, LatestGasPrice); type Task = GasPriceServiceV1, DA, AtomicStorage>; type TaskParams = (); fn shared_data(&self) -> Self::SharedData { - self.shared_algo.clone() + (self.shared_algo.clone(), self.latest_gas_price.clone()) } async fn into_task( diff --git a/tests/proptest-regressions/recovery.txt b/tests/proptest-regressions/recovery.txt index 964abdad7e9..28f5541dd30 100644 --- a/tests/proptest-regressions/recovery.txt +++ b/tests/proptest-regressions/recovery.txt @@ -6,3 +6,4 @@ # everyone who runs the test benefits from these saved cases. cc 7c4e472cb1f611f337e2b9e029d79cad58b6e80786135c0cea9e37808467f109 # shrinks to (height, lower_height) = (53, 0) cc 34f1effd503ddfe54ad3da854765812601969db5335e6bf5c385a99a7ef59e90 # shrinks to (height, lower_height) = (64, 27) +cc 46741826ecf4f441d17d822657a94fb33b941f19c222d181ccbb865b0b62b7fc # shrinks to (height, lower_height) = (4, 3) diff --git a/tests/tests/recovery.rs b/tests/tests/recovery.rs index 60806b58f05..07a2c9797d1 100644 --- a/tests/tests/recovery.rs +++ b/tests/tests/recovery.rs @@ -171,7 +171,12 @@ async fn _gas_price_updater__can_recover_on_startup_when_gas_price_db_is_behind( for _ in 0..diff { let _ = database.gas_price().rollback_last_block(); } - assert!(database.on_chain().latest_height() > database.gas_price().latest_height()); + assert!( + database.on_chain().latest_height() > database.gas_price().latest_height(), + "on_chain: {:?}, gas_price: {:?}", + database.on_chain().latest_height(), + database.gas_price().latest_height() + ); let temp_dir = driver.kill().await; // When