diff --git a/CHANGELOG.md b/CHANGELOG.md index af801e7b974..8340fc71d31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - "gas-price-threshold-percent" - the threshold percent for determining if the gas price will be increase or decreased And the following CLI flags are serving a new purpose - "min-gas-price" - the minimum gas price that the gas price algorithm will return +- [2041](https://github.com/FuelLabs/fuel-core/pull/2041): Add code for startup of the gas price algorithm updater so + the gas price db on startup is always in sync with the on chain db + ## [Version 0.31.0] ### Added diff --git a/Cargo.lock b/Cargo.lock index 642878d8eee..e59aa499364 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3052,7 +3052,6 @@ dependencies = [ "tokio", "tokio-stream", "tracing", - "tracing-subscriber", ] [[package]] @@ -3300,6 +3299,7 @@ dependencies = [ "itertools 0.12.1", "pretty_assertions", "primitive-types", + "proptest", "rand", "reqwest", "rstest", diff --git a/crates/fuel-core/src/graphql_api/ports.rs b/crates/fuel-core/src/graphql_api/ports.rs index 138397c50a8..cfa03cce579 100644 --- a/crates/fuel-core/src/graphql_api/ports.rs +++ b/crates/fuel-core/src/graphql_api/ports.rs @@ -236,7 +236,7 @@ pub trait P2pPort: Send + Sync { #[async_trait::async_trait] pub trait GasPriceEstimate: Send + Sync { /// The worst case scenario for gas price at a given horizon - async fn worst_case_gas_price(&self, height: BlockHeight) -> u64; + async fn worst_case_gas_price(&self, height: BlockHeight) -> Option; } /// Trait for getting VM memory. diff --git a/crates/fuel-core/src/schema/gas_price.rs b/crates/fuel-core/src/schema/gas_price.rs index dbc56958448..577d68cec9e 100644 --- a/crates/fuel-core/src/schema/gas_price.rs +++ b/crates/fuel-core/src/schema/gas_price.rs @@ -105,7 +105,10 @@ impl EstimateGasPriceQuery { let gas_price_provider = ctx.data_unchecked::(); let gas_price = gas_price_provider .worst_case_gas_price(target_block.into()) - .await; + .await + .ok_or(async_graphql::Error::new(format!( + "Failed to estimate gas price for block, algorithm not yet set: {target_block:?}" + )))?; Ok(EstimateGasPrice { gas_price: gas_price.into(), diff --git a/crates/fuel-core/src/service/adapters/fuel_gas_price_provider.rs b/crates/fuel-core/src/service/adapters/fuel_gas_price_provider.rs index 4319dfd5218..1c7e28ebda5 100644 --- a/crates/fuel-core/src/service/adapters/fuel_gas_price_provider.rs +++ b/crates/fuel-core/src/service/adapters/fuel_gas_price_provider.rs @@ -7,7 +7,10 @@ use fuel_core_producer::block_producer::gas_price::GasPriceProvider as ProducerG use fuel_core_txpool::ports::GasPriceProvider as TxPoolGasPriceProvider; use fuel_core_types::{ fuel_types::BlockHeight, - services::txpool::Result as TxPoolResult, + services::txpool::{ + Error as TxPoolError, + Result as TxPoolResult, + }, }; pub type Result = std::result::Result; @@ -53,7 +56,7 @@ impl FuelGasPriceProvider where A: GasPriceAlgorithm + Send + Sync, { - async fn next_gas_price(&self) -> u64 { + async fn next_gas_price(&self) -> Option { self.algorithm.next_gas_price().await } } @@ -64,7 +67,9 @@ where A: GasPriceAlgorithm + Send + Sync, { async fn next_gas_price(&self) -> anyhow::Result { - Ok(self.next_gas_price().await) + self.next_gas_price() + .await + .ok_or(anyhow::anyhow!("No gas price available")) } } @@ -74,7 +79,11 @@ where A: GasPriceAlgorithm + Send + Sync, { async fn next_gas_price(&self) -> TxPoolResult { - Ok(self.next_gas_price().await) + self.next_gas_price() + .await + .ok_or(TxPoolError::GasPriceNotFound( + "Gas price not set yet".to_string(), + )) } } @@ -83,7 +92,7 @@ impl GraphqlGasPriceEstimate for FuelGasPriceProvider where A: GasPriceAlgorithm + Send + Sync, { - async fn worst_case_gas_price(&self, height: BlockHeight) -> u64 { + async fn worst_case_gas_price(&self, height: BlockHeight) -> Option { self.algorithm.worst_case_gas_price(height).await } } diff --git a/crates/fuel-core/src/service/adapters/fuel_gas_price_provider/tests.rs b/crates/fuel-core/src/service/adapters/fuel_gas_price_provider/tests.rs index 7ae74e62d68..0782e170c9c 100644 --- a/crates/fuel-core/src/service/adapters/fuel_gas_price_provider/tests.rs +++ b/crates/fuel-core/src/service/adapters/fuel_gas_price_provider/tests.rs @@ -15,7 +15,7 @@ fn build_provider(algorithm: A) -> FuelGasPriceProvider where A: Send + Sync, { - let algorithm = SharedGasPriceAlgo::new(algorithm); + let algorithm = SharedGasPriceAlgo::new_with_algorithm(algorithm); FuelGasPriceProvider::new(algorithm) } diff --git a/crates/fuel-core/src/service/adapters/fuel_gas_price_provider/tests/graph_ql_gas_price_estimate_tests.rs b/crates/fuel-core/src/service/adapters/fuel_gas_price_provider/tests/graph_ql_gas_price_estimate_tests.rs index 5ffb079c462..18aa966e321 100644 --- a/crates/fuel-core/src/service/adapters/fuel_gas_price_provider/tests/graph_ql_gas_price_estimate_tests.rs +++ b/crates/fuel-core/src/service/adapters/fuel_gas_price_provider/tests/graph_ql_gas_price_estimate_tests.rs @@ -11,7 +11,10 @@ async fn estimate_gas_price__happy_path() { // when let expected_price = algo.worst_case_gas_price(next_height); - let actual_price = gas_price_provider.worst_case_gas_price(next_height).await; + let actual_price = gas_price_provider + .worst_case_gas_price(next_height) + .await + .unwrap(); // then assert_eq!(expected_price, actual_price); diff --git a/crates/fuel-core/src/service/adapters/fuel_gas_price_provider/tests/producer_gas_price_tests.rs b/crates/fuel-core/src/service/adapters/fuel_gas_price_provider/tests/producer_gas_price_tests.rs index 9c1c9d3aad3..42fe3c26605 100644 --- a/crates/fuel-core/src/service/adapters/fuel_gas_price_provider/tests/producer_gas_price_tests.rs +++ b/crates/fuel-core/src/service/adapters/fuel_gas_price_provider/tests/producer_gas_price_tests.rs @@ -13,7 +13,7 @@ async fn gas_price__if_requested_block_height_is_latest_return_gas_price() { // when let expected_price = algo.next_gas_price(); - let actual_price = gas_price_provider.next_gas_price().await; + let actual_price = gas_price_provider.next_gas_price().await.unwrap(); // then assert_eq!(expected_price, actual_price); diff --git a/crates/fuel-core/src/service/adapters/fuel_gas_price_provider/tests/tx_pool_gas_price_tests.rs b/crates/fuel-core/src/service/adapters/fuel_gas_price_provider/tests/tx_pool_gas_price_tests.rs index 9c1c9d3aad3..42fe3c26605 100644 --- a/crates/fuel-core/src/service/adapters/fuel_gas_price_provider/tests/tx_pool_gas_price_tests.rs +++ b/crates/fuel-core/src/service/adapters/fuel_gas_price_provider/tests/tx_pool_gas_price_tests.rs @@ -13,7 +13,7 @@ async fn gas_price__if_requested_block_height_is_latest_return_gas_price() { // when let expected_price = algo.next_gas_price(); - let actual_price = gas_price_provider.next_gas_price().await; + let actual_price = gas_price_provider.next_gas_price().await.unwrap(); // then assert_eq!(expected_price, actual_price); diff --git a/crates/fuel-core/src/service/adapters/graphql_api.rs b/crates/fuel-core/src/service/adapters/graphql_api.rs index 8e1744f4b78..686cff125b9 100644 --- a/crates/fuel-core/src/service/adapters/graphql_api.rs +++ b/crates/fuel-core/src/service/adapters/graphql_api.rs @@ -165,8 +165,8 @@ impl worker::TxPool for TxPoolAdapter { #[async_trait::async_trait] impl GasPriceEstimate for StaticGasPrice { - async fn worst_case_gas_price(&self, _height: BlockHeight) -> u64 { - self.gas_price + async fn worst_case_gas_price(&self, _height: BlockHeight) -> Option { + Some(self.gas_price) } } diff --git a/crates/fuel-core/src/service/sub_services.rs b/crates/fuel-core/src/service/sub_services.rs index 0e39a872d12..e35d24050c5 100644 --- a/crates/fuel-core/src/service/sub_services.rs +++ b/crates/fuel-core/src/service/sub_services.rs @@ -1,4 +1,5 @@ #![allow(clippy::let_unit_value)] + use super::{ adapters::P2PAdapter, genesis::create_genesis_block, @@ -32,16 +33,22 @@ use crate::{ SubServices, }, }; +#[allow(unused_imports)] use fuel_core_gas_price_service::fuel_gas_price_updater::{ fuel_core_storage_adapter::FuelL2BlockSource, Algorithm, + AlgorithmV0, FuelGasPriceUpdater, UpdaterMetadata, V0Metadata, }; use fuel_core_poa::Trigger; +use fuel_core_services::{ + RunnableService, + ServiceRunner, +}; use fuel_core_storage::{ - structured_storage::StructuredStorage, + self, transactional::AtomicView, }; #[cfg(feature = "relayer")] @@ -49,6 +56,8 @@ use fuel_core_types::blockchain::primitives::DaBlockHeight; use std::sync::Arc; use tokio::sync::Mutex; +mod algorithm_updater; + pub type PoAService = fuel_core_poa::Service; #[cfg(feature = "p2p")] @@ -176,24 +185,20 @@ pub fn init_sub_services( #[cfg(not(feature = "p2p"))] let p2p_adapter = P2PAdapter::new(); - let updater_metadata = UpdaterMetadata::V0(V0Metadata { - new_exec_price: config.starting_gas_price, - min_exec_gas_price: config.min_gas_price, - exec_gas_price_change_percent: config.gas_price_change_percent, - l2_block_height: last_height.into(), - l2_block_fullness_threshold_percent: config.gas_price_threshold_percent, - }); let genesis_block_height = *genesis_block.header().height(); let settings = consensus_parameters_provider.clone(); let block_stream = importer_adapter.events_shared_result(); - let l2_block_source = - FuelL2BlockSource::new(genesis_block_height, settings, block_stream); - let metadata_storage = StructuredStorage::new(database.gas_price().clone()); - let update_algo = - FuelGasPriceUpdater::init(updater_metadata, l2_block_source, metadata_storage)?; - let gas_price_service = - fuel_core_gas_price_service::new_service(last_height, update_algo)?; - let next_algo = gas_price_service.shared.clone(); + + let gas_price_init = algorithm_updater::InitializeTask::new( + config.clone(), + genesis_block_height, + settings, + block_stream, + database.gas_price().clone(), + database.on_chain().clone(), + )?; + let next_algo = gas_price_init.shared_data(); + let gas_price_service = ServiceRunner::new(gas_price_init); let gas_price_provider = FuelGasPriceProvider::new(next_algo); let txpool = fuel_core_txpool::new_service( diff --git a/crates/fuel-core/src/service/sub_services/algorithm_updater.rs b/crates/fuel-core/src/service/sub_services/algorithm_updater.rs new file mode 100644 index 00000000000..47b635ac318 --- /dev/null +++ b/crates/fuel-core/src/service/sub_services/algorithm_updater.rs @@ -0,0 +1,290 @@ +use crate::{ + database::{ + database_description::{ + gas_price::GasPriceDatabase, + on_chain::OnChain, + }, + Database, + RegularStage, + }, + service::{ + adapters::ConsensusParametersProvider, + Config, + }, +}; + +use fuel_core_gas_price_service::{ + fuel_gas_price_updater::{ + fuel_core_storage_adapter::{ + FuelL2BlockSource, + GasPriceSettingsProvider, + }, + Algorithm, + AlgorithmUpdater, + AlgorithmUpdaterV0, + FuelGasPriceUpdater, + MetadataStorage, + UpdaterMetadata, + V0Metadata, + }, + GasPriceService, + SharedGasPriceAlgo, +}; +use fuel_core_services::{ + stream::BoxStream, + RunnableService, + StateWatcher, +}; +use fuel_core_storage::{ + structured_storage::StructuredStorage, + tables::{ + FuelBlocks, + Transactions, + }, + transactional::HistoricalView, + StorageAsRef, +}; +use fuel_core_types::{ + fuel_tx::field::MintAmount, + fuel_types::BlockHeight, + services::block_importer::SharedImportResult, +}; + +type Updater = FuelGasPriceUpdater< + FuelL2BlockSource, + MetadataStorageAdapter, +>; + +pub struct InitializeTask { + pub config: Config, + pub genesis_block_height: BlockHeight, + pub settings: ConsensusParametersProvider, + pub gas_price_db: Database>, + pub on_chain_db: Database>, + pub block_stream: BoxStream, + pub shared_algo: SharedGasPriceAlgo, +} + +type MetadataStorageAdapter = + StructuredStorage>>; + +type Task = GasPriceService< + Algorithm, + FuelGasPriceUpdater< + FuelL2BlockSource, + MetadataStorageAdapter, + >, +>; + +impl InitializeTask { + pub fn new( + config: Config, + genesis_block_height: BlockHeight, + settings: ConsensusParametersProvider, + block_stream: BoxStream, + gas_price_db: Database>, + on_chain_db: Database>, + ) -> anyhow::Result { + let shared_algo = SharedGasPriceAlgo::new(); + let task = Self { + config, + genesis_block_height, + settings, + gas_price_db, + on_chain_db, + block_stream, + shared_algo, + }; + Ok(task) + } +} + +#[async_trait::async_trait] +impl RunnableService for InitializeTask { + const NAME: &'static str = "GasPriceUpdater"; + type SharedData = SharedGasPriceAlgo; + type Task = Task; + type TaskParams = (); + + fn shared_data(&self) -> Self::SharedData { + self.shared_algo.clone() + } + + async fn into_task( + self, + _state_watcher: &StateWatcher, + _params: Self::TaskParams, + ) -> anyhow::Result { + let starting_height = self + .on_chain_db + .latest_height()? + .unwrap_or(self.genesis_block_height); + + let updater = get_synced_gas_price_updater( + &self.config, + self.genesis_block_height, + self.settings, + self.gas_price_db, + self.on_chain_db, + self.block_stream, + )?; + let inner_service = + GasPriceService::new(starting_height, updater, self.shared_algo).await; + Ok(inner_service) + } +} + +pub fn get_synced_gas_price_updater( + config: &Config, + genesis_block_height: BlockHeight, + settings: ConsensusParametersProvider, + mut gas_price_db: Database>, + on_chain_db: Database>, + block_stream: BoxStream, +) -> anyhow::Result { + let mut metadata_height: u32 = gas_price_db + .latest_height()? + .unwrap_or(genesis_block_height) + .into(); + let latest_block_height: u32 = on_chain_db + .latest_height()? + .unwrap_or(genesis_block_height) + .into(); + + let genesis_metadata = UpdaterMetadata::V0(V0Metadata { + new_exec_price: config.starting_gas_price, + min_exec_gas_price: config.min_gas_price, + exec_gas_price_change_percent: config.gas_price_change_percent, + l2_block_height: genesis_block_height.into(), + l2_block_fullness_threshold_percent: config.gas_price_threshold_percent, + }); + + if metadata_height > latest_block_height { + revert_gas_price_db_to_height(&mut gas_price_db, latest_block_height.into())?; + metadata_height = gas_price_db + .latest_height()? + .unwrap_or(genesis_block_height) + .into(); + } + + let mut metadata_storage = StructuredStorage::new(gas_price_db); + let l2_block_source = + FuelL2BlockSource::new(genesis_block_height, settings.clone(), block_stream); + + if BlockHeight::from(latest_block_height) == genesis_block_height { + let updater = FuelGasPriceUpdater::new( + genesis_metadata.into(), + l2_block_source, + metadata_storage, + ); + Ok(updater) + } else { + if latest_block_height > metadata_height { + sync_metadata_storage_with_on_chain_storage( + &settings, + &mut metadata_storage, + on_chain_db, + metadata_height, + latest_block_height, + genesis_metadata, + genesis_block_height.into(), + )?; + } + + FuelGasPriceUpdater::init( + latest_block_height.into(), + l2_block_source, + metadata_storage, + ) + .map_err(|e| anyhow::anyhow!("Could not initialize gas price updater: {e:?}")) + } +} + +fn sync_metadata_storage_with_on_chain_storage( + settings: &ConsensusParametersProvider, + metadata_storage: &mut StructuredStorage< + Database>, + >, + on_chain_db: Database>, + metadata_height: u32, + latest_block_height: u32, + genesis_metadata: UpdaterMetadata, + genesis_block_height: u32, +) -> anyhow::Result<()> { + let metadata = if metadata_height == genesis_block_height { + genesis_metadata + } else { + metadata_storage + .get_metadata(&metadata_height.into())? + .ok_or(anyhow::anyhow!( + "Expected metadata to exist for height: {metadata_height}" + ))? + }; + let mut inner: AlgorithmUpdater = metadata.into(); + match &mut inner { + AlgorithmUpdater::V0(ref mut updater) => { + sync_v0_metadata( + settings, + on_chain_db, + metadata_height, + latest_block_height, + updater, + metadata_storage, + )?; + } + } + Ok(()) +} + +fn sync_v0_metadata( + settings: &ConsensusParametersProvider, + on_chain_db: Database>, + metadata_height: u32, + latest_block_height: u32, + updater: &mut AlgorithmUpdaterV0, + metadata_storage: &mut StructuredStorage< + Database>, + >, +) -> anyhow::Result<()> { + let first = metadata_height.saturating_add(1); + for height in first..=latest_block_height { + let view = on_chain_db.view_at(&height.into())?; + let block = view + .storage::() + .get(&height.into())? + .ok_or(anyhow::anyhow!("Expected block to exist"))?; + let last_tx_id = block.transactions().last().ok_or(anyhow::anyhow!( + "Expected block to have at least one transaction" + ))?; + let param_version = block.header().consensus_parameters_version; + let params = settings.settings(¶m_version)?; + let mint = view + .storage::() + .get(last_tx_id)? + .ok_or(anyhow::anyhow!("Expected tx to exist for id: {last_tx_id}"))? + .as_mint() + .ok_or(anyhow::anyhow!("Expected tx to be a mint"))? + .to_owned(); + let block_gas_used = mint.mint_amount(); + let block_gas_capacity = params.block_gas_limit.try_into()?; + updater.update_l2_block_data(height, *block_gas_used, block_gas_capacity)?; + let metadata = AlgorithmUpdater::V0(updater.clone()).into(); + metadata_storage.set_metadata(metadata)?; + } + Ok(()) +} + +fn revert_gas_price_db_to_height( + gas_price_db: &mut Database>, + height: BlockHeight, +) -> anyhow::Result<()> { + if let Some(gas_price_db_height) = gas_price_db.latest_height()? { + let gas_price_db_height: u32 = gas_price_db_height.into(); + let height: u32 = height.into(); + let diff = gas_price_db_height.saturating_sub(height); + for _ in 0..diff { + gas_price_db.rollback_last_block()?; + } + } + Ok(()) +} diff --git a/crates/services/gas_price_service/Cargo.toml b/crates/services/gas_price_service/Cargo.toml index 7bfe31fb4a6..3244cd4aa6d 100644 --- a/crates/services/gas_price_service/Cargo.toml +++ b/crates/services/gas_price_service/Cargo.toml @@ -30,4 +30,3 @@ tracing = { workspace = true } fuel-core-services = { workspace = true, features = ["test-helpers"] } fuel-core-storage = { workspace = true, features = ["test-helpers"] } fuel-core-types = { path = "./../../types", features = ["test-helpers"] } -tracing-subscriber = { workspace = true } diff --git a/crates/services/gas_price_service/src/fuel_gas_price_updater.rs b/crates/services/gas_price_service/src/fuel_gas_price_updater.rs index e32f3ddcae7..13d1c4e79fb 100644 --- a/crates/services/gas_price_service/src/fuel_gas_price_updater.rs +++ b/crates/services/gas_price_service/src/fuel_gas_price_updater.rs @@ -151,7 +151,7 @@ pub struct V0Metadata { /// The Percentage the execution gas price will change in a single block, either increase or decrease /// based on the fullness of the last L2 block pub exec_gas_price_change_percent: u64, - /// The height of the next L2 block + /// The height for which the `new_exec_price` is calculated, which should be the _next_ block pub l2_block_height: u32, /// The threshold of gas usage above and below which the gas price will increase or decrease /// This is a percentage of the total capacity of the L2 block @@ -187,14 +187,16 @@ where Metadata: MetadataStorage, { pub fn init( - init_metadata: UpdaterMetadata, + target_block_height: BlockHeight, l2_block_source: L2, metadata_storage: Metadata, ) -> Result { - let target_block_height = init_metadata.l2_block_height(); let inner = metadata_storage .get_metadata(&target_block_height)? - .unwrap_or(init_metadata) + .ok_or(Error::CouldNotInitUpdater(anyhow::anyhow!( + "No metadata found for block height: {:?}", + target_block_height + )))? .into(); let updater = Self { inner, diff --git a/crates/services/gas_price_service/src/fuel_gas_price_updater/tests.rs b/crates/services/gas_price_service/src/fuel_gas_price_updater/tests.rs index cea31928f37..6a29b3f34dc 100644 --- a/crates/services/gas_price_service/src/fuel_gas_price_updater/tests.rs +++ b/crates/services/gas_price_service/src/fuel_gas_price_updater/tests.rs @@ -90,9 +90,11 @@ async fn next__fetches_l2_block() { let metadata_storage = FakeMetadata::empty(); let starting_metadata = arb_metadata(); - let mut updater = - FuelGasPriceUpdater::init(starting_metadata, l2_block_source, metadata_storage) - .unwrap(); + let mut updater = FuelGasPriceUpdater::new( + starting_metadata.into(), + l2_block_source, + metadata_storage, + ); let start = updater.start(0.into()); // when @@ -116,10 +118,9 @@ async fn init__if_exists_already_reload() { let l2_block_source = PendingL2BlockSource; // when - let different_metadata = different_arb_metadata(); + let height = metadata.l2_block_height(); let updater = - FuelGasPriceUpdater::init(different_metadata, l2_block_source, metadata_storage) - .unwrap(); + FuelGasPriceUpdater::init(height, l2_block_source, metadata_storage).unwrap(); // then let expected: AlgorithmUpdater = metadata.into(); @@ -128,21 +129,18 @@ async fn init__if_exists_already_reload() { } #[tokio::test] -async fn init__if_it_does_not_exist_create_with_provided_values() { +async fn init__if_it_does_not_exist_fail() { // given let metadata_storage = FakeMetadata::empty(); let l2_block_source = PendingL2BlockSource; // when let metadata = different_arb_metadata(); - let updater = - FuelGasPriceUpdater::init(metadata.clone(), l2_block_source, metadata_storage) - .unwrap(); + let height = u32::from(metadata.l2_block_height()) + 1; + let res = FuelGasPriceUpdater::init(height.into(), l2_block_source, metadata_storage); // then - let expected: AlgorithmUpdater = metadata.into(); - let actual = updater.inner; - assert_eq!(expected, actual); + assert!(matches!(res, Err(Error::CouldNotInitUpdater(_)))); } #[tokio::test] @@ -163,12 +161,11 @@ async fn next__new_l2_block_saves_old_metadata() { }; let starting_metadata = arb_metadata(); - let mut updater = FuelGasPriceUpdater::init( - starting_metadata.clone(), + let mut updater = FuelGasPriceUpdater::new( + starting_metadata.clone().into(), l2_block_source, metadata_storage, - ) - .unwrap(); + ); // when let next = tokio::spawn(async move { updater.next().await }); diff --git a/crates/services/gas_price_service/src/lib.rs b/crates/services/gas_price_service/src/lib.rs index 09c80c3b98a..bf0ff05c1dd 100644 --- a/crates/services/gas_price_service/src/lib.rs +++ b/crates/services/gas_price_service/src/lib.rs @@ -7,7 +7,6 @@ use async_trait::async_trait; use fuel_core_services::{ RunnableService, RunnableTask, - ServiceRunner, StateWatcher, }; use fuel_core_types::fuel_types::BlockHeight; @@ -19,18 +18,6 @@ pub mod static_updater; pub mod fuel_gas_price_updater; -pub fn new_service( - current_fuel_block_height: BlockHeight, - update_algo: U, -) -> anyhow::Result>> -where - U: UpdateAlgorithm + Send + Sync, - A: Send + Sync, -{ - let service = GasPriceService::new(current_fuel_block_height, update_algo); - Ok(ServiceRunner::new(service)) -} - /// The service that updates the gas price algorithm. pub struct GasPriceService { /// The algorithm that can be used in the next block @@ -44,11 +31,15 @@ where U: UpdateAlgorithm, A: Send + Sync, { - pub fn new(starting_block_height: BlockHeight, update_algorithm: U) -> Self { + pub async fn new( + starting_block_height: BlockHeight, + update_algorithm: U, + mut shared_algo: SharedGasPriceAlgo, + ) -> Self { let algorithm = update_algorithm.start(starting_block_height); - let next_block_algorithm = SharedGasPriceAlgo::new(algorithm); + shared_algo.update(algorithm).await; Self { - next_block_algorithm, + next_block_algorithm: shared_algo, update_algorithm, } } @@ -86,8 +77,8 @@ where } } -#[derive(Debug)] -pub struct SharedGasPriceAlgo(Arc>); +#[derive(Debug, Default)] +pub struct SharedGasPriceAlgo(Arc>>); impl Clone for SharedGasPriceAlgo { fn clone(&self) -> Self { @@ -99,12 +90,17 @@ impl SharedGasPriceAlgo where A: Send + Sync, { - pub fn new(algo: A) -> Self { - Self(Arc::new(RwLock::new(algo))) + pub fn new() -> Self { + Self(Arc::new(RwLock::new(None))) + } + + pub fn new_with_algorithm(algorithm: A) -> Self { + Self(Arc::new(RwLock::new(Some(algorithm)))) } + pub async fn update(&mut self, new_algo: A) { let mut write_lock = self.0.write().await; - *write_lock = new_algo; + *write_lock = Some(new_algo); } } @@ -112,12 +108,16 @@ impl SharedGasPriceAlgo where A: GasPriceAlgorithm + Send + Sync, { - pub async fn next_gas_price(&self) -> u64 { - self.0.read().await.next_gas_price() + pub async fn next_gas_price(&self) -> Option { + self.0.read().await.as_ref().map(|a| a.next_gas_price()) } - pub async fn worst_case_gas_price(&self, block_height: BlockHeight) -> u64 { - self.0.read().await.worst_case_gas_price(block_height) + pub async fn worst_case_gas_price(&self, block_height: BlockHeight) -> Option { + self.0 + .read() + .await + .as_ref() + .map(|a| a.worst_case_gas_price(block_height)) } } @@ -181,6 +181,7 @@ mod tests { use crate::{ GasPriceAlgorithm, GasPriceService, + SharedGasPriceAlgo, UpdateAlgorithm, }; use fuel_core_services::{ @@ -227,15 +228,14 @@ mod tests { } #[tokio::test] async fn run__updates_gas_price() { - let _ = tracing_subscriber::fmt::try_init(); - // given let (price_sender, price_receiver) = mpsc::channel(1); let updater = TestAlgorithmUpdater { start: TestAlgorithm { price: 0 }, price_source: price_receiver, }; - let service = GasPriceService::new(0.into(), updater); + let shared_algo = SharedGasPriceAlgo::new(); + let service = GasPriceService::new(0.into(), updater, shared_algo).await; let watcher = StateWatcher::started(); let read_algo = service.next_block_algorithm(); let task = service.into_task(&watcher, ()).await.unwrap(); @@ -248,7 +248,7 @@ mod tests { tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; // then - let actual_price = read_algo.next_gas_price().await; + let actual_price = read_algo.next_gas_price().await.unwrap(); assert_eq!(expected_price, actual_price); } } diff --git a/tests/Cargo.toml b/tests/Cargo.toml index e24e285c91e..b752e9e2539 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -65,6 +65,7 @@ tokio = { workspace = true, features = [ [dev-dependencies] pretty_assertions = "1.4" +proptest = { workspace = true } tracing = { workspace = true } [features] diff --git a/tests/proptest-regressions/recovery.txt b/tests/proptest-regressions/recovery.txt new file mode 100644 index 00000000000..1b72a2638cd --- /dev/null +++ b/tests/proptest-regressions/recovery.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 7c4e472cb1f611f337e2b9e029d79cad58b6e80786135c0cea9e37808467f109 # shrinks to (height, lower_height) = (53, 0) diff --git a/tests/tests/recovery.rs b/tests/tests/recovery.rs index 3052ff1eb60..0b826f96546 100644 --- a/tests/tests/recovery.rs +++ b/tests/tests/recovery.rs @@ -1,4 +1,14 @@ +#![allow(non_snake_case)] + use fuel_core_types::fuel_types::BlockHeight; +use proptest::{ + prelude::{ + Just, + ProptestConfig, + }, + prop_compose, + proptest, +}; use test_helpers::fuel_core_driver::FuelCoreDriver; #[tokio::test(flavor = "multi_thread")] @@ -21,7 +31,7 @@ async fn off_chain_worker_can_recover_on_start_up_when_is_behind() -> anyhow::Re Some(BlockHeight::new(HEIGHTS)) ); for _ in 0..HEIGHTS { - let _ = database.off_chain().rollback_last_block(); + database.off_chain().rollback_last_block()?; } assert!(database.on_chain().latest_height()? > database.off_chain().latest_height()?); let temp_dir = driver.kill().await; @@ -52,3 +62,150 @@ async fn off_chain_worker_can_recover_on_start_up_when_is_behind() -> anyhow::Re Ok(()) } + +prop_compose! { + fn height_and_lower_height()(height in 1..100u32)(height in Just(height), lower_height in 0..height) -> (u32, u32) { + (height, lower_height) + } +} + +async fn _gas_price_updater__can_recover_on_startup_when_gas_price_db_is_ahead( + height: u32, + lower_height: u32, +) -> anyhow::Result<()> { + let driver = FuelCoreDriver::spawn(&[ + "--debug", + "--poa-instant", + "true", + "--state-rewind-duration", + "7d", + ]) + .await?; + + // Given + driver.client.produce_blocks(height, None).await?; + let database = &driver.node.shared.database; + assert_eq!( + database.on_chain().latest_height()?, + Some(BlockHeight::new(height)) + ); + let diff = height - lower_height; + for _ in 0..diff { + database.on_chain().rollback_last_block()?; + database.off_chain().rollback_last_block()?; + } + assert!(database.on_chain().latest_height()? < database.gas_price().latest_height()?); + let temp_dir = driver.kill().await; + + // When + let recovered_driver = FuelCoreDriver::spawn_with_directory( + temp_dir, + &[ + "--debug", + "--poa-instant", + "true", + "--state-rewind-duration", + "7d", + ], + ) + .await?; + + // Then + let recovered_database = &recovered_driver.node.shared.database; + let actual_onchain_height = recovered_database.on_chain().latest_height()?.unwrap(); + let expected_onchain_height = BlockHeight::new(lower_height); + + let actual_gas_price_height = recovered_database + .gas_price() + .latest_height()? + .unwrap_or(0.into()); // Gas price metadata never gets written for block 0 + let expected_gas_price_height = BlockHeight::new(lower_height); + + assert_eq!(actual_onchain_height, expected_onchain_height); + assert_eq!(actual_gas_price_height, expected_gas_price_height); + + Ok(()) +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(10))] + #[test] + fn gas_price_updater__can_recover_on_startup_when_gas_price_db_is_ahead((height, lower_height) in height_and_lower_height()) { + let rt = multithreaded_runtime(); + rt.block_on( + _gas_price_updater__can_recover_on_startup_when_gas_price_db_is_ahead(height, lower_height) + ).unwrap() + } +} +async fn _gas_price_updater__can_recover_on_startup_when_gas_price_db_is_behind( + height: u32, + lower_height: u32, +) -> anyhow::Result<()> { + let driver = FuelCoreDriver::spawn(&[ + "--debug", + "--poa-instant", + "true", + "--state-rewind-duration", + "7d", + ]) + .await?; + + // Given + driver.client.produce_blocks(height, None).await?; + let database = &driver.node.shared.database; + assert_eq!( + database.on_chain().latest_height()?, + Some(BlockHeight::new(height)) + ); + + let diff = height - lower_height; + for _ in 0..diff { + let _ = database.gas_price().rollback_last_block(); + } + assert!(database.on_chain().latest_height()? > database.gas_price().latest_height()?); + let temp_dir = driver.kill().await; + + // When + let recovered_driver = FuelCoreDriver::spawn_with_directory( + temp_dir, + &[ + "--debug", + "--poa-instant", + "true", + "--state-rewind-duration", + "7d", + ], + ) + .await?; + + // Then + let recovered_database = &recovered_driver.node.shared.database; + assert_eq!( + recovered_database.on_chain().latest_height()?, + Some(BlockHeight::new(height)) + ); + assert_eq!( + recovered_database.gas_price().latest_height()?, + Some(BlockHeight::new(height)) + ); + + Ok(()) +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(10))] + #[test] + fn gas_price_updater__can_recover_on_startup_when_gas_price_db_is_behind((height, lower_height) in height_and_lower_height()) { + let rt = multithreaded_runtime(); + rt.block_on( + _gas_price_updater__can_recover_on_startup_when_gas_price_db_is_behind(height, lower_height) + ).unwrap() + } +} + +fn multithreaded_runtime() -> tokio::runtime::Runtime { + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap() +}