diff --git a/.gitignore b/.gitignore index f62b3ff7167..a35bee1be51 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,4 @@ node_modules/ package-lock.json package.json bin/fuel-core/chainspec/local-testnet/state_transition_bytecode.wasm - +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index ea3c4352f99..3d8f9b4003b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - [2535](https://github.com/FuelLabs/fuel-core/pull/2535): Expose `backup` and `restore` APIs on the `CombinedDatabase` struct to create portable backups and restore from them. ### Fixed +- [2469](https://github.com/FuelLabs/fuel-core/pull/2469): Improved the logic for syncing the gas price database with on_chain database - [2365](https://github.com/FuelLabs/fuel-core/pull/2365): Fixed the error during dry run in the case of race condition. - [2366](https://github.com/FuelLabs/fuel-core/pull/2366): The `importer_gas_price_for_block` metric is properly collected. - [2369](https://github.com/FuelLabs/fuel-core/pull/2369): The `transaction_insertion_time_in_thread_pool_milliseconds` metric is properly collected. @@ -50,6 +51,8 @@ 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 +- [2469](https://github.com/FuelLabs/fuel-core/pull/2469): Updated adapter for querying costs from DA Block committer API +- [2469](https://github.com/FuelLabs/fuel-core/pull/2469): Use the gas price from the latest block to estimate future gas prices - [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. @@ -62,6 +65,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - [2463](https://github.com/FuelLabs/fuel-core/pull/2463): The `coinsToSpend` GraphQL query handler now uses index to provide the response in a more performant way. As the index is not created retroactively, the client must be initialized with an empty database and synced from the genesis block to utilize it. Otherwise, the legacy way of retrieving data will be used. #### Breaking +- [2469](https://github.com/FuelLabs/fuel-core/pull/2469): Move from `GasPriceServicev0` to `GasPriceServiceV1`. Include new config values. - [2438](https://github.com/FuelLabs/fuel-core/pull/2438): The `fuel-core-client` can only work with new version of the `fuel-core`. The `0.40` and all older versions are not supported. - [2438](https://github.com/FuelLabs/fuel-core/pull/2438): Updated `fuel-vm` to `0.59.1` release. Check [release notes](https://github.com/FuelLabs/fuel-vm/releases/tag/v0.59.0) for more details. - [2389](https://github.com/FuelLabs/fuel-core/pull/2258): Updated the `messageProof` GraphQL schema to return a non-nullable `MessageProof`. diff --git a/Cargo.lock b/Cargo.lock index a797d35f850..bae26117c26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -251,6 +251,16 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "155a5a185e42c6b77ac7b88a15143d930a9e9727a5b7b77eed417404ab15c247" +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "assert_cmd" version = "2.0.16" @@ -1680,6 +1690,16 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -3396,6 +3416,7 @@ dependencies = [ "tower 0.4.13", "tower-http 0.4.4", "tracing", + "url", "uuid 1.11.0", ] @@ -3631,16 +3652,19 @@ dependencies = [ "fuel-core-types 0.40.2", "fuel-gas-price-algorithm", "futures", + "mockito", "num_enum", "parking_lot", "reqwest 0.12.12", "serde", + "serde_json", "strum 0.25.0", "strum_macros 0.25.3", "thiserror 1.0.69", "tokio", "tokio-stream", "tracing", + "url", ] [[package]] @@ -3944,6 +3968,8 @@ dependencies = [ "test-helpers", "tokio", "tracing", + "tracing-subscriber", + "url", ] [[package]] @@ -6566,6 +6592,30 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "mockito" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "652cd6d169a36eaf9d1e6bce1a221130439a966d7f27858af66a33a66e9c4ee2" +dependencies = [ + "assert-json-diff", + "bytes", + "colored", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.5.2", + "hyper-util", + "log", + "rand", + "regex", + "serde_json", + "serde_urlencoded", + "similar", + "tokio", +] + [[package]] name = "multer" version = "3.1.0" diff --git a/Cargo.toml b/Cargo.toml index 6b49ff33df8..e06070f3f02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -151,3 +151,4 @@ itertools = { version = "0.12", default-features = false } insta = "1.8" tempfile = "3.4" tikv-jemallocator = "0.5" +url = "2.2" diff --git a/bin/e2e-test-client/tests/integration_tests.rs b/bin/e2e-test-client/tests/integration_tests.rs index bb62d4e8dc3..720a4be4939 100644 --- a/bin/e2e-test-client/tests/integration_tests.rs +++ b/bin/e2e-test-client/tests/integration_tests.rs @@ -136,7 +136,7 @@ fn dev_config() -> Config { let reader = reader.with_chain_config(chain_config); let mut config = Config::local_node_with_reader(reader); - config.starting_gas_price = 1; + config.starting_exec_gas_price = 1; config.block_producer.coinbase_recipient = Some( ContractId::from_str( "0x7777777777777777777777777777777777777777777777777777777777777777", diff --git a/bin/fuel-core/Cargo.toml b/bin/fuel-core/Cargo.toml index 880a69f9296..3ffda465384 100644 --- a/bin/fuel-core/Cargo.toml +++ b/bin/fuel-core/Cargo.toml @@ -45,7 +45,7 @@ tracing-subscriber = { workspace = true, features = [ "env-filter", "json", ] } -url = { version = "2.2", optional = true } +url = { workspace = true, optional = true } [dev-dependencies] fuel-core = { workspace = true, features = ["test-helpers"] } diff --git a/bin/fuel-core/src/cli/run.rs b/bin/fuel-core/src/cli/run.rs index 763ffdef897..e7fed9d4014 100644 --- a/bin/fuel-core/src/cli/run.rs +++ b/bin/fuel-core/src/cli/run.rs @@ -87,6 +87,7 @@ use tracing::{ trace, warn, }; +use url::Url; #[cfg(feature = "rocksdb")] use fuel_core::state::historical_rocksdb::StateRewindPolicy; @@ -192,13 +193,27 @@ pub struct Command { #[arg(long = "native-executor-version", env)] pub native_executor_version: Option, - /// The starting gas price for the network - #[arg(long = "starting-gas-price", default_value = "0", env)] + /// The starting execution gas price for the network + #[cfg_attr( + feature = "production", + arg(long = "starting-gas-price", default_value = "1000", env) + )] + #[cfg_attr( + not(feature = "production"), + arg(long = "starting-gas-price", default_value = "0", env) + )] pub starting_gas_price: u64, /// The percentage change in gas price per block - #[arg(long = "gas-price-change-percent", default_value = "0", env)] - pub gas_price_change_percent: u64, + #[cfg_attr( + feature = "production", + arg(long = "gas-price-change-percent", default_value = "10", env) + )] + #[cfg_attr( + not(feature = "production"), + arg(long = "gas-price-change-percent", default_value = "0", env) + )] + pub gas_price_change_percent: u16, /// The minimum allowed gas price #[arg(long = "min-gas-price", default_value = "0", env)] @@ -206,7 +221,50 @@ pub struct Command { /// The percentage threshold for gas price increase #[arg(long = "gas-price-threshold-percent", default_value = "50", env)] - pub gas_price_threshold_percent: u64, + pub gas_price_threshold_percent: u8, + + /// Minimum DA gas price + #[cfg_attr( + feature = "production", + arg(long = "min-da-gas-price", default_value = "1000", env) + )] + #[cfg_attr( + not(feature = "production"), + arg(long = "min-da-gas-price", default_value = "0", env) + )] + pub min_da_gas_price: u64, + + /// Maximum allowed gas price for DA. + #[arg(long = "max-da-gas-price", default_value = "100000", 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. + #[arg( + long = "da-gas-price-p-component", + default_value = "799999999999993", + env + )] + pub da_gas_price_p_component: i64, + + /// D component of DA gas price calculation + /// **NOTE**: This is the **inverse** anticipatory control factor of a typical PD controller. + /// Increasing this value will reduce the dampening effect of quick algorithm changes. + #[arg( + long = "da-gas-price-d-component", + default_value = "10000000000000000", + env + )] + pub da_gas_price_d_component: i64, + + /// The URL for the DA Block Committer info + #[arg(long = "da-committer-url", env)] + pub da_committer_url: Option, + + /// The interval at which the `DaSourceService` polls for new data + #[arg(long = "da-poll-interval", env)] + pub da_poll_interval: Option, /// The signing key used when producing blocks. /// Setting via the `CONSENSUS_KEY_SECRET` ENV var is preferred. @@ -310,6 +368,12 @@ impl Command { gas_price_change_percent, min_gas_price, gas_price_threshold_percent, + min_da_gas_price, + max_da_gas_price, + da_gas_price_p_component, + da_gas_price_d_component, + da_committer_url, + da_poll_interval, consensus_key, #[cfg(feature = "aws-kms")] consensus_aws_kms, @@ -344,6 +408,12 @@ impl Command { info!("All metrics are disabled"); } + if max_da_gas_price < min_da_gas_price { + anyhow::bail!( + "The maximum DA gas price must be greater than or equal to the minimum DA gas price" + ); + } + let addr = net::SocketAddr::new(graphql.ip, graphql.port); let snapshot_reader = match snapshot.as_ref() { @@ -604,10 +674,10 @@ impl Command { coinbase_recipient, metrics: disabled_metrics.is_enabled(Module::Producer), }, - starting_gas_price, - gas_price_change_percent, - min_gas_price, - gas_price_threshold_percent, + starting_exec_gas_price: starting_gas_price, + exec_gas_price_change_percent: gas_price_change_percent, + min_exec_gas_price: min_gas_price, + exec_gas_price_threshold_percent: gas_price_threshold_percent, block_importer, da_compression, #[cfg(feature = "relayer")] @@ -624,6 +694,18 @@ impl Command { min_connected_reserved_peers, time_until_synced: time_until_synced.into(), 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: gas_price_change_percent, + da_gas_price_p_component, + da_gas_price_d_component, + activity_normal_range_size: 100, + activity_capped_range_size: 0, + activity_decrease_range_size: 0, + da_committer_url, + block_activity_threshold: 0, + da_poll_interval: da_poll_interval.map(Into::into), }; Ok(config) } diff --git a/ci_checks.sh b/ci_checks.sh index 50741bdf47e..9c2e95bd297 100755 --- a/ci_checks.sh +++ b/ci_checks.sh @@ -31,7 +31,7 @@ cargo make check --all-features --locked && cargo make check --locked && OVERRIDE_CHAIN_CONFIGS=true cargo test --test integration_tests local_node && cargo nextest run --workspace && -FUEL_ALWAYS_USE_WASM=true cargo nextest run --all-features --workspace && +FUEL_ALWAYS_USE_WASM=true cargo test run --all-features --workspace && cargo nextest run -p fuel-core --no-default-features && cargo nextest run -p fuel-core-client --no-default-features && cargo nextest run -p fuel-core-chain-config --no-default-features && diff --git a/crates/fuel-core/Cargo.toml b/crates/fuel-core/Cargo.toml index f63ae9c7a78..34a36fd6c20 100644 --- a/crates/fuel-core/Cargo.toml +++ b/crates/fuel-core/Cargo.toml @@ -68,6 +68,7 @@ tokio-util = { workspace = true } tower = { version = "0.4", features = ["limit"] } tower-http = { version = "0.4", features = ["set-header", "trace", "timeout"] } tracing = { workspace = true } +url = { workspace = true } uuid = { version = "1.1", features = ["v4"] } [dev-dependencies] diff --git a/crates/fuel-core/src/combined_database.rs b/crates/fuel-core/src/combined_database.rs index 9219ca486a0..64ce3459e83 100644 --- a/crates/fuel-core/src/combined_database.rs +++ b/crates/fuel-core/src/combined_database.rs @@ -284,6 +284,7 @@ impl CombinedDatabase { self.on_chain.check_version()?; self.off_chain.check_version()?; self.relayer.check_version()?; + self.gas_price.check_version()?; Ok(()) } diff --git a/crates/fuel-core/src/service.rs b/crates/fuel-core/src/service.rs index 535ebbba86c..628a3a9d6e4 100644 --- a/crates/fuel-core/src/service.rs +++ b/crates/fuel-core/src/service.rs @@ -1,17 +1,13 @@ -use self::adapters::BlockImporterAdapter; -use crate::{ - combined_database::{ - CombinedDatabase, - ShutdownListener, - }, - database::Database, - service::{ - adapters::{ - ExecutorAdapter, - PoAAdapter, - }, - sub_services::TxPoolSharedState, - }, +use std::{ + net::SocketAddr, + sync::Arc, +}; + +pub use config::{ + Config, + DbType, + RelayerConsensusConfig, + VMConfig, }; use fuel_core_chain_config::{ ConsensusConfig, @@ -21,6 +17,7 @@ use fuel_core_poa::{ ports::BlockImporter, verifier::verify_consensus, }; +pub use fuel_core_services::Service as ServiceTrait; use fuel_core_services::{ RunnableService, RunnableTask, @@ -40,18 +37,23 @@ use fuel_core_storage::{ StorageAsMut, }; use fuel_core_types::blockchain::consensus::Consensus; -use std::{ - net::SocketAddr, - sync::Arc, -}; -pub use config::{ - Config, - DbType, - RelayerConsensusConfig, - VMConfig, +use crate::{ + combined_database::{ + CombinedDatabase, + ShutdownListener, + }, + database::Database, + service::{ + adapters::{ + ExecutorAdapter, + PoAAdapter, + }, + sub_services::TxPoolSharedState, + }, }; -pub use fuel_core_services::Service as ServiceTrait; + +use self::adapters::BlockImporterAdapter; pub mod adapters; pub mod config; 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 aeb17627ccf..3acc97c9519 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 @@ -54,6 +54,10 @@ where fn next_gas_price(&self) -> u64 { self.algorithm.next_gas_price() } + + async fn get_worst_case_gas_price(&self, height: BlockHeight) -> u64 { + self.algorithm.worst_case_gas_price(height).await + } } #[async_trait::async_trait] @@ -81,6 +85,6 @@ where A: GasPriceAlgorithm + Send + Sync, { async fn worst_case_gas_price(&self, height: BlockHeight) -> Option { - Some(self.algorithm.worst_case_gas_price(height).await) + Some(self.get_worst_case_gas_price(height).await) } } 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 857c6ded988..cef071a1ecb 100644 --- a/crates/fuel-core/src/service/adapters/gas_price_adapters.rs +++ b/crates/fuel-core/src/service/adapters/gas_price_adapters.rs @@ -15,9 +15,9 @@ use fuel_core_gas_price_service::{ }, ports::{ GasPriceData, - GasPriceServiceConfig, L2Data, }, + v1::metadata::V1AlgorithmConfig, }; use fuel_core_storage::{ transactional::HistoricalView, @@ -62,14 +62,25 @@ impl GasPriceData for Database { } } -impl From for GasPriceServiceConfig { +impl From for V1AlgorithmConfig { fn from(value: Config) -> Self { - GasPriceServiceConfig::new_v0( - value.starting_gas_price, - value.min_gas_price, - value.gas_price_change_percent, - value.gas_price_threshold_percent, - ) + V1AlgorithmConfig { + new_exec_gas_price: value.starting_exec_gas_price, + 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.exec_gas_price_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, + da_p_component: value.da_gas_price_p_component, + da_d_component: value.da_gas_price_d_component, + normal_range_size: value.activity_normal_range_size, + capped_range_size: value.activity_capped_range_size, + decrease_range_size: value.activity_decrease_range_size, + block_activity_threshold: value.block_activity_threshold, + da_poll_interval: value.da_poll_interval, + gas_price_factor: value.da_gas_price_factor, + } } } diff --git a/crates/fuel-core/src/service/config.rs b/crates/fuel-core/src/service/config.rs index 02c463334f1..9505893e732 100644 --- a/crates/fuel-core/src/service/config.rs +++ b/crates/fuel-core/src/service/config.rs @@ -1,9 +1,9 @@ +use clap::ValueEnum; use std::{ + num::NonZeroU64, path::PathBuf, time::Duration, }; - -use clap::ValueEnum; use strum_macros::{ Display, EnumString, @@ -59,10 +59,12 @@ pub struct Config { pub vm: VMConfig, pub txpool: TxPoolConfig, pub block_producer: fuel_core_producer::Config, - pub starting_gas_price: u64, - pub gas_price_change_percent: u64, - pub min_gas_price: u64, - pub gas_price_threshold_percent: u64, + pub starting_exec_gas_price: u64, + pub exec_gas_price_change_percent: u16, + pub min_exec_gas_price: u64, + pub exec_gas_price_threshold_percent: u8, + pub da_committer_url: Option, + pub da_poll_interval: Option, pub da_compression: DaCompressionConfig, pub block_importer: fuel_core_importer::Config, #[cfg(feature = "relayer")] @@ -82,6 +84,16 @@ pub struct Config { pub time_until_synced: Duration, /// The size of the memory pool in number of `MemoryInstance`s. 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_gas_price_p_component: i64, + pub da_gas_price_d_component: i64, + pub activity_normal_range_size: u16, + pub activity_capped_range_size: u16, + pub activity_decrease_range_size: u16, + pub block_activity_threshold: u8, } impl Config { @@ -109,7 +121,6 @@ impl Config { #[cfg(feature = "test-helpers")] pub fn local_node_with_reader(snapshot_reader: SnapshotReader) -> Self { use crate::state::rocks_db::DatabaseConfig; - let block_importer = fuel_core_importer::Config::new(false); let latest_block = snapshot_reader.last_block_config(); // In tests, we always want to use the native executor as a default configuration. @@ -180,10 +191,10 @@ impl Config { ..Default::default() }, da_compression: DaCompressionConfig::Disabled, - starting_gas_price, - gas_price_change_percent, - min_gas_price, - gas_price_threshold_percent, + starting_exec_gas_price: starting_gas_price, + exec_gas_price_change_percent: gas_price_change_percent, + min_exec_gas_price: min_gas_price, + exec_gas_price_threshold_percent: gas_price_threshold_percent, block_importer, #[cfg(feature = "relayer")] relayer: None, @@ -201,6 +212,18 @@ impl Config { min_connected_reserved_peers: 0, time_until_synced: Duration::ZERO, 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_gas_price_p_component: 0, + da_gas_price_d_component: 0, + activity_normal_range_size: 0, + activity_capped_range_size: 0, + activity_decrease_range_size: 0, + da_committer_url: None, + block_activity_threshold: 0, + da_poll_interval: Some(Duration::from_secs(1)), } } diff --git a/crates/fuel-core/src/service/sub_services.rs b/crates/fuel-core/src/service/sub_services.rs index cf2ea92122b..f68dbb7d0af 100644 --- a/crates/fuel-core/src/service/sub_services.rs +++ b/crates/fuel-core/src/service/sub_services.rs @@ -38,14 +38,18 @@ use crate::{ SubServices, }, }; -use fuel_core_gas_price_service::v0::uninitialized_task::{ - new_gas_price_service_v0, - AlgorithmV0, +use fuel_core_gas_price_service::v1::{ + algorithm::AlgorithmV1, + da_source_service::block_committer_costs::{ + BlockCommitterDaBlockCosts, + BlockCommitterHttpApi, + }, + metadata::V1AlgorithmConfig, + uninitialized_task::new_gas_price_service_v1, }; use fuel_core_poa::Trigger; use fuel_core_storage::{ self, - structured_storage::StructuredStorage, transactional::AtomicView, }; #[cfg(feature = "relayer")] @@ -69,7 +73,7 @@ pub type BlockProducerService = fuel_core_producer::block_producer::Producer< Database, TxPoolAdapter, ExecutorAdapter, - FuelGasPriceProvider, + FuelGasPriceProvider, ConsensusParametersProvider, >; @@ -180,20 +184,22 @@ pub fn init_sub_services( let genesis_block_height = *genesis_block.header().height(); let settings = consensus_parameters_provider.clone(); let block_stream = importer_adapter.events_shared_result(); - let metadata = database.gas_price().clone(); - let gas_price_service_v0 = new_gas_price_service_v0( - config.clone().into(), + let committer_api = BlockCommitterHttpApi::new(config.da_committer_url.clone()); + let da_source = BlockCommitterDaBlockCosts::new(committer_api); + let v1_config = V1AlgorithmConfig::from(config.clone()); + + let gas_price_service_v1 = new_gas_price_service_v1( + v1_config, genesis_block_height, settings, block_stream, database.gas_price().clone(), - StructuredStorage::new(metadata), + da_source, database.on_chain().clone(), )?; - - let gas_price_provider = - FuelGasPriceProvider::new(gas_price_service_v0.shared.clone()); + let (gas_price_algo, _) = gas_price_service_v1.shared.clone(); + let gas_price_provider = FuelGasPriceProvider::new(gas_price_algo); let txpool = fuel_core_txpool::new_service( chain_id, config.txpool.clone(), @@ -343,7 +349,7 @@ pub fn init_sub_services( #[allow(unused_mut)] // `FuelService` starts and shutdowns all sub-services in the `services` order let mut services: SubServices = vec![ - Box::new(gas_price_service_v0), + Box::new(gas_price_service_v1), Box::new(txpool), Box::new(consensus_parameters_provider_service), ]; diff --git a/crates/fuel-gas-price-algorithm/gas-price-analysis/src/charts.rs b/crates/fuel-gas-price-algorithm/gas-price-analysis/src/charts.rs index 41f0fe8fab6..aa3f401ddcc 100644 --- a/crates/fuel-gas-price-algorithm/gas-price-analysis/src/charts.rs +++ b/crates/fuel-gas-price-algorithm/gas-price-analysis/src/charts.rs @@ -191,16 +191,16 @@ pub fn draw_fullness( pub fn draw_bytes_and_cost_per_block( drawing_area: &DrawingArea, - bytes_and_costs_per_block: &[(u64, u64)], + bytes_and_costs_per_block: &[(u32, u64)], title: &str, ) -> anyhow::Result<()> { const BYTES_PER_BLOCK_COLOR: RGBColor = BLACK; - let (bytes, costs): (Vec, Vec) = + let (bytes, costs): (Vec, Vec) = bytes_and_costs_per_block.iter().cloned().unzip(); let costs_gwei: Vec<_> = costs.into_iter().map(|x| x / ONE_GWEI).collect(); let min = 0; - let max_left = *bytes.iter().max().unwrap(); + let max_left = *bytes.iter().max().unwrap() as u64; let max_right = *costs_gwei.iter().max().unwrap(); let mut chart = ChartBuilder::on(drawing_area) @@ -228,7 +228,7 @@ pub fn draw_bytes_and_cost_per_block( chart .draw_series(LineSeries::new( - bytes.iter().enumerate().map(|(x, y)| (x, *y)), + bytes.iter().enumerate().map(|(x, y)| (x, *y as u64)), BYTES_PER_BLOCK_COLOR, )) .unwrap() diff --git a/crates/fuel-gas-price-algorithm/gas-price-analysis/src/main.rs b/crates/fuel-gas-price-algorithm/gas-price-analysis/src/main.rs index ae15233f37f..998f508151b 100644 --- a/crates/fuel-gas-price-algorithm/gas-price-analysis/src/main.rs +++ b/crates/fuel-gas-price-algorithm/gas-price-analysis/src/main.rs @@ -186,9 +186,9 @@ fn print_info(results: &SimulationResults) { .enumerate() .max_by(|(_, a), (_, b)| a.cmp(b)) .unwrap(); - let eth = *max_da_gas_price as f64 / (10_f64).powf(18.); + let eth = *max_da_gas_price as f64 / (10_f64).powf(9.); println!( - "max DA gas price: {} Wei ({} ETH) at {}", + "max DA gas price: {} Wei ({} Gwei) at {}", max_da_gas_price, eth, index ); @@ -198,9 +198,9 @@ fn print_info(results: &SimulationResults) { .enumerate() .min_by(|(_, a), (_, b)| a.cmp(b)) .unwrap(); - let eth = *min_da_gas_price as f64 / (10_f64).powf(18.); + let eth = *min_da_gas_price as f64 / (10_f64).powf(9.); println!( - "min DA gas price: {} Wei ({} ETH) at {}", + "min DA gas price: {} Wei ({} Gwei) at {}", min_da_gas_price, eth, index ); } diff --git a/crates/fuel-gas-price-algorithm/gas-price-analysis/src/simulation.rs b/crates/fuel-gas-price-algorithm/gas-price-analysis/src/simulation.rs index 92003cebb19..c4095d05f2c 100644 --- a/crates/fuel-gas-price-algorithm/gas-price-analysis/src/simulation.rs +++ b/crates/fuel-gas-price-algorithm/gas-price-analysis/src/simulation.rs @@ -1,14 +1,15 @@ -use super::*; -use fuel_gas_price_algorithm::v1::{ - AlgorithmUpdaterV1, - L2ActivityTracker, -}; use std::{ collections::BTreeMap, num::NonZeroU64, - ops::Range, }; +use fuel_gas_price_algorithm::v1::{ + AlgorithmUpdaterV1, + L2ActivityTracker, +}; + +use super::*; + pub mod da_cost_per_byte; pub struct SimulationResults { @@ -16,7 +17,7 @@ pub struct SimulationResults { pub exec_gas_prices: Vec, pub da_gas_prices: Vec, pub fullness: Vec<(u64, u64)>, - pub bytes_and_costs: Vec<(u64, u64)>, + pub bytes_and_costs: Vec<(u32, u64)>, pub actual_profit: Vec, pub projected_profit: Vec, pub pessimistic_costs: Vec, @@ -30,8 +31,15 @@ pub struct Simulator { // (usize, ((u64, u64), &'a Option<(Range, u128) struct BlockData { fullness: u64, - bytes: u64, - maybe_da_block: Option<(Range, u128)>, + bytes: u32, + maybe_da_bundle: Option, +} + +#[derive(Clone, Debug)] +struct Bundle { + heights: Vec, + bytes: u32, + cost: u128, } impl Simulator { @@ -51,7 +59,6 @@ impl Simulator { let max_block_bytes = capacity / gas_per_byte; let size = self.da_cost_per_byte.len(); let fullness_and_bytes = fullness_and_bytes_per_block(size, capacity); - let l2_blocks = fullness_and_bytes.clone().into_iter(); let da_blocks = self.calculate_da_blocks( update_period, @@ -64,7 +71,7 @@ impl Simulator { .map(|((fullness, bytes), maybe_da_block)| BlockData { fullness, bytes, - maybe_da_block: maybe_da_block.clone(), + maybe_da_bundle: maybe_da_block.clone(), }) .enumerate(); @@ -88,12 +95,12 @@ impl Simulator { let gas_price_factor = 100; let always_normal_activity = L2ActivityTracker::new_always_normal(); AlgorithmUpdaterV1 { - min_exec_gas_price: 10, - min_da_gas_price: 10, + min_exec_gas_price: 10_000_000, + min_da_gas_price: 100_000_000, // Change to adjust where the exec gas price starts on block 0 - new_scaled_exec_price: 10 * gas_price_factor, + new_scaled_exec_price: 10_000_000 * gas_price_factor, // Change to adjust where the da gas price starts on block 0 - new_scaled_da_gas_price: 100, + new_scaled_da_gas_price: 10_000_000 * gas_price_factor, gas_price_factor: NonZeroU64::new(gas_price_factor).unwrap(), l2_block_height: 0, // Choose the ideal fullness percentage for the L2 block @@ -101,13 +108,12 @@ impl Simulator { // Increase to make the exec price change faster exec_gas_price_change_percent: 2, // Increase to make the da price change faster - max_da_gas_price_change_percent: 10, + max_da_gas_price_change_percent: 15, total_da_rewards_excess: 0, // Change to adjust the cost per byte of the DA on block 0 latest_da_cost_per_byte: 0, projected_total_da_cost: 0, latest_known_total_da_cost_excess: 0, - unrecorded_blocks: BTreeMap::new(), da_p_component, da_d_component, last_profit: 0, @@ -121,7 +127,7 @@ impl Simulator { &self, capacity: u64, max_block_bytes: u64, - fullness_and_bytes: Vec<(u64, u64)>, + fullness_and_bytes: Vec<(u64, u32)>, blocks: impl Iterator, mut updater: AlgorithmUpdaterV1, ) -> SimulationResults { @@ -132,11 +138,12 @@ impl Simulator { let mut projected_cost_totals = vec![]; let mut actual_costs = vec![]; let mut pessimistic_costs = vec![]; + let mut unrecorded_blocks = BTreeMap::new(); for (index, block_data) in blocks { let BlockData { fullness, bytes, - maybe_da_block: da_block, + maybe_da_bundle, } = block_data; let height = index as u32 + 1; exec_gas_prices.push(updater.new_scaled_exec_price); @@ -149,8 +156,9 @@ impl Simulator { height, fullness, capacity.try_into().unwrap(), - bytes, + bytes as u64, total_fee, + &mut unrecorded_blocks, ) .unwrap(); pessimistic_costs @@ -159,10 +167,22 @@ impl Simulator { projected_cost_totals.push(updater.projected_total_da_cost); // Update DA blocks on the occasion there is one - if let Some((range, cost)) = da_block { - for height in range.to_owned() { + if let Some(bundle) = maybe_da_bundle { + let Bundle { + heights, + bytes, + cost, + } = bundle; + for height in heights { let block_heights: Vec = (height..(height) + 1).collect(); - updater.update_da_record_data(&block_heights, cost).unwrap(); + updater + .update_da_record_data( + &block_heights, + bytes, + cost, + &mut unrecorded_blocks, + ) + .unwrap(); actual_costs.push(updater.latest_known_total_da_cost_excess) } } @@ -176,7 +196,7 @@ impl Simulator { let bytes_and_costs: Vec<_> = bytes .iter() .zip(self.da_cost_per_byte.iter()) - .map(|(bytes, da_cost_per_byte)| (*bytes, *bytes * da_cost_per_byte)) + .map(|(bytes, da_cost_per_byte)| (*bytes, *bytes as u64 * da_cost_per_byte)) .collect(); let actual_profit: Vec = actual_costs @@ -207,8 +227,8 @@ impl Simulator { &self, da_recording_rate: usize, da_finalization_rate: usize, - fullness_and_bytes: &[(u64, u64)], - ) -> Vec, u128)>> { + fullness_and_bytes: &[(u64, u32)], + ) -> Vec> { let l2_blocks_with_no_da_blocks = std::iter::repeat(None).take(da_finalization_rate); let (_, da_blocks) = fullness_and_bytes @@ -219,8 +239,7 @@ impl Simulator { (vec![], vec![]), |(mut delayed, mut recorded), (index, ((_fullness, bytes), cost_per_byte))| { - let total_cost = *bytes * cost_per_byte; - + let total_cost = *bytes as u64 * cost_per_byte; let height = index as u32 + 1; let converted = (height, bytes, total_cost); delayed.push(converted); @@ -236,10 +255,13 @@ impl Simulator { let da_block_ranges = da_blocks.into_iter().map(|maybe_recorded_blocks| { maybe_recorded_blocks.map(|list| { let heights_iter = list.iter().map(|(height, _, _)| *height); - let min = heights_iter.clone().min().unwrap(); - let max = heights_iter.max().unwrap(); - let cost: u128 = list.iter().map(|(_, _, cost)| *cost as u128).sum(); - (min..(max + 1), cost) + let bytes = list.iter().map(|(_, bytes, _)| *bytes).sum(); + let cost = list.iter().map(|(_, _, cost)| *cost as u128).sum(); + Bundle { + heights: heights_iter.collect(), + bytes, + cost, + } }) }); l2_blocks_with_no_da_blocks.chain(da_block_ranges).collect() @@ -263,13 +285,12 @@ where gen_noisy_signal(input, COMPONENTS) } -fn fullness_and_bytes_per_block(size: usize, capacity: u64) -> Vec<(u64, u64)> { +fn fullness_and_bytes_per_block(size: usize, capacity: u64) -> Vec<(u64, u32)> { let mut rng = StdRng::seed_from_u64(888); let fullness_noise: Vec<_> = std::iter::repeat(()) .take(size) - .map(|_| rng.gen_range(-0.25..0.25)) - .map(|val| val * capacity as f64) + .map(|_| rng.gen_range(-0.5..0.5)) .collect(); const ROUGH_GAS_TO_BYTE_RATIO: f64 = 0.01; @@ -282,7 +303,7 @@ fn fullness_and_bytes_per_block(size: usize, capacity: u64) -> Vec<(u64, u64)> { (0usize..size) .map(|val| val as f64) .map(noisy_fullness) - .map(|signal| (0.5 * signal + 0.5) * capacity as f64) // Scale and shift so it's between 0 and capacity + .map(|signal| (0.01 * signal + 0.01) * capacity as f64) // Scale and shift so it's between 0 and capacity .zip(fullness_noise) .map(|(fullness, noise)| fullness + noise) .map(|x| f64::min(x, capacity as f64)) @@ -292,6 +313,6 @@ fn fullness_and_bytes_per_block(size: usize, capacity: u64) -> Vec<(u64, u64)> { let bytes = fullness * bytes_scale; (fullness, bytes) }) - .map(|(fullness, bytes)| (fullness as u64, std::cmp::max(bytes as u64, 1))) + .map(|(fullness, bytes)| (fullness as u64, std::cmp::max(bytes as u32, 1))) .collect() } diff --git a/crates/fuel-gas-price-algorithm/gas-price-analysis/src/simulation/da_cost_per_byte.rs b/crates/fuel-gas-price-algorithm/gas-price-analysis/src/simulation/da_cost_per_byte.rs index 8d4353300be..911dcf713d8 100644 --- a/crates/fuel-gas-price-algorithm/gas-price-analysis/src/simulation/da_cost_per_byte.rs +++ b/crates/fuel-gas-price-algorithm/gas-price-analysis/src/simulation/da_cost_per_byte.rs @@ -53,8 +53,9 @@ fn get_costs_from_csv_file(file_path: &str, sample_size: Option) -> Vec= size { + if costs.len() >= size / L2_BLOCKS_PER_L1_BLOCK { break; } }; diff --git a/crates/fuel-gas-price-algorithm/src/v1.rs b/crates/fuel-gas-price-algorithm/src/v1.rs index ac670d7b38f..2ac241a53a1 100644 --- a/crates/fuel-gas-price-algorithm/src/v1.rs +++ b/crates/fuel-gas-price-algorithm/src/v1.rs @@ -1,9 +1,15 @@ use crate::utils::cumulative_percentage_change; use std::{ - cmp::max, + cmp::{ + max, + min, + }, collections::BTreeMap, num::NonZeroU64, - ops::Div, + ops::{ + Div, + RangeInclusive, + }, }; #[cfg(test)] @@ -144,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, @@ -269,9 +277,9 @@ impl L2ActivityTracker { } pub fn safety_mode(&self) -> DAGasPriceSafetyMode { - if self.chain_activity > self.capped_activity_threshold { + if self.chain_activity >= self.capped_activity_threshold { DAGasPriceSafetyMode::Normal - } else if self.chain_activity > self.decrease_activity_threshold { + } else if self.chain_activity >= self.decrease_activity_threshold { DAGasPriceSafetyMode::Capped } else { DAGasPriceSafetyMode::AlwaysDecrease @@ -280,6 +288,11 @@ impl L2ActivityTracker { pub fn update(&mut self, block_usage: ClampedPercentage) { if block_usage < self.block_activity_threshold { + tracing::debug!( + "Decreasing activity {:?} < {:?}", + block_usage, + self.block_activity_threshold + ); self.chain_activity = self.chain_activity.saturating_sub(1); } else { self.chain_activity = @@ -341,7 +354,7 @@ impl core::ops::Deref for ClampedPercentage { impl AlgorithmUpdaterV1 { pub fn update_da_record_data( &mut self, - heights: &[u32], + heights: RangeInclusive, recorded_bytes: u32, recording_cost: u128, unrecorded_blocks: &mut U, @@ -462,7 +475,7 @@ impl AlgorithmUpdaterV1 { .unwrap_or(threshold); match fullness_percent.cmp(&threshold) { - std::cmp::Ordering::Greater => { + std::cmp::Ordering::Greater | std::cmp::Ordering::Equal => { let change_amount = self.exec_change(scaled_exec_gas_price); scaled_exec_gas_price = scaled_exec_gas_price.saturating_add(change_amount); @@ -472,7 +485,6 @@ impl AlgorithmUpdaterV1 { scaled_exec_gas_price = scaled_exec_gas_price.saturating_sub(change_amount); } - std::cmp::Ordering::Equal => {} } self.new_scaled_exec_price = max(self.min_scaled_exec_gas_price(), scaled_exec_gas_price); @@ -486,21 +498,33 @@ impl AlgorithmUpdaterV1 { fn update_da_gas_price(&mut self) { let p = self.p(); let d = self.d(); - let maybe_da_change = self.da_change(p, d); - let da_change = self.da_change_accounting_for_activity(maybe_da_change); + let maybe_scaled_da_change = self.da_change(p, d); + let scaled_da_change = + self.da_change_accounting_for_activity(maybe_scaled_da_change); let maybe_new_scaled_da_gas_price = i128::from(self.new_scaled_da_gas_price) - .checked_add(da_change) + .checked_add(scaled_da_change) .and_then(|x| u64::try_from(x).ok()) .unwrap_or_else(|| { - if da_change.is_positive() { + if scaled_da_change.is_positive() { u64::MAX } else { 0u64 } }); - self.new_scaled_da_gas_price = max( - self.min_scaled_da_gas_price(), - maybe_new_scaled_da_gas_price, + tracing::debug!("Profit: {}", self.last_profit); + tracing::debug!( + "DA gas price change: p: {}, d: {}, change: {}, new: {}", + p, + d, + scaled_da_change, + 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(), ); } @@ -510,6 +534,7 @@ impl AlgorithmUpdaterV1 { DAGasPriceSafetyMode::Normal => maybe_da_change, DAGasPriceSafetyMode::Capped => 0, DAGasPriceSafetyMode::AlwaysDecrease => { + tracing::info!("Activity is low, decreasing DA gas price"); self.max_change().saturating_mul(-1) } } @@ -523,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 the correct maximum is used + 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); @@ -539,10 +570,12 @@ impl AlgorithmUpdaterV1 { } fn da_change(&self, p: i128, d: i128) -> i128 { - let pd_change = p.saturating_add(d); + let scaled_pd_change = p + .saturating_add(d) + .saturating_mul(self.gas_price_factor.get() as i128); let max_change = self.max_change(); - let clamped_change = pd_change.saturating_abs().min(max_change); - pd_change.signum().saturating_mul(clamped_change) + let clamped_change = scaled_pd_change.saturating_abs().min(max_change); + scaled_pd_change.signum().saturating_mul(clamped_change) } // Should always be positive @@ -562,7 +595,7 @@ impl AlgorithmUpdaterV1 { fn da_block_update( &mut self, - heights: &[u32], + heights: RangeInclusive, recorded_bytes: u128, recording_cost: u128, unrecorded_blocks: &mut U, @@ -591,13 +624,13 @@ impl AlgorithmUpdaterV1 { // Always remove the blocks from the unrecorded blocks so they don't build up indefinitely fn update_unrecorded_block_bytes( &mut self, - heights: &[u32], + heights: RangeInclusive, unrecorded_blocks: &mut U, ) -> Result<(), Error> { let mut total: u128 = 0; for expected_height in heights { let maybe_bytes = unrecorded_blocks - .remove(expected_height) + .remove(&expected_height) .map_err(Error::CouldNotRemoveUnrecordedBlock)?; if let Some(bytes) = maybe_bytes { diff --git a/crates/fuel-gas-price-algorithm/src/v1/tests.rs b/crates/fuel-gas-price-algorithm/src/v1/tests.rs index c2dcaba36fa..10f3ac9a775 100644 --- a/crates/fuel-gas-price-algorithm/src/v1/tests.rs +++ b/crates/fuel-gas-price-algorithm/src/v1/tests.rs @@ -26,10 +26,11 @@ 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, - max_change_percent: u16, + max_da_change_percent: u16, da_p_component: i64, da_d_component: i64, @@ -53,10 +54,11 @@ impl UpdaterBuilder { Self { min_exec_gas_price: 0, min_da_gas_price: 0, + max_da_gas_price: u64::MAX, starting_exec_gas_price: 1, starting_da_gas_price: 1, exec_gas_price_change_percent: 0, - max_change_percent: u16::MAX, + max_da_change_percent: u16::MAX, da_p_component: 0, da_d_component: 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 @@ -102,7 +109,7 @@ impl UpdaterBuilder { } fn with_da_max_change_percent(mut self, max_change_percent: u16) -> Self { - self.max_change_percent = max_change_percent; + self.max_da_change_percent = max_change_percent; self } @@ -177,7 +184,7 @@ impl UpdaterBuilder { new_scaled_exec_price: self.starting_exec_gas_price, new_scaled_da_gas_price: self.starting_da_gas_price, exec_gas_price_change_percent: self.exec_gas_price_change_percent, - max_da_gas_price_change_percent: self.max_change_percent, + max_da_gas_price_change_percent: self.max_da_change_percent, da_p_component: self.da_p_component, da_d_component: self.da_d_component, @@ -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 6d1feb220b4..162683e17bc 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 @@ -5,7 +5,7 @@ use std::collections::BTreeMap; fn update_da_record_data__if_receives_batch_with_unknown_blocks_will_include_known_blocks_with_previous_cost( ) { // given - let recorded_heights: Vec = (1u32..3).collect(); + let recorded_heights = 1u32..=2; let recorded_cost = 1_000_000; let recorded_bytes = 500; let block_bytes = 1000; @@ -22,7 +22,7 @@ fn update_da_record_data__if_receives_batch_with_unknown_blocks_will_include_kno // when updater .update_da_record_data( - &recorded_heights, + recorded_heights, recorded_bytes, recorded_cost, &mut unrecorded_blocks, @@ -41,7 +41,7 @@ fn update_da_record_data__if_receives_batch_with_unknown_blocks_will_include_kno fn update_da_record_data__if_receives_batch_with_unknown_blocks_will_never_increase_cost_more_than_recorded_cost( ) { // given - let recorded_heights: Vec = (1u32..3).collect(); + let recorded_heights = 1u32..=2; let recorded_cost = 200; let block_bytes = 1000; let recorded_bytes = 500; @@ -58,7 +58,7 @@ fn update_da_record_data__if_receives_batch_with_unknown_blocks_will_never_incre // when updater .update_da_record_data( - &recorded_heights, + recorded_heights, recorded_bytes, recorded_cost, &mut unrecorded_blocks, @@ -87,11 +87,11 @@ fn update_da_record_data__updates_cost_per_byte() { let new_cost_per_byte = 100; let recorded_bytes = 500; let recorded_cost = (recorded_bytes * new_cost_per_byte) as u128; - let recorded_heights: Vec = (1u32..2).collect(); + let recorded_heights = 1u32..=1; // when updater .update_da_record_data( - &recorded_heights, + recorded_heights, recorded_bytes, recorded_cost, &mut unrecorded_blocks, @@ -121,13 +121,13 @@ fn update_da_record_data__updates_known_total_cost() { .with_unrecorded_blocks(&unrecorded_blocks) .build(); - let recorded_heights: Vec = (11u32..14).collect(); + let recorded_heights = 11u32..=13; let recorded_bytes = 500; let recorded_cost = 300; // when updater .update_da_record_data( - &recorded_heights, + recorded_heights, recorded_bytes, recorded_cost, &mut unrecorded_blocks, @@ -166,13 +166,13 @@ fn update_da_record_data__if_da_height_matches_l2_height_projected_and_known_mat let new_cost_per_byte = 100; let block_cost = block_bytes * new_cost_per_byte; - let recorded_heights: Vec = (11u32..14).collect(); + let recorded_heights = 11u32..=13; let recorded_bytes = 500; let recorded_cost = block_cost * 3; // when updater .update_da_record_data( - &recorded_heights, + recorded_heights, recorded_bytes, recorded_cost, &mut unrecorded_blocks, @@ -216,7 +216,7 @@ fn update_da_record_data__da_block_updates_projected_total_cost_with_known_and_g .build(); let new_cost_per_byte = 100; - let recorded_heights = vec![11, 12, 13]; + let recorded_heights = 11u32..=13; let recorded_bytes = 500; let recorded_cost = recorded_bytes * new_cost_per_byte; let recorded_bytes = 500; @@ -224,7 +224,7 @@ fn update_da_record_data__da_block_updates_projected_total_cost_with_known_and_g // when updater .update_da_record_data( - &recorded_heights, + recorded_heights, recorded_bytes, recorded_cost, &mut unrecorded_blocks, @@ -243,76 +243,49 @@ fn update_da_record_data__da_block_updates_projected_total_cost_with_known_and_g } #[test] -fn update_da_record_data__updates_known_total_cost_if_blocks_are_out_of_order() { +fn update_da_record_data__da_block_lowers_da_gas_price() { // given - let da_cost_per_byte = 20; - let block_bytes = 1000; - let mut unrecorded_blocks: BTreeMap<_, _> = - [(1, block_bytes), (2, block_bytes), (3, block_bytes)] - .into_iter() - .collect(); - let old_known_total_cost = 500; - let old_projected_total_cost = - old_known_total_cost + (block_bytes as u128 * da_cost_per_byte * 3); - let old_da_cost_per_byte = 20; - let mut updater = UpdaterBuilder::new() - .with_da_cost_per_byte(da_cost_per_byte) - .with_unrecorded_blocks(&unrecorded_blocks) - .with_da_cost_per_byte(old_da_cost_per_byte) - .with_known_total_cost(old_known_total_cost) - .with_projected_total_cost(old_projected_total_cost) - .build(); - let new_cost_per_byte = 100; - let recorded_bytes = 500; - let recorded_cost = recorded_bytes * new_cost_per_byte; - let recorded_heights: Vec = vec![3, 2]; - - // when - updater - .update_da_record_data( - &recorded_heights, - recorded_bytes, - recorded_cost as u128, - &mut unrecorded_blocks, - ) - .unwrap(); - - // then - let expected = updater.latest_known_total_da_cost_excess - + (block_bytes as u128 * new_cost_per_byte as u128); - let actual = updater.projected_total_da_cost; - assert_eq!(actual, expected); -} + 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; -#[test] -fn update_da_record_data__updates_projected_total_cost_if_blocks_are_out_of_order() { - // given - let da_cost_per_byte = 20; - let block_bytes = 1000; - let mut unrecorded_blocks: BTreeMap<_, _> = - [(1, block_bytes), (2, block_bytes), (3, block_bytes)] - .into_iter() - .collect(); - let old_known_total_cost = 500; - let old_projected_total_cost = - old_known_total_cost + (block_bytes as u128 * da_cost_per_byte * 3); - let old_da_cost_per_byte = 20; + let old_da_gas_price = 1; let mut updater = UpdaterBuilder::new() - .with_da_cost_per_byte(da_cost_per_byte) + .with_starting_da_gas_price(old_da_gas_price) + .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_da_cost_per_byte(old_da_cost_per_byte) - .with_known_total_cost(old_known_total_cost) - .with_projected_total_cost(old_projected_total_cost) .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 recorded_cost = recorded_bytes * new_cost_per_byte; - let recorded_heights: Vec = vec![3, 2]; + tracing::info!("old_da_gas_price: {}", old_da_gas_price); // when updater .update_da_record_data( - &recorded_heights, + recorded_range, recorded_bytes, recorded_cost as u128, &mut unrecorded_blocks, @@ -320,49 +293,74 @@ fn update_da_record_data__updates_projected_total_cost_if_blocks_are_out_of_orde .unwrap(); // then - let expected = updater.latest_known_total_da_cost_excess - + (block_bytes as u128 * new_cost_per_byte as u128); - let actual = updater.projected_total_da_cost; - assert_eq!(actual, expected); + let actual = updater.new_scaled_da_gas_price; + let change = (updater.last_profit / da_p_component as i128) as u64; + let expected = old_da_gas_price.saturating_sub(change); + assert_eq!(expected, actual); } #[test] -fn update_da_record_data__updates_unrecorded_blocks() { +fn update_da_record_data__da_block_increases_da_gas_price() { // given - let da_cost_per_byte = 20; - let block_bytes = 1000; - let mut unrecorded_blocks: BTreeMap<_, _> = - [(1, block_bytes), (2, block_bytes), (3, block_bytes)] - .into_iter() - .collect(); + 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 old_da_gas_price = 1; let mut updater = UpdaterBuilder::new() - .with_da_cost_per_byte(da_cost_per_byte) + .with_starting_da_gas_price(old_da_gas_price) + .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) .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 recorded_cost = 2 * (recorded_bytes * new_cost_per_byte) as u128; - let recorded_heights: Vec = vec![3, 2]; // when updater .update_da_record_data( - &recorded_heights, + recorded_range, recorded_bytes, - recorded_cost, + recorded_cost as u128, &mut unrecorded_blocks, ) .unwrap(); // then - let expected = vec![(1, block_bytes)]; - let actual: Vec<_> = unrecorded_blocks.into_iter().collect(); - assert_eq!(actual, expected); + let actual = updater.new_scaled_da_gas_price; + let change = (updater.last_profit / da_p_component as i128).unsigned_abs() as u64; + let expected = old_da_gas_price + change; + assert_eq!(expected, actual); } #[test] -fn update_da_record_data__da_block_lowers_da_gas_price() { +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; @@ -377,11 +375,13 @@ fn update_da_record_data__da_block_lowers_da_gas_price() { 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_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; @@ -392,9 +392,10 @@ fn update_da_record_data__da_block_lowers_da_gas_price() { (range, cost + bytes * new_cost_per_byte) }, ); + let min = *recorded_heights.iter().min().unwrap(); let max = *recorded_heights.iter().max().unwrap(); - let recorded_range: Vec = (*min..(max + 1)).collect(); + let recorded_range = *min..=*max; let recorded_bytes = 500; let old_da_gas_price = updater.new_scaled_da_gas_price; @@ -402,7 +403,7 @@ fn update_da_record_data__da_block_lowers_da_gas_price() { // when updater .update_da_record_data( - &recorded_range, + recorded_range, recorded_bytes, recorded_cost as u128, &mut unrecorded_blocks, @@ -411,14 +412,16 @@ fn update_da_record_data__da_block_lowers_da_gas_price() { // 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 lesser than the previous one. - assert_eq!(new_da_gas_price, 0); + // 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__da_block_increases_da_gas_price() { +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; @@ -438,6 +441,8 @@ 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_min_da_gas_price(min_da_gas_price) + .with_max_da_gas_price(max_da_gas_price) .build(); let new_cost_per_byte = 100; @@ -451,15 +456,13 @@ fn update_da_record_data__da_block_increases_da_gas_price() { let min = *recorded_heights.iter().min().unwrap(); let max = *recorded_heights.iter().max().unwrap(); - let recorded_range: Vec = (*min..(max + 1)).collect(); + 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_range, recorded_bytes, recorded_cost as u128, &mut unrecorded_blocks, @@ -468,9 +471,9 @@ fn update_da_record_data__da_block_increases_da_gas_price() { // 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, 6); - assert_ne!(old_da_gas_price, new_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] @@ -507,7 +510,7 @@ fn update_da_record_data__da_block_will_not_change_da_gas_price() { ); let min = *recorded_heights.iter().min().unwrap(); let max = *recorded_heights.iter().max().unwrap(); - let recorded_range: Vec = (*min..(max + 1)).collect(); + let recorded_range = *min..=*max; let recorded_bytes = 500; let old_da_gas_price = updater.new_scaled_da_gas_price; @@ -515,7 +518,7 @@ fn update_da_record_data__da_block_will_not_change_da_gas_price() { // when updater .update_da_record_data( - &recorded_range, + recorded_range, recorded_bytes, recorded_cost as u128, &mut unrecorded_blocks, 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 6f0dbc4debe..f4555551579 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 @@ -199,12 +199,12 @@ fn update_l2_block_data__updates_the_total_reward_value() { } #[test] -fn update_l2_block_data__even_threshold_will_not_change_exec_gas_price() { +fn update_l2_block_data__even_threshold_will_increase_exec_gas_price() { // given - let starting_gas_price = 100; + let starting_exec_gas_price = 100; let unused_percent = 11; let mut updater = UpdaterBuilder::new() - .with_starting_exec_gas_price(starting_gas_price) + .with_starting_exec_gas_price(starting_exec_gas_price) .with_exec_gas_price_change_percent(unused_percent) .build(); @@ -221,7 +221,8 @@ fn update_l2_block_data__even_threshold_will_not_change_exec_gas_price() { .unwrap(); // then - let expected = starting_gas_price; + let expected_change = starting_exec_gas_price * unused_percent as u64 / 100; + let expected = starting_exec_gas_price + expected_change; let actual = updater.new_scaled_exec_price; assert_eq!(actual, expected); } diff --git a/crates/services/gas_price_service/Cargo.toml b/crates/services/gas_price_service/Cargo.toml index 9903bba7d03..385f24a668c 100644 --- a/crates/services/gas_price_service/Cargo.toml +++ b/crates/services/gas_price_service/Cargo.toml @@ -17,18 +17,26 @@ fuel-core-storage = { workspace = true, features = ["std"] } fuel-core-types = { workspace = true, features = ["std"] } fuel-gas-price-algorithm = { workspace = true } futures = { workspace = true } +mockito = { version = "1.6.1", optional = true } num_enum = { workspace = true } parking_lot = { workspace = true } reqwest = { workspace = true, features = ["json"] } serde = { workspace = true } +serde_json = { workspace = true, optional = true } strum = { workspace = true, features = ["derive"] } strum_macros = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } tokio-stream = { workspace = true } tracing = { workspace = true } +url = { workspace = true } [dev-dependencies] fuel-core-services = { workspace = true, features = ["test-helpers"] } fuel-core-storage = { workspace = true, features = ["test-helpers"] } fuel-core-types = { path = "./../../types", features = ["test-helpers"] } +mockito = { version = "1.6.1" } +serde_json = { workspace = true } + +[features] +test-helpers = ["dep:mockito", "dep:serde_json"] 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 506885fe78b..b9eebdd9f5d 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 @@ -1,9 +1,9 @@ use crate::{ common::{ fuel_core_storage_adapter::storage::{ - BundleIdTable, GasPriceColumn, GasPriceMetadata, + RecordedHeights, }, updater_metadata::UpdaterMetadata, utils::{ @@ -14,9 +14,9 @@ use crate::{ }, ports::{ GasPriceServiceAtomicStorage, - GetDaBundleId, + GetLatestRecordedHeight, GetMetadataStorage, - SetDaBundleId, + SetLatestRecordedHeight, SetMetadataStorage, }, }; @@ -99,25 +99,26 @@ where } } -impl GetDaBundleId for Storage +impl GetLatestRecordedHeight for Storage where Storage: Send + Sync, - Storage: StorageInspect, + Storage: StorageInspect, { - fn get_bundle_id(&self, block_height: &BlockHeight) -> GasPriceResult> { - let bundle_id = self - .storage::() - .get(block_height) - .map_err(|err| GasPriceError::CouldNotFetchDARecord(err.into()))? + fn get_recorded_height(&self) -> GasPriceResult> { + const KEY: &() = &(); + let recorded_height = self + .storage::() + .get(KEY) + .map_err(|err| GasPriceError::CouldNotFetchRecordedHeight(err.into()))? .map(|no| *no); - Ok(bundle_id) + Ok(recorded_height) } } impl GasPriceServiceAtomicStorage for Storage where Storage: 'static, - Storage: GetMetadataStorage + GetDaBundleId, + Storage: GetMetadataStorage + GetLatestRecordedHeight, Storage: KeyValueInspect + Modifiable + Send + Sync, { type Transaction<'a> = StorageTransaction<&'a mut Storage> where Self: 'a; @@ -135,19 +136,19 @@ where } } -impl SetDaBundleId for Storage +impl SetLatestRecordedHeight for Storage where Storage: Send + Sync, - Storage: StorageMutate, + Storage: StorageMutate, { - fn set_bundle_id( + fn set_recorded_height( &mut self, - block_height: &BlockHeight, - bundle_id: u32, + recorded_height: BlockHeight, ) -> GasPriceResult<()> { - self.storage_as_mut::() - .insert(block_height, &bundle_id) - .map_err(|err| GasPriceError::CouldNotFetchDARecord(err.into()))?; + const KEY: &() = &(); + self.storage_as_mut::() + .insert(KEY, &recorded_height) + .map_err(|err| GasPriceError::CouldNotSetRecordedHeight(err.into()))?; Ok(()) } } diff --git a/crates/services/gas_price_service/src/common/fuel_core_storage_adapter/storage.rs b/crates/services/gas_price_service/src/common/fuel_core_storage_adapter/storage.rs index e9f20b411d8..dcd65a8bd83 100644 --- a/crates/services/gas_price_service/src/common/fuel_core_storage_adapter/storage.rs +++ b/crates/services/gas_price_service/src/common/fuel_core_storage_adapter/storage.rs @@ -28,7 +28,7 @@ pub enum GasPriceColumn { Metadata = 0, State = 1, UnrecordedBlocks = 2, - BundleId = 3, + LatestRecordedHeight = 3, } impl GasPriceColumn { @@ -93,23 +93,21 @@ impl TableWithBlueprint for UnrecordedBlocksTable { } } -pub struct BundleIdTable; +/// Used to store the latest L2 block that has been recorded on the DA chain +pub struct RecordedHeights; -/// The sequence number or bundle id of the posted blocks. -type BundleId = u32; - -impl Mappable for BundleIdTable { +impl Mappable for RecordedHeights { type Key = Self::OwnedKey; - type OwnedKey = BlockHeight; + type OwnedKey = (); type Value = Self::OwnedValue; - type OwnedValue = BundleId; + type OwnedValue = BlockHeight; } -impl TableWithBlueprint for BundleIdTable { - type Blueprint = Plain, Postcard>; +impl TableWithBlueprint for RecordedHeights { + type Blueprint = Plain>; type Column = GasPriceColumn; fn column() -> Self::Column { - GasPriceColumn::BundleId + GasPriceColumn::LatestRecordedHeight } } diff --git a/crates/services/gas_price_service/src/common/updater_metadata.rs b/crates/services/gas_price_service/src/common/updater_metadata.rs index 6ce274ea1a7..41093056c44 100644 --- a/crates/services/gas_price_service/src/common/updater_metadata.rs +++ b/crates/services/gas_price_service/src/common/updater_metadata.rs @@ -22,6 +22,13 @@ impl UpdaterMetadata { UpdaterMetadata::V1(v1) => v1.l2_block_height.into(), } } + + pub fn v1(&self) -> Option<&V1Metadata> { + match self { + UpdaterMetadata::V1(v1) => Some(v1), + _ => None, + } + } } impl From for UpdaterMetadata { diff --git a/crates/services/gas_price_service/src/common/utils.rs b/crates/services/gas_price_service/src/common/utils.rs index 5c944bd9fb1..e74c7b354eb 100644 --- a/crates/services/gas_price_service/src/common/utils.rs +++ b/crates/services/gas_price_service/src/common/utils.rs @@ -4,8 +4,10 @@ use fuel_core_types::fuel_types::BlockHeight; pub enum Error { #[error("Failed to find L2 block: {source_error:?}")] CouldNotFetchL2Block { source_error: anyhow::Error }, - #[error("Failed to find DA records: {0:?}")] - CouldNotFetchDARecord(anyhow::Error), + #[error("Failed to get the recorded height: {0:?}")] + CouldNotFetchRecordedHeight(anyhow::Error), + #[error("Failed to set the recorded height: {0:?}")] + CouldNotSetRecordedHeight(anyhow::Error), #[error("Failed to retrieve updater metadata: {source_error:?}")] CouldNotFetchMetadata { source_error: anyhow::Error }, #[error( @@ -26,7 +28,7 @@ pub enum Error { pub type Result = core::result::Result; // Info required about the l2 block for the gas price algorithm -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum BlockInfo { // The genesis block of the L2 chain GenesisBlock, diff --git a/crates/services/gas_price_service/src/ports.rs b/crates/services/gas_price_service/src/ports.rs index 0ccd77cb01a..22dba0e1344 100644 --- a/crates/services/gas_price_service/src/ports.rs +++ b/crates/services/gas_price_service/src/ports.rs @@ -10,11 +10,7 @@ use crate::{ updater_metadata::UpdaterMetadata, utils::Result, }, - v0::metadata::V0AlgorithmConfig, - v1::{ - metadata::V1AlgorithmConfig, - uninitialized_task::fuel_storage_unrecorded_blocks::AsUnrecordedBlocks, - }, + v1::uninitialized_task::fuel_storage_unrecorded_blocks::AsUnrecordedBlocks, }; pub trait L2Data: Send + Sync { @@ -34,26 +30,27 @@ pub trait GetMetadataStorage: Send + Sync { -> Result>; } -pub trait SetDaBundleId: Send + Sync { - fn set_bundle_id(&mut self, block_height: &BlockHeight, bundle_id: u32) - -> Result<()>; +pub trait SetLatestRecordedHeight: Send + Sync { + /// Set the latest L2 block height which has been committed to the DA layer + fn set_recorded_height(&mut self, recorded_height: BlockHeight) -> Result<()>; } -pub trait GetDaBundleId: Send + Sync { - fn get_bundle_id(&self, block_height: &BlockHeight) -> Result>; +pub trait GetLatestRecordedHeight: Send + Sync { + /// Get the most recent L2 block that has been committed to DA + fn get_recorded_height(&self) -> Result>; } pub trait GasPriceServiceAtomicStorage where Self: 'static, Self: Send + Sync, - Self: GetMetadataStorage + GetDaBundleId, + Self: GetMetadataStorage + GetLatestRecordedHeight, { type Transaction<'a>: AsUnrecordedBlocks + SetMetadataStorage + GetMetadataStorage - + SetDaBundleId - + GetDaBundleId + + SetLatestRecordedHeight + + GetLatestRecordedHeight where Self: 'a; @@ -68,58 +65,3 @@ where pub trait GasPriceData: Send + Sync { fn latest_height(&self) -> Option; } - -pub enum GasPriceServiceConfig { - V0(V0AlgorithmConfig), - V1(V1AlgorithmConfig), -} - -impl GasPriceServiceConfig { - pub fn new_v0( - starting_gas_price: u64, - min_gas_price: u64, - gas_price_change_percent: u64, - gas_price_threshold_percent: u64, - ) -> Self { - Self::V0(V0AlgorithmConfig { - starting_gas_price, - min_gas_price, - gas_price_change_percent, - gas_price_threshold_percent, - }) - } - - pub fn new_v1(metadata: V1AlgorithmConfig) -> Self { - Self::V1(metadata) - } - - /// Extract V0AlgorithmConfig if it is of V0 version - pub fn v0(self) -> Option { - if let GasPriceServiceConfig::V0(v0) = self { - Some(v0) - } else { - None - } - } - - /// Extract V1AlgorithmConfig if it is of V1 version - pub fn v1(self) -> Option { - if let GasPriceServiceConfig::V1(v1) = self { - Some(v1) - } else { - None - } - } -} - -impl From for GasPriceServiceConfig { - fn from(value: V0AlgorithmConfig) -> Self { - GasPriceServiceConfig::V0(value) - } -} - -impl From for GasPriceServiceConfig { - fn from(value: V1AlgorithmConfig) -> Self { - GasPriceServiceConfig::V1(value) - } -} diff --git a/crates/services/gas_price_service/src/v0/uninitialized_task.rs b/crates/services/gas_price_service/src/v0/uninitialized_task.rs index 0dd0e395d68..7c70fb17c63 100644 --- a/crates/services/gas_price_service/src/v0/uninitialized_task.rs +++ b/crates/services/gas_price_service/src/v0/uninitialized_task.rs @@ -16,7 +16,6 @@ use crate::{ }, ports::{ GasPriceData, - GasPriceServiceConfig, L2Data, SetMetadataStorage, }, @@ -328,7 +327,7 @@ pub fn new_gas_price_service_v0< Metadata, SettingsProvider, >( - config: GasPriceServiceConfig, + v0_config: V0AlgorithmConfig, genesis_block_height: BlockHeight, settings: SettingsProvider, block_stream: BoxStream, @@ -347,7 +346,6 @@ where SettingsProvider: GasPriceSettingsProvider, Metadata: GetMetadataStorage + SetMetadataStorage, { - let v0_config = config.v0().ok_or(anyhow::anyhow!("Expected V0 config"))?; let gas_price_init = UninitializedTask::new( v0_config, genesis_block_height, diff --git a/crates/services/gas_price_service/src/v1/algorithm.rs b/crates/services/gas_price_service/src/v1/algorithm.rs index c7f87aed2ad..86a2b4ba98d 100644 --- a/crates/services/gas_price_service/src/v1/algorithm.rs +++ b/crates/services/gas_price_service/src/v1/algorithm.rs @@ -3,7 +3,7 @@ use crate::common::gas_price_algorithm::{ SharedGasPriceAlgo, }; use fuel_core_types::fuel_types::BlockHeight; -use fuel_gas_price_algorithm::v1::AlgorithmV1; +pub use fuel_gas_price_algorithm::v1::AlgorithmV1; impl GasPriceAlgorithm for AlgorithmV1 { fn next_gas_price(&self) -> u64 { diff --git a/crates/services/gas_price_service/src/v1/da_source_service.rs b/crates/services/gas_price_service/src/v1/da_source_service.rs index bae8d3d46bc..2108f7e10ed 100644 --- a/crates/services/gas_price_service/src/v1/da_source_service.rs +++ b/crates/services/gas_price_service/src/v1/da_source_service.rs @@ -1,15 +1,18 @@ use crate::v1::da_source_service::service::DaBlockCostsSource; -use std::time::Duration; +use std::{ + ops::RangeInclusive, + time::Duration, +}; pub mod block_committer_costs; #[cfg(test)] pub mod dummy_costs; pub mod service; -#[derive(Debug, Default, Clone, Eq, Hash, PartialEq)] +#[derive(Debug, Clone, Eq, Hash, PartialEq)] pub struct DaBlockCosts { pub bundle_id: u32, - pub l2_blocks: Vec, + pub l2_blocks: RangeInclusive, pub bundle_size_bytes: u32, pub blob_cost_wei: u128, } @@ -20,27 +23,48 @@ mod tests { use super::*; use crate::v1::da_source_service::{ dummy_costs::DummyDaBlockCosts, - service::new_service, + service::{ + new_da_service, + DaSourceService, + DA_BLOCK_COSTS_CHANNEL_SIZE, + }, }; - use fuel_core_services::Service; + use fuel_core_services::{ + RunnableTask, + Service, + StateWatcher, + }; + use fuel_core_types::fuel_types::BlockHeight; use std::{ - sync::Arc, + sync::{ + Arc, + Mutex, + }, time::Duration, }; + fn latest_l2_height(height: u32) -> Arc> { + Arc::new(Mutex::new(BlockHeight::new(height))) + } + #[tokio::test] async fn run__when_da_block_cost_source_gives_value_shared_state_is_updated() { // given let expected_da_cost = DaBlockCosts { bundle_id: 1, - l2_blocks: (0..10).collect(), + l2_blocks: 0..=9, bundle_size_bytes: 1024 * 128, blob_cost_wei: 2, }; let notifier = Arc::new(tokio::sync::Notify::new()); let da_block_costs_source = DummyDaBlockCosts::new(Ok(expected_da_cost.clone()), notifier.clone()); - let service = new_service(da_block_costs_source, Some(Duration::from_millis(1))); + let latest_l2_height = Arc::new(Mutex::new(BlockHeight::new(10u32))); + let service = new_da_service( + da_block_costs_source, + Some(Duration::from_millis(1)), + latest_l2_height, + ); let mut shared_state = &mut service.shared.subscribe(); // when @@ -59,7 +83,12 @@ mod tests { let notifier = Arc::new(tokio::sync::Notify::new()); let da_block_costs_source = DummyDaBlockCosts::new(Err(anyhow::anyhow!("boo!")), notifier.clone()); - let service = new_service(da_block_costs_source, Some(Duration::from_millis(1))); + let latest_l2_height = latest_l2_height(0); + let service = new_da_service( + da_block_costs_source, + Some(Duration::from_millis(1)), + latest_l2_height, + ); let mut shared_state = &mut service.shared.subscribe(); // when @@ -71,4 +100,102 @@ mod tests { assert!(da_block_costs_res.is_err()); service.stop_and_await().await.unwrap(); } + + #[tokio::test] + async fn run__will_not_return_cost_bundles_for_bundles_that_are_greater_than_l2_height( + ) { + // given + let l2_height = 4; + let unexpected_costs = DaBlockCosts { + bundle_id: 1, + l2_blocks: 0..=9, + bundle_size_bytes: 1024 * 128, + blob_cost_wei: 2, + }; + assert!(unexpected_costs.l2_blocks.end() > &l2_height); + let notifier = Arc::new(tokio::sync::Notify::new()); + let da_block_costs_source = + DummyDaBlockCosts::new(Ok(unexpected_costs.clone()), notifier.clone()); + let latest_l2_height = latest_l2_height(l2_height); + let service = new_da_service( + da_block_costs_source, + Some(Duration::from_millis(1)), + latest_l2_height, + ); + let mut shared_state = &mut service.shared.subscribe(); + + // when + service.start_and_await().await.unwrap(); + notifier.notified().await; + + // then + let err = shared_state.try_recv(); + assert!(err.is_err()); + } + + #[tokio::test] + async fn run__filtered_da_block_costs_do_not_update_latest_recorded_block() { + // given + let l2_height = 4; + let unexpected_costs = DaBlockCosts { + bundle_id: 1, + l2_blocks: 2..=9, + bundle_size_bytes: 1024 * 128, + blob_cost_wei: 2, + }; + assert!(unexpected_costs.l2_blocks.end() > &l2_height); + let notifier = Arc::new(tokio::sync::Notify::new()); + let da_block_costs_source = + DummyDaBlockCosts::new(Ok(unexpected_costs.clone()), notifier.clone()); + let latest_l2_height = latest_l2_height(l2_height); + let mut service = DaSourceService::new( + da_block_costs_source, + Some(Duration::from_millis(1)), + latest_l2_height, + None, + ); + let mut watcher = StateWatcher::started(); + + // when + let _ = service.run(&mut watcher).await; + + // then + let recorded_height = service.recorded_height(); + assert!(recorded_height.is_none()) + } + + #[tokio::test] + async fn run__recorded_height_updated_by_da_costs() { + // given + let l2_height = 10; + let recorded_height = 9; + let unexpected_costs = DaBlockCosts { + bundle_id: 1, + l2_blocks: 2..=recorded_height, + bundle_size_bytes: 1024 * 128, + blob_cost_wei: 2, + }; + let notifier = Arc::new(tokio::sync::Notify::new()); + let da_block_costs_source = + DummyDaBlockCosts::new(Ok(unexpected_costs.clone()), notifier.clone()); + let latest_l2_height = latest_l2_height(l2_height); + let (sender, mut receiver) = + tokio::sync::broadcast::channel(DA_BLOCK_COSTS_CHANNEL_SIZE); + let mut service = DaSourceService::new_with_sender( + da_block_costs_source, + Some(Duration::from_millis(1)), + latest_l2_height, + None, + sender, + ); + let mut watcher = StateWatcher::started(); + + // when + let next = service.run(&mut watcher).await; + + // then + let actual = service.recorded_height().unwrap(); + let expected = BlockHeight::from(recorded_height); + assert_eq!(expected, actual); + } } diff --git a/crates/services/gas_price_service/src/v1/da_source_service/block_committer_costs.rs b/crates/services/gas_price_service/src/v1/da_source_service/block_committer_costs.rs index 98c024933c8..678a22ead3b 100644 --- a/crates/services/gas_price_service/src/v1/da_source_service/block_committer_costs.rs +++ b/crates/services/gas_price_service/src/v1/da_source_service/block_committer_costs.rs @@ -8,71 +8,78 @@ use crate::v1::da_source_service::{ DaBlockCosts, }; use anyhow::anyhow; -use fuel_core_types::blockchain::primitives::DaBlockHeight; +use fuel_core_types::{ + blockchain::primitives::DaBlockHeight, + fuel_types::BlockHeight, +}; use serde::{ Deserialize, Serialize, }; +use std::ops::Deref; #[async_trait::async_trait] -trait BlockCommitterApi: Send + Sync { +pub trait BlockCommitterApi: Send + Sync { /// Used on first run to get the latest costs and seqno async fn get_latest_costs(&self) -> DaBlockCostsResult>; /// Used to get the costs for a specific seqno - async fn get_costs_by_seqno( - &self, - number: u32, - ) -> DaBlockCostsResult>; - /// Used to get the costs for a range of blocks (inclusive) - async fn get_cost_bundles_by_range( + async fn get_costs_by_l2_block_number( &self, - range: core::ops::Range, - ) -> DaBlockCostsResult>>; + l2_block_number: u32, + ) -> DaBlockCostsResult>; } /// This struct is used to denote the block committer da block costs source /// which receives data from the block committer (only http api for now) pub struct BlockCommitterDaBlockCosts { client: BlockCommitter, - last_raw_da_block_costs: Option, } #[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq)] pub struct RawDaBlockCosts { - /// Sequence number (Monotonically increasing nonce) - pub bundle_id: u32, - /// The range of blocks that the costs apply to - pub blocks_heights: Vec, + pub id: u32, + /// The beginning of the range of blocks that the costs apply to + pub start_height: u32, + /// The end of the range of blocks that the costs apply to + pub end_height: u32, /// The DA block height of the last transaction for the range of blocks pub da_block_height: DaBlockHeight, - /// Rolling sum cost of posting blobs (wei) - pub total_cost: u128, - /// Rolling sum size of blobs (bytes) - pub total_size_bytes: u32, + /// cost of posting this bundle (wei) + pub cost: u128, + /// size of this bundle (bytes) + pub size: u32, } impl From<&RawDaBlockCosts> for DaBlockCosts { fn from(raw_da_block_costs: &RawDaBlockCosts) -> Self { + let RawDaBlockCosts { + start_height, + end_height, + cost: cost_wei, + size: size_bytes, + id: bundle_id, + .. + } = *raw_da_block_costs; DaBlockCosts { - bundle_id: raw_da_block_costs.bundle_id, - l2_blocks: raw_da_block_costs - .blocks_heights - .clone() - .into_iter() - .collect(), - bundle_size_bytes: raw_da_block_costs.total_size_bytes, - blob_cost_wei: raw_da_block_costs.total_cost, + bundle_id, + // construct a vec of l2 blocks from the start_height to the end_height + l2_blocks: start_height..=end_height, + bundle_size_bytes: size_bytes, + blob_cost_wei: cost_wei, } } } +impl From for DaBlockCosts { + fn from(value: RawDaBlockCosts) -> Self { + Self::from(&value) + } +} + impl BlockCommitterDaBlockCosts { /// Create a new instance of the block committer da block costs source - pub fn new(client: BlockCommitter, last_value: Option) -> Self { - Self { - client, - last_raw_da_block_costs: last_value, - } + pub fn new(client: BlockCommitter) -> Self { + Self { client } } } @@ -81,55 +88,34 @@ impl DaBlockCostsSource for BlockCommitterDaBlockCosts DaBlockCostsResult { - let raw_da_block_costs = match self.last_raw_da_block_costs { - Some(ref last_value) => { - self.client.get_costs_by_seqno(last_value.bundle_id + 1) + async fn request_da_block_costs( + &mut self, + last_recorded_height: &Option, + ) -> DaBlockCostsResult> { + let raw_da_block_costs: Vec<_> = match last_recorded_height.and_then(|x| x.succ()) + { + Some(ref next_height) => { + self.client + .get_costs_by_l2_block_number(*next_height.deref()) + .await? } - _ => self.client.get_latest_costs(), - } - .await?; - - let Some(ref raw_da_block_costs) = raw_da_block_costs else { - return Err(anyhow!("No response from block committer")) + None => self.client.get_latest_costs().await?.into_iter().collect(), }; - let da_block_costs = self.last_raw_da_block_costs.iter().fold( - Ok(raw_da_block_costs.into()), - |costs: DaBlockCostsResult, last_value| { - let costs = costs.expect("Defined to be OK"); - let blob_size_bytes = costs - .bundle_size_bytes - .checked_sub(last_value.total_size_bytes) - .ok_or(anyhow!("Blob size bytes underflow"))?; - let blob_cost_wei = raw_da_block_costs - .total_cost - .checked_sub(last_value.total_cost) - .ok_or(anyhow!("Blob cost wei underflow"))?; - Ok(DaBlockCosts { - bundle_size_bytes: blob_size_bytes, - blob_cost_wei, - ..costs - }) - }, - )?; + let da_block_costs: Vec<_> = + raw_da_block_costs.iter().map(DaBlockCosts::from).collect(); - self.last_raw_da_block_costs = Some(raw_da_block_costs.clone()); Ok(da_block_costs) } - async fn set_last_value(&mut self, bundle_id: u32) -> DaBlockCostsResult<()> { - self.last_raw_da_block_costs = self.client.get_costs_by_seqno(bundle_id).await?; - Ok(()) - } } pub struct BlockCommitterHttpApi { client: reqwest::Client, - url: String, + url: Option, } impl BlockCommitterHttpApi { - pub fn new(url: String) -> Self { + pub fn new(url: Option) -> Self { Self { client: reqwest::Client::new(), url, @@ -137,45 +123,313 @@ impl BlockCommitterHttpApi { } } +const NUMBER_OF_BUNDLES: u32 = 10; #[async_trait::async_trait] impl BlockCommitterApi for BlockCommitterHttpApi { + async fn get_costs_by_l2_block_number( + &self, + l2_block_number: u32, + ) -> DaBlockCostsResult> { + // Specific: http://localhost:8080/v1/costs?variant=specific&value=19098935&limit=5 + if let Some(url) = &self.url { + tracing::debug!("getting da costs by l2 block number: {l2_block_number}"); + let formatted_url = format!("{url}/v1/costs?variant=specific&value={l2_block_number}&limit={NUMBER_OF_BUNDLES}"); + let response = self.client.get(formatted_url).send().await?; + let parsed = response.json::>().await?; + Ok(parsed) + } else { + Ok(vec![]) + } + } + async fn get_latest_costs(&self) -> DaBlockCostsResult> { - let response = self - .client - .get(&self.url) - .send() - .await? - .json::() - .await?; - Ok(Some(response)) + // Latest: http://localhost:8080/v1/costs?variant=latest&limit=5 + if let Some(url) = &self.url { + let formatted_url = format!("{url}/v1/costs?variant=latest&limit=1"); + let response = self.client.get(formatted_url).send().await?; + let raw_da_block_costs = response.json::>().await?; + // only take the first element, since we are only looking for the most recent + Ok(raw_da_block_costs.first().cloned()) + } else { + Ok(None) + } } +} - async fn get_costs_by_seqno( - &self, - number: u32, - ) -> DaBlockCostsResult> { - let response = self - .client - .get(format!("{}/{}", self.url, number)) - .send() - .await? - .json::() - .await?; - Ok(Some(response)) +#[cfg(test)] +mod test_block_committer_http_api { + #![allow(non_snake_case)] + + use super::*; + use fake_server::FakeServer; + + #[test] + fn get_costs_by_l2_block_number__when_url_is_none__then_returns_empty_vec() { + let rt = tokio::runtime::Runtime::new().unwrap(); + + // given + let block_committer = BlockCommitterHttpApi::new(None); + let l2_block_number = 1; + + // when + let actual = rt.block_on(async { + block_committer + .get_costs_by_l2_block_number(l2_block_number) + .await + .unwrap() + }); + + // then + assert_eq!(actual.len(), 0); } + #[test] + fn get_costs_by_l2_block_number__when_url_is_some__then_returns_expected_costs() { + let rt = tokio::runtime::Runtime::new().unwrap(); + let mut mock = FakeServer::new(); + let url = mock.url(); - async fn get_cost_bundles_by_range( - &self, - range: core::ops::Range, - ) -> DaBlockCostsResult>> { - let response = self - .client - .get(format!("{}/{}-{}", self.url, range.start, range.end)) - .send() - .await? - .json::>() - .await?; - Ok(response.into_iter().map(Some).collect()) + // given + let l2_block_number = 51; + let block_committer = BlockCommitterHttpApi::new(Some(url)); + + let too_early_count = 5; + let do_not_fit_count = 5; + + let mut current_height = 0; + let mut bundle_id = 0; + let mut da_block_height: u64 = 0; + + // shouldn't return + for _ in 0..too_early_count { + bundle_id += 1; + da_block_height += 1; + current_height += 1; + let start_height = current_height; + current_height += 9; + let end_height = current_height; + let costs = RawDaBlockCosts { + id: bundle_id, + start_height, + end_height, + da_block_height: DaBlockHeight::from(da_block_height), + cost: 1, + size: 1, + }; + mock.add_response(costs); + } + let mut expected = Vec::new(); + + // should return + for _ in 0..NUMBER_OF_BUNDLES { + bundle_id += 1; + da_block_height += 1; + current_height += 1; + let start_height = current_height; + current_height += 9; + let end_height = current_height; + let costs = RawDaBlockCosts { + id: bundle_id, + start_height, + end_height, + da_block_height: DaBlockHeight::from(da_block_height), + cost: 1, + size: 1, + }; + mock.add_response(costs.clone()); + expected.push(costs); + } + // don't fit + for _ in 0..do_not_fit_count { + bundle_id += 1; + da_block_height += 1; + current_height += 1; + let start_height = current_height; + current_height += 9; + let end_height = current_height; + let costs = RawDaBlockCosts { + id: bundle_id, + start_height, + end_height, + da_block_height: DaBlockHeight::from(da_block_height), + cost: 1, + size: 1, + }; + mock.add_response(costs); + } + + // when + mock.init(); + let actual = rt.block_on(async { + block_committer + .get_costs_by_l2_block_number(l2_block_number) + .await + .unwrap() + }); + + // then + assert_eq!(actual, expected); + } + + #[test] + fn get_latest_costs__when_url_is_none__then_returns_none() { + let rt = tokio::runtime::Runtime::new().unwrap(); + + // given + let block_committer = BlockCommitterHttpApi::new(None); + + // when + let actual = + rt.block_on(async { block_committer.get_latest_costs().await.unwrap() }); + + // then + assert_eq!(actual, None); + } + + #[test] + fn get_latest_costs__when_url_is_some__then_returns_expected_costs() { + let rt = tokio::runtime::Runtime::new().unwrap(); + let mut mock = FakeServer::new(); + let url = mock.url(); + + // given + let block_committer = BlockCommitterHttpApi::new(Some(url)); + let not_expected = RawDaBlockCosts { + id: 1, + start_height: 1, + end_height: 10, + da_block_height: 1u64.into(), + cost: 1, + size: 1, + }; + mock.add_response(not_expected); + let expected = RawDaBlockCosts { + id: 2, + start_height: 11, + end_height: 20, + da_block_height: 2u64.into(), + cost: 2, + size: 2, + }; + mock.add_response(expected.clone()); + + // when + let actual = + rt.block_on(async { block_committer.get_latest_costs().await.unwrap() }); + + // then + assert_eq!(actual, Some(expected)); + } +} +#[cfg(any(test, feature = "test-helpers"))] +pub mod fake_server { + use crate::v1::da_source_service::block_committer_costs::RawDaBlockCosts; + use mockito::Matcher::Any; + use std::{ + collections::HashMap, + sync::{ + Arc, + Mutex, + }, + }; + + pub struct FakeServer { + server: mockito::ServerGuard, + responses: Arc>>, + } + + impl FakeServer { + pub fn new() -> Self { + let server = mockito::Server::new(); + let responses = Arc::new(Mutex::new(Vec::new())); + let mut fake = Self { server, responses }; + fake.init(); + fake + } + + #[allow(unused_variables)] + pub fn init(&mut self) { + let shared_responses = self.responses.clone(); + self.server + .mock("GET", Any) + .with_status(201) + .with_body_from_request(move |request| { + // take the requested number and return the corresponding response from the `responses` hashmap + let path = request.path_and_query(); + tracing::info!("Path: {:?}", path); + let query = path.split("variant=").last().unwrap(); + tracing::info!("Query: {:?}", query); + let mut values = query.split('&'); + let variant = values.next().unwrap(); + tracing::info!("Variant: {:?}", variant); + match variant { + // Latest: http://localhost:8080/v1/costs?variant=latest&limit=5 + // We don't support `limit` yet!!!! + "latest" => { + let args = values.next().unwrap(); + let limit = + args.split('=').last().unwrap().parse::().unwrap(); + assert!(limit == 1); + let guard = shared_responses.lock().unwrap(); + let most_recent = guard + .iter() + .fold(None, |acc, x| match acc { + None => Some(x), + Some(acc) => { + if x.end_height > acc.end_height { + Some(x) + } else { + Some(acc) + } + } + }) + .cloned(); + let response: Vec = + most_recent.into_iter().collect(); + serde_json::to_string(&response).unwrap().into() + } + // Specific: http://localhost:8080/v1/costs?variant=specific&value=19098935&limit=5 + "specific" => { + let args = values.next().unwrap(); + let mut specific_values = args.split('='); + let height = + specific_values.last().unwrap().parse::().unwrap(); + tracing::info!("Height: {:?}", height); + let maybe_limit = values + .next() + .and_then(|x| x.split('=').last()) + .and_then(|x| x.parse::().ok()); + tracing::info!("Limit: {:?}", maybe_limit); + let guard = shared_responses.lock().unwrap(); + let response = guard + .iter() + .filter(|costs| costs.end_height >= height) + .take(maybe_limit.unwrap_or(usize::MAX)) + .cloned() + .collect::>(); + serde_json::to_string(&response).unwrap().into() + } + _ => { + panic!("Invalid variant: {}", variant); + } + } + }) + .expect_at_least(1) + .create(); + } + + pub fn add_response(&mut self, costs: RawDaBlockCosts) { + let mut guard = self.responses.lock().unwrap(); + guard.push(costs); + } + + pub fn url(&self) -> url::Url { + url::Url::parse(self.server.url().as_str()).unwrap() + } + } + impl Default for FakeServer { + fn default() -> Self { + Self::new() + } } } @@ -199,41 +453,32 @@ mod tests { async fn get_latest_costs(&self) -> DaBlockCostsResult> { Ok(self.value.clone()) } - async fn get_costs_by_seqno( + async fn get_costs_by_l2_block_number( &self, - seq_no: u32, - ) -> DaBlockCostsResult> { + l2_block_number: u32, + ) -> DaBlockCostsResult> { // arbitrary logic to generate a new value let mut value = self.value.clone(); if let Some(value) = &mut value { - value.bundle_id = seq_no; - value.blocks_heights = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - .to_vec() - .iter() - .map(|x| x * seq_no) - .collect(); + value.start_height = l2_block_number; + value.end_height = value.end_height + l2_block_number + 10; value.da_block_height = - value.da_block_height + ((seq_no + 1) as u64).into(); - value.total_cost += 1; - value.total_size_bytes += 1; + value.da_block_height + ((l2_block_number + 1) as u64).into(); + value.cost += 1; + value.size += 1; } - Ok(value) - } - async fn get_cost_bundles_by_range( - &self, - _: core::ops::Range, - ) -> DaBlockCostsResult>> { - Ok(vec![self.value.clone()]) + Ok(value.into_iter().collect()) } } fn test_da_block_costs() -> RawDaBlockCosts { RawDaBlockCosts { - bundle_id: 1, - blocks_heights: (0..10).collect(), + id: 1, + start_height: 1, + end_height: 10, da_block_height: 1u64.into(), - total_cost: 1, - total_size_bytes: 1, + cost: 1, + size: 1, } } @@ -242,97 +487,36 @@ mod tests { ) { // given let da_block_costs = test_da_block_costs(); - let expected = (&da_block_costs).into(); + let expected = vec![(&da_block_costs).into()]; let mock_api = MockBlockCommitterApi::new(Some(da_block_costs)); - let mut block_committer = BlockCommitterDaBlockCosts::new(mock_api, None); + let mut block_committer = BlockCommitterDaBlockCosts::new(mock_api); // when - let actual = block_committer.request_da_block_cost().await.unwrap(); + let actual = block_committer.request_da_block_costs(&None).await.unwrap(); // then assert_eq!(actual, expected); } #[tokio::test] - async fn request_da_block_cost__when_last_value_is_some__then_get_costs_by_seqno_is_called( + async fn request_da_block_cost__when_last_value_is_some__then_get_costs_by_l2_block_number_is_called( ) { // given let mut da_block_costs = test_da_block_costs(); + let da_block_costs_len = da_block_costs.end_height - da_block_costs.start_height; let mock_api = MockBlockCommitterApi::new(Some(da_block_costs.clone())); - let mut block_committer = - BlockCommitterDaBlockCosts::new(mock_api, Some(da_block_costs.clone())); - - // when - let actual = block_committer.request_da_block_cost().await.unwrap(); - - // then - assert_ne!(da_block_costs.blocks_heights, actual.l2_blocks); - } - - #[tokio::test] - async fn request_da_block_cost__when_response_is_none__then_error() { - // given - let mock_api = MockBlockCommitterApi::new(None); - let mut block_committer = BlockCommitterDaBlockCosts::new(mock_api, None); - - // when - let result = block_committer.request_da_block_cost().await; - - // then - assert!(result.is_err()); - } - - struct UnderflowingMockBlockCommitterApi { - value: Option, - } - - impl UnderflowingMockBlockCommitterApi { - fn new(value: Option) -> Self { - Self { value } - } - } - - #[async_trait::async_trait] - impl BlockCommitterApi for UnderflowingMockBlockCommitterApi { - async fn get_latest_costs(&self) -> DaBlockCostsResult> { - Ok(self.value.clone()) - } - async fn get_costs_by_seqno( - &self, - seq_no: u32, - ) -> DaBlockCostsResult> { - // arbitrary logic to generate a new value - let mut value = self.value.clone(); - if let Some(value) = &mut value { - value.bundle_id = seq_no; - value.blocks_heights = - value.blocks_heights.iter().map(|x| x + seq_no).collect(); - value.da_block_height = value.da_block_height + 1u64.into(); - value.total_cost -= 1; - value.total_size_bytes -= 1; - } - Ok(value) - } - async fn get_cost_bundles_by_range( - &self, - _: core::ops::Range, - ) -> DaBlockCostsResult>> { - Ok(vec![self.value.clone()]) - } - } - - #[tokio::test] - async fn request_da_block_cost__when_underflow__then_error() { - // given - let da_block_costs = test_da_block_costs(); - let mock_api = UnderflowingMockBlockCommitterApi::new(Some(da_block_costs)); - let mut block_committer = BlockCommitterDaBlockCosts::new(mock_api, None); - let _ = block_committer.request_da_block_cost().await.unwrap(); + let latest_height = BlockHeight::new(da_block_costs.end_height); + let mut block_committer = BlockCommitterDaBlockCosts::new(mock_api); // when - let result = block_committer.request_da_block_cost().await; + let actual = block_committer + .request_da_block_costs(&Some(latest_height)) + .await + .unwrap(); // then - assert!(result.is_err()); + let l2_blocks = actual.first().unwrap().l2_blocks.clone(); + let range_len = l2_blocks.end() - l2_blocks.start(); + assert_ne!(da_block_costs_len, range_len); } } diff --git a/crates/services/gas_price_service/src/v1/da_source_service/dummy_costs.rs b/crates/services/gas_price_service/src/v1/da_source_service/dummy_costs.rs index 5204ea5fba0..82c352d975e 100644 --- a/crates/services/gas_price_service/src/v1/da_source_service/dummy_costs.rs +++ b/crates/services/gas_price_service/src/v1/da_source_service/dummy_costs.rs @@ -5,6 +5,7 @@ use crate::v1::da_source_service::{ }, DaBlockCosts, }; +use fuel_core_types::fuel_types::BlockHeight; use std::sync::Arc; use tokio::sync::Notify; @@ -22,11 +23,14 @@ impl DummyDaBlockCosts { #[async_trait::async_trait] impl DaBlockCostsSource for DummyDaBlockCosts { - async fn request_da_block_cost(&mut self) -> DaBlockCostsResult { + async fn request_da_block_costs( + &mut self, + _latest_recorded_height: &Option, + ) -> DaBlockCostsResult> { match &self.value { Ok(da_block_costs) => { self.notifier.notify_waiters(); - Ok(da_block_costs.clone()) + Ok(vec![da_block_costs.clone()]) } Err(err) => { self.notifier.notify_waiters(); @@ -34,8 +38,4 @@ impl DaBlockCostsSource for DummyDaBlockCosts { } } } - - async fn set_last_value(&mut self, _bundle_id: u32) -> DaBlockCostsResult<()> { - unimplemented!("This is a dummy implementation"); - } } diff --git a/crates/services/gas_price_service/src/v1/da_source_service/service.rs b/crates/services/gas_price_service/src/v1/da_source_service/service.rs index d7dfca30a20..6a65ec82529 100644 --- a/crates/services/gas_price_service/src/v1/da_source_service/service.rs +++ b/crates/services/gas_price_service/src/v1/da_source_service/service.rs @@ -5,7 +5,13 @@ use fuel_core_services::{ StateWatcher, TaskNextAction, }; -use std::time::Duration; +use std::{ + sync::{ + Arc, + Mutex, + }, + time::Duration, +}; use tokio::{ sync::broadcast::Sender, time::{ @@ -16,6 +22,7 @@ use tokio::{ use crate::v1::da_source_service::DaBlockCosts; pub use anyhow::Result; +use fuel_core_types::fuel_types::BlockHeight; #[derive(Clone)] pub struct SharedState(Sender); @@ -36,16 +43,23 @@ pub struct DaSourceService { poll_interval: Interval, source: Source, shared_state: SharedState, + latest_l2_height: Arc>, + recorded_height: Option, } -const DA_BLOCK_COSTS_CHANNEL_SIZE: usize = 16 * 1024; +pub(crate) const DA_BLOCK_COSTS_CHANNEL_SIZE: usize = 16 * 1024; const POLLING_INTERVAL_MS: u64 = 10_000; impl DaSourceService where Source: DaBlockCostsSource, { - pub fn new(source: Source, poll_interval: Option) -> Self { + pub fn new( + source: Source, + poll_interval: Option, + latest_l2_height: Arc>, + recorded_height: Option, + ) -> Self { let (sender, _) = tokio::sync::broadcast::channel(DA_BLOCK_COSTS_CHANNEL_SIZE); #[allow(clippy::arithmetic_side_effects)] Self { @@ -54,22 +68,87 @@ where poll_interval.unwrap_or(Duration::from_millis(POLLING_INTERVAL_MS)), ), source, + latest_l2_height, + recorded_height, + } + } + + #[cfg(test)] + pub fn new_with_sender( + source: Source, + poll_interval: Option, + latest_l2_height: Arc>, + recorded_height: Option, + sender: Sender, + ) -> Self { + Self { + shared_state: SharedState::new(sender), + poll_interval: interval( + poll_interval.unwrap_or(Duration::from_millis(POLLING_INTERVAL_MS)), + ), + source, + latest_l2_height, + recorded_height, } } async fn process_block_costs(&mut self) -> Result<()> { - let da_block_costs = self.source.request_da_block_cost().await?; - self.shared_state.0.send(da_block_costs)?; + let da_block_costs_res = self + .source + .request_da_block_costs(&self.recorded_height) + .await; + tracing::debug!("Received block costs: {:?}", da_block_costs_res); + let da_block_costs = da_block_costs_res?; + let filtered_block_costs = self + .filter_costs_that_have_values_greater_than_l2_block_height(da_block_costs)?; + tracing::debug!( + "the latest l2 height is: {:?}", + *self.latest_l2_height.lock().unwrap() + ); + for da_block_costs in filtered_block_costs { + tracing::debug!("Sending block costs: {:?}", da_block_costs); + let end = BlockHeight::from(*da_block_costs.l2_blocks.end()); + self.shared_state.0.send(da_block_costs)?; + if let Some(recorded_height) = self.recorded_height { + if end > recorded_height { + self.recorded_height = Some(end) + } + } else { + self.recorded_height = Some(end) + } + } Ok(()) } + + fn filter_costs_that_have_values_greater_than_l2_block_height( + &self, + da_block_costs: Vec, + ) -> Result> { + let latest_l2_height = *self + .latest_l2_height + .lock() + .map_err(|err| anyhow::anyhow!("lock error: {:?}", err))?; + let iter = da_block_costs.into_iter().filter(move |da_block_costs| { + let end = BlockHeight::from(*da_block_costs.l2_blocks.end()); + end < latest_l2_height + }); + Ok(iter) + } + + #[cfg(test)] + pub fn recorded_height(&self) -> Option { + self.recorded_height + } } /// This trait is implemented by the sources to obtain the /// da block costs in a way they see fit #[async_trait::async_trait] pub trait DaBlockCostsSource: Send + Sync { - async fn request_da_block_cost(&mut self) -> Result; - async fn set_last_value(&mut self, bundle_id: u32) -> Result<()>; + async fn request_da_block_costs( + &mut self, + recorded_height: &Option, + ) -> Result>; } #[async_trait::async_trait] @@ -113,6 +192,7 @@ where TaskNextAction::Stop } _ = self.poll_interval.tick() => { + tracing::debug!("Polling DaSourceService for block costs"); let da_block_costs_res = self.process_block_costs().await; TaskNextAction::always_continue(da_block_costs_res) } @@ -126,9 +206,16 @@ where } } -pub fn new_service( +#[cfg(feature = "test-helpers")] +pub fn new_da_service( da_source: S, poll_interval: Option, + latest_l2_height: Arc>, ) -> ServiceRunner> { - ServiceRunner::new(DaSourceService::new(da_source, poll_interval)) + ServiceRunner::new(DaSourceService::new( + da_source, + poll_interval, + latest_l2_height, + None, + )) } diff --git a/crates/services/gas_price_service/src/v1/metadata.rs b/crates/services/gas_price_service/src/v1/metadata.rs index ef2e88466f5..f52a2bff2b0 100644 --- a/crates/services/gas_price_service/src/v1/metadata.rs +++ b/crates/services/gas_price_service/src/v1/metadata.rs @@ -3,7 +3,10 @@ use fuel_gas_price_algorithm::v1::{ AlgorithmUpdaterV1, L2ActivityTracker, }; -use std::num::NonZeroU64; +use std::{ + num::NonZeroU64, + time::Duration, +}; #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)] pub struct V1Metadata { @@ -67,6 +70,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, @@ -74,9 +78,14 @@ pub struct V1AlgorithmConfig { pub capped_range_size: u16, pub decrease_range_size: u16, pub block_activity_threshold: u8, + /// The interval at which the `DaSourceService` polls for new data + pub da_poll_interval: Option, } -pub fn updater_from_config(value: &V1AlgorithmConfig) -> AlgorithmUpdaterV1 { +pub fn updater_from_config( + value: &V1AlgorithmConfig, + l2_block_height: u32, +) -> AlgorithmUpdaterV1 { let l2_activity = L2ActivityTracker::new_full( value.normal_range_size, value.capped_range_size, @@ -88,8 +97,10 @@ pub fn updater_from_config(value: &V1AlgorithmConfig) -> AlgorithmUpdaterV1 { new_scaled_exec_price: value .new_exec_gas_price .saturating_mul(value.gas_price_factor.get()), - l2_block_height: 0, - new_scaled_da_gas_price: value.min_da_gas_price, + l2_block_height, + new_scaled_da_gas_price: value + .min_da_gas_price + .saturating_mul(value.gas_price_factor.get()), gas_price_factor: value.gas_price_factor, total_da_rewards_excess: 0, latest_known_total_da_cost_excess: 0, @@ -104,6 +115,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, @@ -162,6 +174,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 0e9b4c2f741..82a109227b9 100644 --- a/crates/services/gas_price_service/src/v1/service.rs +++ b/crates/services/gas_price_service/src/v1/service.rs @@ -10,9 +10,9 @@ use crate::{ }, ports::{ GasPriceServiceAtomicStorage, - GetDaBundleId, + GetLatestRecordedHeight, GetMetadataStorage, - SetDaBundleId, + SetLatestRecordedHeight, SetMetadataStorage, }, v0::metadata::V0Metadata, @@ -43,6 +43,8 @@ use async_trait::async_trait; use fuel_core_services::{ RunnableService, RunnableTask, + Service, + ServiceRunner, StateWatcher, TaskNextAction, }; @@ -58,7 +60,10 @@ use fuel_gas_price_algorithm::{ use futures::FutureExt; use std::{ num::NonZeroU64, - sync::Arc, + sync::{ + Arc, + Mutex, + }, }; use tokio::sync::broadcast::Receiver; @@ -86,7 +91,10 @@ impl LatestGasPrice { } /// The service that updates the gas price algorithm. -pub struct GasPriceServiceV1 { +pub struct GasPriceServiceV1 +where + DA: DaBlockCostsSource + 'static, +{ /// The algorithm that can be used in the next block shared_algo: SharedV1Algorithm, /// The latest gas price @@ -96,16 +104,21 @@ pub struct GasPriceServiceV1 { /// The algorithm updater algorithm_updater: AlgorithmUpdaterV1, /// the da source adapter handle - da_source_adapter_handle: DaSourceService, + da_source_adapter_handle: ServiceRunner>, /// The da source channel da_source_channel: Receiver, /// Buffer of block costs from the DA chain da_block_costs_buffer: Vec, /// Storage transaction provider for metadata and unrecorded blocks - storage_tx_provider: StorageTxProvider, + storage_tx_provider: AtomicStorage, + /// communicates to the Da source service what the latest L2 block was + latest_l2_block: Arc>, } -impl GasPriceServiceV1 { +impl GasPriceServiceV1 +where + DA: DaBlockCostsSource + 'static, +{ pub(crate) fn update_latest_gas_price(&mut self, block_info: &BlockInfo) { match block_info { BlockInfo::GenesisBlock => { @@ -136,6 +149,23 @@ where self.update_latest_gas_price(&block); tracing::debug!("Updating gas price algorithm"); self.apply_block_info_to_gas_algorithm(block).await?; + + self.notify_da_source_service_l2_block(block)?; + Ok(()) + } + + fn notify_da_source_service_l2_block(&self, block: BlockInfo) -> anyhow::Result<()> { + tracing::debug!("Notifying the Da source service of the latest L2 block"); + match block { + BlockInfo::GenesisBlock => {} + BlockInfo::Block { height, .. } => { + let mut latest_l2_block = self + .latest_l2_block + .lock() + .map_err(|err| anyhow!("Error locking latest L2 block: {:?}", err))?; + *latest_l2_block = BlockHeight::from(height); + } + } Ok(()) } } @@ -150,11 +180,11 @@ where shared_algo: SharedV1Algorithm, latest_gas_price: LatestGasPrice, algorithm_updater: AlgorithmUpdaterV1, - da_source_adapter_handle: DaSourceService, + da_source_adapter_handle: ServiceRunner>, storage_tx_provider: AtomicStorage, + latest_l2_block: Arc>, ) -> Self { - let da_source_channel = - da_source_adapter_handle.shared_data().clone().subscribe(); + let da_source_channel = da_source_adapter_handle.shared.clone().subscribe(); Self { shared_algo, latest_gas_price, @@ -164,6 +194,7 @@ where da_source_channel, da_block_costs_buffer: Vec::new(), storage_tx_provider, + latest_l2_block, } } @@ -201,43 +232,47 @@ where ) -> anyhow::Result<()> { let capacity = Self::validate_block_gas_capacity(block_gas_capacity)?; let mut storage_tx = self.storage_tx_provider.begin_transaction()?; - let prev_height = height.saturating_sub(1); - let mut bundle_id = storage_tx - .get_bundle_id(&BlockHeight::from(prev_height)) + let mut latest_recorded_height = storage_tx + .get_recorded_height() .map_err(|err| anyhow!(err))?; for da_block_costs in &self.da_block_costs_buffer { tracing::debug!("Updating DA block costs: {:?}", da_block_costs); + let l2_blocks = da_block_costs.l2_blocks.clone(); + let end = *l2_blocks.end(); self.algorithm_updater.update_da_record_data( - &da_block_costs.l2_blocks, + l2_blocks, da_block_costs.bundle_size_bytes, da_block_costs.blob_cost_wei, &mut storage_tx.as_unrecorded_blocks(), )?; - bundle_id = Some(da_block_costs.bundle_id); + latest_recorded_height = Some(BlockHeight::from(end)); } - if let Some(bundle_id) = bundle_id { + if let Some(recorded_height) = latest_recorded_height { storage_tx - .set_bundle_id(&BlockHeight::from(height), bundle_id) + .set_recorded_height(recorded_height) .map_err(|err| anyhow!(err))?; } + let fee_in_wei = u128::from(block_fees).saturating_mul(1_000_000_000); self.algorithm_updater.update_l2_block_data( height, gas_used, capacity, block_bytes, - block_fees as u128, + fee_in_wei, &mut storage_tx.as_unrecorded_blocks(), )?; let metadata = self.algorithm_updater.clone().into(); + tracing::debug!("Setting metadata: {:?}", metadata); storage_tx .set_metadata(&metadata) .map_err(|err| anyhow!(err))?; AtomicStorage::commit_transaction(storage_tx)?; let new_algo = self.algorithm_updater.algorithm(); + tracing::debug!("Updating gas price: {}", &new_algo.calculate()); self.shared_algo.update(new_algo).await; // Clear the buffer after committing changes self.da_block_costs_buffer.clear(); @@ -323,7 +358,7 @@ where } // run shutdown hooks for internal services - self.da_source_adapter_handle.shutdown().await?; + self.da_source_adapter_handle.stop_and_await().await?; Ok(()) } @@ -349,21 +384,22 @@ fn convert_to_v1_metadata( pub fn initialize_algorithm( config: &V1AlgorithmConfig, - latest_block_height: u32, + latest_metadata_block_height: u32, + latest_l2_block_height: u32, metadata_storage: &Metadata, ) -> crate::common::utils::Result<(AlgorithmUpdaterV1, SharedV1Algorithm)> where Metadata: GetMetadataStorage, { let algorithm_updater = if let Some(updater_metadata) = metadata_storage - .get_metadata(&latest_block_height.into()) + .get_metadata(&latest_metadata_block_height.into()) .map_err(|err| { crate::common::utils::Error::CouldNotInitUpdater(anyhow::anyhow!(err)) })? { let v1_metadata = convert_to_v1_metadata(updater_metadata, config)?; v1_algorithm_from_metadata(v1_metadata, config) } else { - updater_from_config(config) + updater_from_config(config, latest_l2_block_height) }; let shared_algo = @@ -378,14 +414,18 @@ where mod tests { use std::{ num::NonZeroU64, - sync::Arc, + sync::{ + Arc, + Mutex, + }, time::Duration, }; - use tokio::sync::mpsc; use fuel_core_services::{ RunnableTask, + Service, + ServiceRunner, StateWatcher, }; use fuel_core_storage::{ @@ -403,9 +443,10 @@ mod tests { use crate::{ common::{ fuel_core_storage_adapter::storage::{ - BundleIdTable, GasPriceColumn, GasPriceColumn::UnrecordedBlocks, + GasPriceMetadata, + RecordedHeights, UnrecordedBlocksTable, }, gas_price_algorithm::SharedGasPriceAlgo, @@ -417,6 +458,7 @@ mod tests { }, }, ports::{ + GetLatestRecordedHeight, GetMetadataStorage, SetMetadataStorage, }, @@ -429,6 +471,7 @@ mod tests { metadata::{ updater_from_config, V1AlgorithmConfig, + V1Metadata, }, service::{ initialize_algorithm, @@ -479,9 +522,13 @@ mod tests { Ok(metadata) } } + fn database() -> StorageTransaction> { InMemoryStorage::default().into_transaction() } + fn latest_l2_height(height: u32) -> Arc> { + Arc::new(Mutex::new(BlockHeight::new(height))) + } #[tokio::test] async fn run__updates_gas_price_with_l2_block_source() { @@ -510,6 +557,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, @@ -517,28 +565,41 @@ mod tests { capped_range_size: 100, decrease_range_size: 4, block_activity_threshold: 20, + da_poll_interval: None, }; let inner = database(); - let (algo_updater, shared_algo) = - initialize_algorithm(&config, l2_block_height, &metadata_storage).unwrap(); + let (algo_updater, shared_algo) = initialize_algorithm( + &config, + l2_block_height, + l2_block_height, + &metadata_storage, + ) + .unwrap(); let notifier = Arc::new(tokio::sync::Notify::new()); + let latest_l2_block = Arc::new(Mutex::new(BlockHeight::new(0))); let dummy_da_source = DaSourceService::new( DummyDaBlockCosts::new( Err(anyhow::anyhow!("unused at the moment")), notifier.clone(), ), None, + latest_l2_block, + None, ); + let da_service_runner = ServiceRunner::new(dummy_da_source); + da_service_runner.start_and_await().await.unwrap(); let latest_gas_price = LatestGasPrice::new(0, 0); + let latest_l2_height = latest_l2_height(0); let mut service = GasPriceServiceV1::new( l2_block_source, shared_algo, latest_gas_price, algo_updater, - dummy_da_source, + da_service_runner, inner, + latest_l2_height, ); let read_algo = service.next_block_algorithm(); let mut watcher = StateWatcher::default(); @@ -557,7 +618,7 @@ mod tests { #[tokio::test] async fn run__updates_gas_price_with_da_block_cost_source() { // given - let block_height = 2; + let block_height = 3; let l2_block_2 = BlockInfo::Block { height: block_height, gas_used: 60, @@ -581,6 +642,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, @@ -588,6 +650,7 @@ mod tests { capped_range_size: 100, decrease_range_size: 4, block_activity_threshold: 20, + da_poll_interval: None, }; let mut inner = database(); let mut tx = inner.write_transaction(); @@ -595,27 +658,32 @@ mod tests { .insert(&BlockHeight::from(1), &100) .unwrap(); tx.commit().unwrap(); - let mut algo_updater = updater_from_config(&config); + let mut algo_updater = updater_from_config(&config, 0); let shared_algo = SharedGasPriceAlgo::new_with_algorithm(algo_updater.algorithm()); algo_updater.l2_block_height = block_height - 1; algo_updater.last_profit = 10_000; algo_updater.new_scaled_da_gas_price = 10_000_000; + let latest_l2_block = latest_l2_height(block_height - 1); let notifier = Arc::new(tokio::sync::Notify::new()); let da_source = DaSourceService::new( DummyDaBlockCosts::new( Ok(DaBlockCosts { bundle_id: 1, - l2_blocks: (1..2).collect(), - blob_cost_wei: 9000, + l2_blocks: 1..=1, + blob_cost_wei: u128::MAX, // Very expensive to trigger a change bundle_size_bytes: 3000, }), notifier.clone(), ), Some(Duration::from_millis(1)), + latest_l2_block.clone(), + None, ); let mut watcher = StateWatcher::started(); + let da_service_runner = ServiceRunner::new(da_source); + da_service_runner.start_and_await().await.unwrap(); let latest_gas_price = LatestGasPrice::new(0, 0); let mut service = GasPriceServiceV1::new( @@ -623,28 +691,20 @@ mod tests { shared_algo, latest_gas_price, algo_updater, - da_source, + da_service_runner, inner, + latest_l2_block, ); let read_algo = service.next_block_algorithm(); let initial_price = read_algo.next_gas_price(); - // the RunnableTask depends on the handle passed to it for the da block cost source to already be running, - // which is the responsibility of the UninitializedTask in the `into_task` method of the RunnableService - // here we mimic that behaviour by running the da block cost service. - let mut da_source_watcher = StateWatcher::started(); - service - .da_source_adapter_handle - .run(&mut da_source_watcher) - .await; - - service.run(&mut watcher).await; - tokio::time::sleep(Duration::from_millis(100)).await; + let next = service.run(&mut watcher).await; + tokio::time::sleep(Duration::from_millis(3)).await; l2_block_sender.send(l2_block_2).await.unwrap(); // when - service.run(&mut watcher).await; - tokio::time::sleep(Duration::from_millis(100)).await; + let next = service.run(&mut watcher).await; + tokio::time::sleep(Duration::from_millis(3)).await; service.shutdown().await.unwrap(); // then @@ -660,6 +720,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, @@ -667,15 +728,16 @@ mod tests { capped_range_size: 100, decrease_range_size: 4, block_activity_threshold: 20, + da_poll_interval: None, } } #[tokio::test] - async fn run__responses_from_da_service_update_bundle_id_in_storage() { + async fn run__responses_from_da_service_update_recorded_height_in_storage() { // given - let bundle_id = 1234; - let block_height = 2; - let l2_block_2 = BlockInfo::Block { + let recorded_block_height = 100; + let block_height = 200; + let l2_block = BlockInfo::Block { height: block_height, gas_used: 60, block_gas_capacity: 100, @@ -698,27 +760,32 @@ mod tests { .insert(&BlockHeight::from(1), &100) .unwrap(); tx.commit().unwrap(); - let mut algo_updater = updater_from_config(&config); + let mut algo_updater = updater_from_config(&config, 0); let shared_algo = SharedGasPriceAlgo::new_with_algorithm(algo_updater.algorithm()); algo_updater.l2_block_height = block_height - 1; algo_updater.last_profit = 10_000; algo_updater.new_scaled_da_gas_price = 10_000_000; + let latest_l2_height = latest_l2_height(block_height - 1); let notifier = Arc::new(tokio::sync::Notify::new()); let da_source = DaSourceService::new( DummyDaBlockCosts::new( Ok(DaBlockCosts { - bundle_id, - l2_blocks: (1..2).collect(), + bundle_id: 8765, + l2_blocks: 1..=recorded_block_height, blob_cost_wei: 9000, bundle_size_bytes: 3000, }), notifier.clone(), ), Some(Duration::from_millis(1)), + latest_l2_height.clone(), + None, ); let mut watcher = StateWatcher::started(); + let da_service_runner = ServiceRunner::new(da_source); + da_service_runner.start_and_await().await.unwrap(); let latest_gas_price = LatestGasPrice::new(0, 0); let mut service = GasPriceServiceV1::new( @@ -726,37 +793,121 @@ mod tests { shared_algo, latest_gas_price, algo_updater, - da_source, + da_service_runner, inner, + latest_l2_height, ); let read_algo = service.next_block_algorithm(); let initial_price = read_algo.next_gas_price(); - // the RunnableTask depends on the handle passed to it for the da block cost source to already be running, - // which is the responsibility of the UninitializedTask in the `into_task` method of the RunnableService - // here we mimic that behaviour by running the da block cost service. - let mut da_source_watcher = StateWatcher::started(); - service - .da_source_adapter_handle - .run(&mut da_source_watcher) - .await; + service.run(&mut watcher).await; + tokio::time::sleep(Duration::from_millis(100)).await; + l2_block_sender.send(l2_block).await.unwrap(); + // when service.run(&mut watcher).await; tokio::time::sleep(Duration::from_millis(100)).await; - l2_block_sender.send(l2_block_2).await.unwrap(); + + // then + let latest_recorded_block_height = service + .storage_tx_provider + .get_recorded_height() + .unwrap() + .unwrap(); + assert_eq!( + latest_recorded_block_height, + BlockHeight::from(recorded_block_height) + ); + + service.shutdown().await.unwrap(); + } + + #[tokio::test] + async fn run__stores_correct_amount_for_costs() { + // given + let recorded_block_height = 100; + let block_height = 200; + let l2_block = BlockInfo::Block { + height: block_height, + gas_used: 60, + block_gas_capacity: 100, + block_bytes: 100, + block_fees: 100, + gas_price: 0, + }; + + let (l2_block_sender, l2_block_receiver) = mpsc::channel(1); + let l2_block_source = FakeL2BlockSource { + l2_block: l2_block_receiver, + }; + + let metadata_storage = FakeMetadata::empty(); + // Configured so exec gas price doesn't change, only da gas price + let config = arbitrary_v1_algorithm_config(); + let mut inner = database(); + let mut tx = inner.write_transaction(); + tx.storage_as_mut::() + .insert(&BlockHeight::from(1), &100) + .unwrap(); + tx.commit().unwrap(); + let mut algo_updater = updater_from_config(&config, 0); + let shared_algo = + SharedGasPriceAlgo::new_with_algorithm(algo_updater.algorithm()); + algo_updater.l2_block_height = block_height - 1; + algo_updater.last_profit = 10_000; + algo_updater.new_scaled_da_gas_price = 10_000_000; + + let notifier = Arc::new(tokio::sync::Notify::new()); + let blob_cost_wei = 9000; + let latest_l2_height = latest_l2_height(block_height - 1); + let da_source = DaSourceService::new( + DummyDaBlockCosts::new( + Ok(DaBlockCosts { + bundle_id: 8765, + l2_blocks: 1..=recorded_block_height, + blob_cost_wei, + bundle_size_bytes: 3000, + }), + notifier.clone(), + ), + Some(Duration::from_millis(1)), + latest_l2_height.clone(), + None, + ); + let mut watcher = StateWatcher::started(); + let da_service_runner = ServiceRunner::new(da_source); + da_service_runner.start_and_await().await.unwrap(); + let latest_gas_price = LatestGasPrice::new(0, 0); + + let mut service = GasPriceServiceV1::new( + l2_block_source, + shared_algo, + latest_gas_price, + algo_updater, + da_service_runner, + inner, + latest_l2_height, + ); + let read_algo = service.next_block_algorithm(); + let initial_price = read_algo.next_gas_price(); + + service.run(&mut watcher).await; + tokio::time::sleep(Duration::from_millis(100)).await; + l2_block_sender.send(l2_block).await.unwrap(); // when service.run(&mut watcher).await; tokio::time::sleep(Duration::from_millis(100)).await; // then - let latest_bundle_id = service + let metadata: V1Metadata = service .storage_tx_provider - .storage::() - .get(&BlockHeight::from(block_height)) + .storage::() + .get(&block_height.into()) .unwrap() + .and_then(|x| x.v1().cloned()) .unwrap(); - assert_eq!(*latest_bundle_id, bundle_id); + assert_eq!(metadata.latest_known_total_da_cost_excess, blob_cost_wei); service.shutdown().await.unwrap(); } diff --git a/crates/services/gas_price_service/src/v1/tests.rs b/crates/services/gas_price_service/src/v1/tests.rs index edbbbe9d6b8..8f208692c7a 100644 --- a/crates/services/gas_price_service/src/v1/tests.rs +++ b/crates/services/gas_price_service/src/v1/tests.rs @@ -20,16 +20,18 @@ use crate::{ ports::{ GasPriceData, GasPriceServiceAtomicStorage, - GetDaBundleId, + GetLatestRecordedHeight, GetMetadataStorage, L2Data, - SetDaBundleId, + SetLatestRecordedHeight, SetMetadataStorage, }, v1::{ algorithm::SharedV1Algorithm, + da_source_service, da_source_service::{ service::{ + new_da_service, DaBlockCostsSource, DaSourceService, }, @@ -61,10 +63,11 @@ use fuel_core_services::{ IntoBoxStream, }, RunnableTask, + Service, + ServiceRunner, StateWatcher, }; use fuel_core_storage::{ - iter::IteratorOverTable, structured_storage::test::InMemoryStorage, transactional::{ AtomicView, @@ -77,11 +80,18 @@ use fuel_core_storage::{ }; use fuel_core_types::{ blockchain::{ - block::Block, + block::{ + Block, + BlockV1, + PartialFuelBlock, + }, header::ConsensusParametersVersion, }, fuel_asm::op::exp, - fuel_tx::Transaction, + fuel_tx::{ + Mint, + Transaction, + }, fuel_types::BlockHeight, services::block_importer::{ ImportResult, @@ -96,12 +106,14 @@ use fuel_gas_price_algorithm::v1::{ UnrecordedBlocks, }; use std::{ + collections::HashMap, num::NonZeroU64, ops::Deref, sync::{ Arc, Mutex, }, + time::Duration, }; use tokio::sync::mpsc::Receiver; @@ -118,13 +130,13 @@ impl L2BlockSource for FakeL2BlockSource { } struct FakeMetadata { - inner: Arc>>, + inner: Arc>>, } impl FakeMetadata { fn empty() -> Self { Self { - inner: Arc::new(std::sync::Mutex::new(None)), + inner: Arc::new(Mutex::new(None)), } } } @@ -162,9 +174,9 @@ impl GetMetadataStorage for ErroringPersistedData { } } -impl GetDaBundleId for ErroringPersistedData { - fn get_bundle_id(&self, _block_height: &BlockHeight) -> GasPriceResult> { - Err(GasPriceError::CouldNotFetchDARecord(anyhow!("boo!"))) +impl GetLatestRecordedHeight for ErroringPersistedData { + fn get_recorded_height(&self) -> GasPriceResult> { + Err(GasPriceError::CouldNotFetchRecordedHeight(anyhow!("boo!"))) } } @@ -207,18 +219,14 @@ impl UnrecordedBlocks for UnimplementedStorageTx { } } -impl SetDaBundleId for UnimplementedStorageTx { - fn set_bundle_id( - &mut self, - _block_height: &BlockHeight, - _bundle_id: u32, - ) -> GasPriceResult<()> { +impl SetLatestRecordedHeight for UnimplementedStorageTx { + fn set_recorded_height(&mut self, _bundle_id: BlockHeight) -> GasPriceResult<()> { unimplemented!() } } -impl GetDaBundleId for UnimplementedStorageTx { - fn get_bundle_id(&self, _block_height: &BlockHeight) -> GasPriceResult> { +impl GetLatestRecordedHeight for UnimplementedStorageTx { + fn get_recorded_height(&self) -> GasPriceResult> { unimplemented!() } } @@ -235,7 +243,7 @@ impl AsUnrecordedBlocks for UnimplementedStorageTx { struct FakeDABlockCost { da_block_costs: Receiver, - bundle_id: Arc>>, + latest_requested_height: Arc>>, } impl FakeDABlockCost { @@ -243,38 +251,38 @@ impl FakeDABlockCost { let (_sender, receiver) = tokio::sync::mpsc::channel(1); Self { da_block_costs: receiver, - bundle_id: Arc::new(Mutex::new(None)), + latest_requested_height: Arc::new(Mutex::new(None)), } } fn new(da_block_costs: Receiver) -> Self { Self { da_block_costs, - bundle_id: Arc::new(Mutex::new(None)), + latest_requested_height: Arc::new(Mutex::new(None)), } } - fn never_returns_with_handle_to_bundle_id() -> (Self, Arc>>) { + fn never_returns_with_handle_to_last_height( + ) -> (Self, Arc>>) { let (_sender, receiver) = tokio::sync::mpsc::channel(1); - let bundle_id = Arc::new(Mutex::new(None)); + let height = Arc::new(Mutex::new(None)); let service = Self { da_block_costs: receiver, - bundle_id: bundle_id.clone(), + latest_requested_height: height.clone(), }; - (service, bundle_id) + (service, height) } } #[async_trait::async_trait] impl DaBlockCostsSource for FakeDABlockCost { - async fn request_da_block_cost(&mut self) -> Result { + async fn request_da_block_costs( + &mut self, + latest_recorded_height: &Option, + ) -> Result> { + *self.latest_requested_height.lock().unwrap() = *latest_recorded_height; let costs = self.da_block_costs.recv().await.unwrap(); - Ok(costs) - } - - async fn set_last_value(&mut self, bundle_id: u32) -> Result<()> { - self.bundle_id.lock().unwrap().replace(bundle_id); - Ok(()) + Ok(vec![costs]) } } @@ -286,6 +294,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, @@ -293,6 +302,7 @@ fn zero_threshold_arbitrary_config() -> V1AlgorithmConfig { capped_range_size: 0, decrease_range_size: 0, block_activity_threshold: 0, + da_poll_interval: None, } } @@ -319,6 +329,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, @@ -326,6 +337,7 @@ fn different_arb_config() -> V1AlgorithmConfig { capped_range_size: 0, decrease_range_size: 0, block_activity_threshold: 0, + da_poll_interval: None, } } @@ -333,7 +345,7 @@ fn database() -> StorageTransaction> { InMemoryStorage::default().into_transaction() } -fn database_with_metadata( +fn gas_price_database_with_metadata( metadata: &V1Metadata, ) -> StorageTransaction> { let mut db = database(); @@ -346,6 +358,9 @@ fn database_with_metadata( tx.commit().unwrap(); db } +fn latest_l2_height(height: u32) -> Arc> { + Arc::new(Mutex::new(BlockHeight::new(height))) +} #[tokio::test] async fn next_gas_price__affected_by_new_l2_block() { @@ -368,17 +383,21 @@ async fn next_gas_price__affected_by_new_l2_block() { let height = 0; let inner = database(); let (algo_updater, shared_algo) = - initialize_algorithm(&config, height, &metadata_storage).unwrap(); + initialize_algorithm(&config, height, height, &metadata_storage).unwrap(); let da_source = FakeDABlockCost::never_returns(); - let da_source_service = DaSourceService::new(da_source, None); + let latest_l2_height = latest_l2_height(0); + let da_service_runner = new_da_service(da_source, None, latest_l2_height.clone()); + da_service_runner.start_and_await().await.unwrap(); + 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, + da_service_runner, inner, + latest_l2_height, ); let read_algo = service.next_block_algorithm(); @@ -414,18 +433,21 @@ async fn run__new_l2_block_saves_old_metadata() { let config = zero_threshold_arbitrary_config(); let inner = database(); - let algo_updater = updater_from_config(&config); + let algo_updater = updater_from_config(&config, 0); 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_l2_height = latest_l2_height(0); + let da_service_runner = new_da_service(da_source, None, latest_l2_height.clone()); + da_service_runner.start_and_await().await.unwrap(); 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, + da_service_runner, inner, + latest_l2_height, ); let mut watcher = StateWatcher::started(); @@ -465,18 +487,20 @@ async fn run__new_l2_block_updates_latest_gas_price_arc() { let config = zero_threshold_arbitrary_config(); let inner = database(); - let algo_updater = updater_from_config(&config); + let algo_updater = updater_from_config(&config, 0); 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_l2_height = latest_l2_height(0); + let da_service_runner = new_da_service(da_source, None, latest_l2_height.clone()); 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, + da_service_runner, inner, + latest_l2_height, ); let mut watcher = StateWatcher::started(); @@ -493,15 +517,75 @@ async fn run__new_l2_block_updates_latest_gas_price_arc() { service.shutdown().await.unwrap(); } +#[tokio::test] +async fn run__updates_da_service_latest_l2_height() { + // given + let l2_height = 10; + let l2_block = BlockInfo::Block { + height: l2_height, + gas_used: 60, + 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 { + l2_block: l2_block_receiver, + }; + + let config = zero_threshold_arbitrary_config(); + let inner = database(); + let mut algo_updater = updater_from_config(&config, 0); + algo_updater.l2_block_height = l2_height - 1; + let shared_algo = SharedV1Algorithm::new_with_algorithm(algo_updater.algorithm()); + let da_source = FakeDABlockCost::never_returns(); + let latest_l2_height = latest_l2_height(0); + let latest_gas_price = LatestGasPrice::new(0, 0); + let da_service_runner = new_da_service(da_source, None, latest_l2_height.clone()); + da_service_runner.start_and_await().await.unwrap(); + let mut service = GasPriceServiceV1::new( + l2_block_source, + shared_algo, + latest_gas_price, + algo_updater, + da_service_runner, + inner, + latest_l2_height.clone(), + ); + let mut watcher = StateWatcher::started(); + + // when + l2_block_sender.send(l2_block).await.unwrap(); + let _ = service.run(&mut watcher).await; + + // then + let latest_value = *latest_l2_height.lock().unwrap(); + assert_eq!(*latest_value, l2_height); +} + #[derive(Clone)] -struct FakeSettings; +struct FakeSettings { + settings: GasPriceSettings, +} + +impl Default for FakeSettings { + fn default() -> Self { + Self { + settings: GasPriceSettings { + gas_price_factor: 100, + block_gas_limit: u64::MAX, + }, + } + } +} impl GasPriceSettingsProvider for FakeSettings { fn settings( &self, _param_version: &ConsensusParametersVersion, ) -> GasPriceResult { - unimplemented!() + Ok(self.settings.clone()) } } @@ -532,23 +616,29 @@ impl GasPriceData for FakeGasPriceDb { #[derive(Clone)] struct FakeOnChainDb { height: BlockHeight, + blocks: HashMap>, } impl FakeOnChainDb { fn new(height: u32) -> Self { Self { height: height.into(), + blocks: HashMap::new(), } } } struct FakeL2Data { height: BlockHeight, + blocks: HashMap>, } impl FakeL2Data { - fn new(height: BlockHeight) -> Self { - Self { height } + fn new( + height: BlockHeight, + blocks: HashMap>, + ) -> Self { + Self { height, blocks } } } @@ -559,16 +649,16 @@ impl L2Data for FakeL2Data { fn get_block( &self, - _height: &BlockHeight, + height: &BlockHeight, ) -> StorageResult>> { - Ok(None) + Ok(self.blocks.get(height).cloned()) } } impl AtomicView for FakeOnChainDb { type LatestView = FakeL2Data; fn latest_view(&self) -> StorageResult { - Ok(FakeL2Data::new(self.height)) + Ok(FakeL2Data::new(self.height, self.blocks.clone())) } } @@ -581,22 +671,16 @@ fn empty_block_stream() -> BoxStream { async fn uninitialized_task__new__if_exists_already_reload_old_values_with_overrides() { // given let original_metadata = arbitrary_metadata(); - let original = UpdaterMetadata::V1(original_metadata.clone()); - let metadata_inner = Arc::new(std::sync::Mutex::new(Some(original.clone()))); - let metadata_storage = FakeMetadata { - inner: metadata_inner, - }; - let different_config = different_arb_config(); let descaleed_exec_price = original_metadata.new_scaled_exec_price / original_metadata.gas_price_factor; assert_ne!(different_config.new_exec_gas_price, descaleed_exec_price); let different_l2_block = 0; - let settings = FakeSettings; + let settings = FakeSettings::default(); let block_stream = empty_block_stream(); let on_chain_db = FakeOnChainDb::new(different_l2_block); let da_cost_source = FakeDABlockCost::never_returns(); - let inner = database_with_metadata(&original_metadata); + let inner = gas_price_database_with_metadata(&original_metadata); // when let service = UninitializedTask::new( different_config.clone(), @@ -691,13 +775,42 @@ fn algo_updater_override_values_match( assert_eq!(algo_updater.da_d_component, config.da_d_component); } +#[tokio::test] +async fn uninitialized_task__new__if_no_metadata_found_use_latest_l2_height() { + // given + let different_config = different_arb_config(); + let l2_block = 1234; + let settings = FakeSettings::default(); + let block_stream = empty_block_stream(); + let on_chain_db = FakeOnChainDb::new(l2_block); + let da_cost_source = FakeDABlockCost::never_returns(); + let inner = database(); + // when + let service = UninitializedTask::new( + different_config.clone(), + None, + 0.into(), + settings, + block_stream, + inner, + da_cost_source, + on_chain_db, + ) + .unwrap(); + + // then + let UninitializedTask { algo_updater, .. } = service; + let algo_height = algo_updater.l2_block_height; + assert_eq!(algo_height, l2_block); +} + #[tokio::test] async fn uninitialized_task__new__should_fail_if_cannot_fetch_metadata() { // given let config = zero_threshold_arbitrary_config(); let different_l2_block = 1231; let erroring_persisted_data = ErroringPersistedData; - let settings = FakeSettings; + let settings = FakeSettings::default(); let block_stream = empty_block_stream(); let on_chain_db = FakeOnChainDb::new(different_l2_block); let da_cost_source = FakeDABlockCost::never_returns(); @@ -720,26 +833,28 @@ async fn uninitialized_task__new__should_fail_if_cannot_fetch_metadata() { } #[tokio::test] -async fn uninitialized_task__init__starts_da_service_with_bundle_id_in_storage() { +async fn uninitialized_task__init__starts_da_service_with_recorded_height_in_storage() { // given - let block_height = 1; - let bundle_id: u32 = 123; + let block_height = 100; + let recorded_height: u32 = 200; let original_metadata = arbitrary_metadata(); - let different_config = different_arb_config(); + let mut different_config = different_arb_config(); let descaleed_exec_price = original_metadata.new_scaled_exec_price / original_metadata.gas_price_factor; assert_ne!(different_config.new_exec_gas_price, descaleed_exec_price); let different_l2_block = 0; - let settings = FakeSettings; + let settings = FakeSettings::default(); let block_stream = empty_block_stream(); let on_chain_db = FakeOnChainDb::new(different_l2_block); - let (da_cost_source, bundle_id_handle) = - FakeDABlockCost::never_returns_with_handle_to_bundle_id(); - let mut inner = database_with_metadata(&original_metadata); + let (da_cost_source, latest_requested_recorded_height) = + FakeDABlockCost::never_returns_with_handle_to_last_height(); + let mut inner = gas_price_database_with_metadata(&original_metadata); let mut tx = inner.begin_transaction().unwrap(); - tx.set_bundle_id(&block_height.into(), bundle_id).unwrap(); + tx.set_recorded_height(BlockHeight::from(recorded_height)) + .unwrap(); StorageTransaction::commit_transaction(tx).unwrap(); + different_config.da_poll_interval = Some(Duration::from_millis(1)); let service = UninitializedTask::new( different_config.clone(), Some(block_height.into()), @@ -753,10 +868,69 @@ async fn uninitialized_task__init__starts_da_service_with_bundle_id_in_storage() .unwrap(); // when - service.init().await.unwrap(); + let _gas_price_service = service.init(&StateWatcher::started()).await.unwrap(); + tokio::time::sleep(std::time::Duration::from_millis(3)).await; // then - let actual = bundle_id_handle.lock().unwrap(); - let expected = Some(bundle_id); + let actual = latest_requested_recorded_height.lock().unwrap(); + let expected = Some(BlockHeight::new(recorded_height)); assert_eq!(*actual, expected); } + +fn arb_block() -> Block { + let mut block = Block::default(); + let mint = Mint::default(); + block.transactions_mut().push(mint.into()); + block +} + +#[tokio::test] +async fn uninitialized_task__init__if_metadata_behind_l2_height_then_sync() { + // given + let metadata_height = 100; + let l2_height = 200; + let config = zero_threshold_arbitrary_config(); + + let metadata = V1Metadata { + new_scaled_exec_price: 100, + l2_block_height: metadata_height, + new_scaled_da_gas_price: 0, + gas_price_factor: NonZeroU64::new(100).unwrap(), + total_da_rewards_excess: 0, + latest_known_total_da_cost_excess: 0, + last_profit: 0, + second_to_last_profit: 0, + latest_da_cost_per_byte: 0, + unrecorded_block_bytes: 0, + }; + let gas_price_db = gas_price_database_with_metadata(&metadata); + let mut onchain_db = FakeOnChainDb::new(l2_height); + for height in 1..=l2_height { + let block = arb_block(); + onchain_db.blocks.insert(BlockHeight::from(height), block); + } + + let service = UninitializedTask::new( + config, + Some(metadata_height.into()), + 0.into(), + FakeSettings::default(), + empty_block_stream(), + gas_price_db, + FakeDABlockCost::never_returns(), + onchain_db.clone(), + ) + .unwrap(); + + // when + let gas_price_service = service.init(&StateWatcher::started()).await.unwrap(); + + // then + // sleep to allow the service to sync + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + + let on_chain_height = u32::from(onchain_db.height); + let algo_updater_height = gas_price_service.algorithm_updater().l2_block_height; + + assert_eq!(on_chain_height, algo_updater_height); +} 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 571f3e9f84c..af6e1925a4f 100644 --- a/crates/services/gas_price_service/src/v1/uninitialized_task.rs +++ b/crates/services/gas_price_service/src/v1/uninitialized_task.rs @@ -4,6 +4,7 @@ use crate::{ block_bytes, get_block_info, mint_values, + storage::GasPriceColumn, GasPriceSettings, GasPriceSettingsProvider, }, @@ -19,11 +20,10 @@ use crate::{ ports::{ GasPriceData, GasPriceServiceAtomicStorage, - GasPriceServiceConfig, - GetDaBundleId, + GetLatestRecordedHeight, GetMetadataStorage, L2Data, - SetDaBundleId, + SetLatestRecordedHeight, SetMetadataStorage, }, v1::{ @@ -55,7 +55,9 @@ use anyhow::Error; use fuel_core_services::{ stream::BoxStream, RunnableService, + Service, ServiceRunner, + State, StateWatcher, }; use fuel_core_storage::{ @@ -79,7 +81,10 @@ use fuel_gas_price_algorithm::v1::{ AlgorithmUpdaterV1, UnrecordedBlocks, }; -use std::sync::Arc; +use std::{ + sync::Arc, + time::Duration, +}; pub mod fuel_storage_unrecorded_blocks; @@ -103,7 +108,7 @@ where L2DataStore: L2Data, L2DataStoreView: AtomicView, AtomicStorage: GasPriceServiceAtomicStorage, - DA: DaBlockCostsSource, + DA: DaBlockCostsSource + 'static, SettingsProvider: GasPriceSettingsProvider, { #[allow(clippy::too_many_arguments)] @@ -130,9 +135,16 @@ where Some(gas_price) }) .unwrap_or(0); + let gas_price_metadata_height = gas_metadata_height + .map(|x| x.into()) + .unwrap_or(latest_block_height); - let (algo_updater, shared_algo) = - initialize_algorithm(&config, latest_block_height, &gas_price_db)?; + let (algo_updater, shared_algo) = initialize_algorithm( + &config, + gas_price_metadata_height, + latest_block_height, + &gas_price_db, + )?; let latest_gas_price = LatestGasPrice::new(latest_block_height, latest_gas_price); @@ -154,6 +166,7 @@ where pub async fn init( mut self, + state_watcher: &StateWatcher, ) -> anyhow::Result< GasPriceServiceV1, DA, AtomicStorage>, > { @@ -179,37 +192,40 @@ where self.block_stream, ); - // TODO: Add to config - // https://github.com/FuelLabs/fuel-core/issues/2140 - let poll_interval = None; - if let Some(bundle_id) = - self.gas_price_db.get_bundle_id(&metadata_height.into())? - { - self.da_source.set_last_value(bundle_id).await?; - } - let da_service = DaSourceService::new(self.da_source, poll_interval); + let recorded_height = self.gas_price_db.get_recorded_height()?; + let poll_duration = self.config.da_poll_interval; + let latest_l2_height = + Arc::new(std::sync::Mutex::new(BlockHeight::new(latest_block_height))); + let da_service = DaSourceService::new( + self.da_source, + poll_duration, + latest_l2_height.clone(), + recorded_height, + ); + let da_service_runner = ServiceRunner::new(da_service); + da_service_runner.start_and_await().await?; - if BlockHeight::from(latest_block_height) == self.genesis_block_height - || first_run - { + if BlockHeight::from(latest_block_height) == self.genesis_block_height { let service = GasPriceServiceV1::new( l2_block_source, self.shared_algo, self.latest_gas_price, self.algo_updater, - da_service, + da_service_runner, self.gas_price_db, + latest_l2_height, ); Ok(service) } else { if latest_block_height > metadata_height { sync_gas_price_db_with_on_chain_storage( &self.settings, - &self.config, &self.on_chain_db, metadata_height, latest_block_height, + &mut self.algo_updater, &mut self.gas_price_db, + state_watcher, )?; } @@ -218,8 +234,9 @@ where self.shared_algo, self.latest_gas_price, self.algo_updater, - da_service, + da_service_runner, self.gas_price_db, + latest_l2_height, ); Ok(service) } @@ -247,10 +264,10 @@ where async fn into_task( self, - _state_watcher: &StateWatcher, + state_watcher: &StateWatcher, _params: Self::TaskParams, ) -> anyhow::Result { - UninitializedTask::init(self).await + UninitializedTask::init(self, state_watcher).await } } @@ -260,52 +277,13 @@ fn sync_gas_price_db_with_on_chain_storage< SettingsProvider, AtomicStorage, >( - settings: &SettingsProvider, - config: &V1AlgorithmConfig, - on_chain_db: &L2DataStoreView, - metadata_height: u32, - latest_block_height: u32, - persisted_data: &mut AtomicStorage, -) -> anyhow::Result<()> -where - L2DataStore: L2Data, - L2DataStoreView: AtomicView, - SettingsProvider: GasPriceSettingsProvider, - AtomicStorage: GasPriceServiceAtomicStorage, -{ - let metadata = persisted_data - .get_metadata(&metadata_height.into())? - .ok_or(anyhow::anyhow!( - "Expected metadata to exist for height: {metadata_height}" - ))?; - - let metadata = match metadata { - UpdaterMetadata::V1(metadata) => metadata, - UpdaterMetadata::V0(metadata) => { - V1Metadata::construct_from_v0_metadata(metadata, config)? - } - }; - let mut algo_updater = v1_algorithm_from_metadata(metadata, config); - - sync_v1_metadata( - settings, - on_chain_db, - metadata_height, - latest_block_height, - &mut algo_updater, - persisted_data, - )?; - - Ok(()) -} - -fn sync_v1_metadata( settings: &SettingsProvider, on_chain_db: &L2DataStoreView, metadata_height: u32, latest_block_height: u32, updater: &mut AlgorithmUpdaterV1, da_storage: &mut AtomicStorage, + state_watcher: &StateWatcher, ) -> anyhow::Result<()> where L2DataStore: L2Data, @@ -315,8 +293,20 @@ where { let first = metadata_height.saturating_add(1); let view = on_chain_db.latest_view()?; - let mut tx = da_storage.begin_transaction()?; + tracing::debug!( + "Syncing gas price metadata from {} to {}", + first, + latest_block_height + ); for height in first..=latest_block_height { + // allows early exit if the service is stopping + let state = state_watcher.borrow(); + if state.stopping() || state.stopped() { + return Ok(()); + } + + tracing::info!("Syncing gas price metadata for block {}", height); + let mut tx = da_storage.begin_transaction()?; let block = view .get_block(&height.into())? .ok_or(not_found!("FullBlock"))?; @@ -337,7 +327,8 @@ where }; let block_bytes = block_bytes(&block); - let (fee_wei, _) = mint_values(&block)?; + let (fee_gwei, _) = mint_values(&block)?; + let fee_wei = fee_gwei.saturating_mul(1_000_000_000); updater.update_l2_block_data( height, block_gas_used, @@ -348,8 +339,8 @@ where )?; let metadata: UpdaterMetadata = updater.clone().into(); tx.set_metadata(&metadata)?; + AtomicStorage::commit_transaction(tx)?; } - AtomicStorage::commit_transaction(tx)?; Ok(()) } diff --git a/crates/services/gas_price_service/src/v1/uninitialized_task/fuel_storage_unrecorded_blocks.rs b/crates/services/gas_price_service/src/v1/uninitialized_task/fuel_storage_unrecorded_blocks.rs index 61d6bae5528..62c9f3ff39c 100644 --- a/crates/services/gas_price_service/src/v1/uninitialized_task/fuel_storage_unrecorded_blocks.rs +++ b/crates/services/gas_price_service/src/v1/uninitialized_task/fuel_storage_unrecorded_blocks.rs @@ -16,7 +16,10 @@ use fuel_core_storage::{ StorageAsRef, StorageMutate, }; -use fuel_core_types::fuel_merkle::storage::StorageMutateInfallible; +use fuel_core_types::{ + fuel_merkle::storage::StorageMutateInfallible, + fuel_types::BlockHeight, +}; use fuel_gas_price_algorithm::{ v1, v1::UnrecordedBlocks, @@ -59,18 +62,20 @@ where S: StorageMutate, { fn insert(&mut self, height: v1::Height, bytes: v1::Bytes) -> Result<(), String> { + let block_height = BlockHeight::from(height); self.inner .storage_as_mut::() - .insert(&height.into(), &bytes) + .insert(&block_height, &bytes) .map_err(|err| format!("Error: {:?}", err))?; Ok(()) } fn remove(&mut self, height: &v1::Height) -> Result, String> { + let block_height = BlockHeight::from(*height); let bytes = self .inner .storage_as_mut::() - .take(&(*height).into()) + .take(&block_height) .map_err(|err| format!("Error: {:?}", err))?; Ok(bytes) } diff --git a/crates/services/relayer/Cargo.toml b/crates/services/relayer/Cargo.toml index e29efbf4aa9..017f11a8478 100644 --- a/crates/services/relayer/Cargo.toml +++ b/crates/services/relayer/Cargo.toml @@ -36,7 +36,7 @@ strum_macros = { workspace = true } thiserror = { workspace = true, optional = true } tokio = { workspace = true, features = ["macros"] } tracing = { workspace = true } -url = "2.2" +url = { workspace = true } [dev-dependencies] fuel-core-relayer = { path = "", features = ["test-helpers"] } diff --git a/crates/services/src/service.rs b/crates/services/src/service.rs index 5b298ffe5e0..2964841d6f6 100644 --- a/crates/services/src/service.rs +++ b/crates/services/src/service.rs @@ -84,6 +84,7 @@ pub trait RunnableService: Send { } /// The result of a single iteration of the service task +#[derive(Debug)] pub enum TaskNextAction { /// Request the task to be run again Continue, @@ -356,7 +357,7 @@ async fn run( let mut task = service .into_task(&state, params) .await - .expect("The initialization of the service failed."); + .expect("The initialization of the service failed"); sender.send_if_modified(|s| { if s.starting() { diff --git a/tests/Cargo.toml b/tests/Cargo.toml index bffb2fc966a..f11175776e8 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -38,7 +38,9 @@ fuel-core-benches = { path = "../benches" } fuel-core-bin = { path = "../bin/fuel-core", features = ["parquet", "p2p"] } fuel-core-client = { path = "../crates/client", features = ["test-helpers"] } fuel-core-compression = { path = "../crates/compression" } -fuel-core-gas-price-service = { path = "../crates/services/gas_price_service" } +fuel-core-gas-price-service = { path = "../crates/services/gas_price_service", features = [ + "test-helpers", +] } fuel-core-p2p = { path = "../crates/services/p2p", features = [ "test-helpers", ], optional = true } @@ -74,6 +76,8 @@ tokio = { workspace = true, features = [ "rt-multi-thread", "test-util", ] } +tracing-subscriber = { workspace = true } +url = { workspace = true } [dev-dependencies] fuel-core-executor = { workspace = true, features = ["limited-tx-count"] } diff --git a/tests/test-helpers/src/builder.rs b/tests/test-helpers/src/builder.rs index cb615cea811..1b866796f95 100644 --- a/tests/test-helpers/src/builder.rs +++ b/tests/test-helpers/src/builder.rs @@ -248,7 +248,7 @@ impl TestSetupBuilder { utxo_validation: self.utxo_validation, txpool, block_production: self.trigger, - starting_gas_price: self.starting_gas_price, + starting_exec_gas_price: self.starting_gas_price, ..Config::local_node_with_configs(chain_conf, state) }; config.combined_db_config.database_config = self.database_config; diff --git a/tests/test-helpers/src/fuel_core_driver.rs b/tests/test-helpers/src/fuel_core_driver.rs index a290712e5d2..adac26b70b0 100644 --- a/tests/test-helpers/src/fuel_core_driver.rs +++ b/tests/test-helpers/src/fuel_core_driver.rs @@ -26,6 +26,12 @@ impl FuelCoreDriver { "0", "--gas-price-change-percent", "0", + "--min-da-gas-price", + "0", + "--da-gas-price-p-component", + "0", + "--da-gas-price-d-component", + "0", ]; args.extend(extra_args); Self::spawn_with_directory(tempdir()?, &args).await diff --git a/tests/tests/chain.rs b/tests/tests/chain.rs index c5c62b8f600..42daf4f86d0 100644 --- a/tests/tests/chain.rs +++ b/tests/tests/chain.rs @@ -141,7 +141,7 @@ async fn network_operates_with_non_zero_base_asset_id() { let node_config = Config { debug: true, utxo_validation: true, - starting_gas_price, + starting_exec_gas_price: starting_gas_price, ..Config::local_node_with_configs(chain_config, state_config) }; diff --git a/tests/tests/gas_price.rs b/tests/tests/gas_price.rs index 5b24470ca34..6b3e8d87e7c 100644 --- a/tests/tests/gas_price.rs +++ b/tests/tests/gas_price.rs @@ -21,7 +21,17 @@ use fuel_core_client::client::{ }; use fuel_core_gas_price_service::{ common::fuel_core_storage_adapter::storage::GasPriceMetadata, - v0::metadata::V0Metadata, + ports::{ + GasPriceData, + GetMetadataStorage, + }, + v1::{ + da_source_service::block_committer_costs::{ + fake_server::FakeServer, + RawDaBlockCosts, + }, + metadata::V1Metadata, + }, }; use fuel_core_poa::Trigger; use fuel_core_storage::{ @@ -29,6 +39,7 @@ use fuel_core_storage::{ StorageAsRef, }; use fuel_core_types::{ + blockchain::primitives::DaBlockHeight, fuel_asm::*, fuel_crypto::{ coins_bip32::ecdsa::signature::rand_core::SeedableRng, @@ -36,6 +47,7 @@ use fuel_core_types::{ }, fuel_tx::{ consensus_parameters::ConsensusParametersV1, + AssetId, ConsensusParameters, Finalizable, Transaction, @@ -45,6 +57,7 @@ use fuel_core_types::{ }; use rand::Rng; use std::{ + self, iter::repeat, ops::Deref, time::Duration, @@ -59,14 +72,61 @@ fn tx_for_gas_limit(max_fee_limit: Word) -> Transaction { .into() } +fn infinite_loop_tx( + max_fee_limit: Word, + rng: &mut R, + asset_id: Option, +) -> Transaction { + let script = vec![op::jmp(RegId::ZERO)]; + let script_bytes = script.iter().flat_map(|op| op.to_bytes()).collect(); + let mut builder = TransactionBuilder::script(script_bytes, vec![]); + let asset_id = asset_id.unwrap_or_else(|| *builder.get_params().base_asset_id()); + builder + .max_fee_limit(max_fee_limit) + .script_gas_limit(800_000) + .add_unsigned_coin_input( + SecretKey::random(rng), + rng.gen(), + u32::MAX as u64, + asset_id, + Default::default(), + ) + .finalize() + .into() +} + fn arb_large_tx( max_fee_limit: Word, rng: &mut R, + asset_id: Option, ) -> Transaction { let mut script: Vec<_> = repeat(op::noop()).take(10_000).collect(); script.push(op::ret(RegId::ONE)); let script_bytes = script.iter().flat_map(|op| op.to_bytes()).collect(); let mut builder = TransactionBuilder::script(script_bytes, vec![]); + let asset_id = asset_id.unwrap_or_else(|| *builder.get_params().base_asset_id()); + builder + .max_fee_limit(max_fee_limit) + .script_gas_limit(600_000) + .add_unsigned_coin_input( + SecretKey::random(rng), + rng.gen(), + u32::MAX as u64, + asset_id, + Default::default(), + ) + .finalize() + .into() +} + +fn arb_small_tx( + max_fee_limit: Word, + rng: &mut R, +) -> Transaction { + let mut script: Vec<_> = repeat(op::noop()).take(10).collect(); + script.push(op::ret(RegId::ONE)); + let script_bytes = script.iter().flat_map(|op| op.to_bytes()).collect(); + let mut builder = TransactionBuilder::script(script_bytes, vec![]); let asset_id = *builder.get_params().base_asset_id(); builder .max_fee_limit(max_fee_limit) @@ -103,7 +163,7 @@ async fn latest_gas_price__for_single_block_should_be_starting_gas_price() { // given let mut config = Config::local_node(); let starting_gas_price = 982; - config.starting_gas_price = starting_gas_price; + config.starting_exec_gas_price = starting_gas_price; let srv = FuelService::from_database(Database::default(), config.clone()) .await .unwrap(); @@ -137,10 +197,15 @@ async fn produce_block__raises_gas_price() { let percent = 10; let threshold = 50; node_config.block_producer.coinbase_recipient = Some([5; 32].into()); - node_config.starting_gas_price = starting_gas_price; - node_config.gas_price_change_percent = percent; - node_config.gas_price_threshold_percent = threshold; + node_config.starting_exec_gas_price = starting_gas_price; + node_config.exec_gas_price_change_percent = percent; + node_config.exec_gas_price_threshold_percent = threshold; node_config.block_production = Trigger::Never; + node_config.da_gas_price_p_component = 0; + node_config.da_gas_price_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); @@ -149,7 +214,7 @@ async fn produce_block__raises_gas_price() { // when let arb_tx_count = 10; for i in 0..arb_tx_count { - let tx = arb_large_tx(189028 + i as Word, &mut rng); + let tx = arb_large_tx(18902800 + i as Word, &mut rng, None); let _status = client.submit(&tx).await.unwrap(); } // starting gas price @@ -158,7 +223,7 @@ async fn produce_block__raises_gas_price() { let _ = client.produce_blocks(1, None).await.unwrap(); // then - let change = starting_gas_price * percent / 100; + let change = starting_gas_price * percent as u64 / 100; let expected = starting_gas_price + change; let latest = client.latest_gas_price().await.unwrap(); let actual = latest.gas_price; @@ -182,10 +247,15 @@ async fn produce_block__lowers_gas_price() { let percent = 10; let threshold = 50; node_config.block_producer.coinbase_recipient = Some([5; 32].into()); - node_config.starting_gas_price = starting_gas_price; - node_config.gas_price_change_percent = percent; - node_config.gas_price_threshold_percent = threshold; + node_config.starting_exec_gas_price = starting_gas_price; + node_config.exec_gas_price_change_percent = percent; + node_config.exec_gas_price_threshold_percent = threshold; node_config.block_production = Trigger::Never; + node_config.da_gas_price_p_component = 0; + node_config.da_gas_price_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); @@ -194,7 +264,7 @@ async fn produce_block__lowers_gas_price() { // when let arb_tx_count = 5; for i in 0..arb_tx_count { - let tx = arb_large_tx(189028 + i as Word, &mut rng); + let tx = arb_large_tx(18902800 + i as Word, &mut rng, None); let _status = client.submit(&tx).await.unwrap(); } // starting gas price @@ -203,23 +273,68 @@ async fn produce_block__lowers_gas_price() { let _ = client.produce_blocks(1, None).await.unwrap(); // then - let change = starting_gas_price * percent / 100; + let change = starting_gas_price * percent as u64 / 100; let expected = starting_gas_price - change; let latest = client.latest_gas_price().await.unwrap(); let actual = latest.gas_price; assert_eq!(expected, actual); } +#[tokio::test] +async fn produce_block__dont_raises_gas_price_with_default_parameters() { + // given + let args = vec![ + "--debug", + "--poa-instant", + "false", + "--coinbase-recipient", + "0x1111111111111111111111111111111111111111111111111111111111111111", + ]; + let driver = FuelCoreDriver::spawn(&args).await.unwrap(); + + let expected_default_da_gas_price = 0; + + let expected_gas_price = expected_default_da_gas_price; + + let mut rng = rand::rngs::StdRng::seed_from_u64(2322u64); + + let base_asset_id = driver + .client + .consensus_parameters(0) + .await + .unwrap() + .unwrap() + .base_asset_id() + .clone(); + + // when + let arb_tx_count = 20; + for _ in 0..arb_tx_count { + let tx = infinite_loop_tx(200_000_000, &mut rng, Some(base_asset_id)); + let _status = driver.client.submit(&tx).await.unwrap(); + } + + // starting gas price + let _ = driver.client.produce_blocks(1, None).await.unwrap(); + + // updated gas price + let _ = driver.client.produce_blocks(1, None).await.unwrap(); + let latest_gas_price = driver.client.latest_gas_price().await.unwrap().gas_price; + + assert_eq!(expected_gas_price, latest_gas_price); + driver.kill().await; +} + #[tokio::test] async fn estimate_gas_price__is_greater_than_actual_price_at_desired_height() { // given let mut node_config = Config::local_node(); let starting_gas_price = 1000; let percent = 10; - node_config.starting_gas_price = starting_gas_price; - node_config.gas_price_change_percent = percent; + node_config.starting_exec_gas_price = starting_gas_price; + node_config.exec_gas_price_change_percent = percent; // Always increase - node_config.gas_price_threshold_percent = 0; + node_config.exec_gas_price_threshold_percent = 0; let srv = FuelService::new_node(node_config.clone()).await.unwrap(); let client = FuelClient::from(srv.bound_address); @@ -241,25 +356,6 @@ async fn estimate_gas_price__is_greater_than_actual_price_at_desired_height() { assert!(estimated >= real); } -#[tokio::test] -async fn estimate_gas_price__returns_min_gas_price_if_starting_gas_price_is_zero() { - const MIN_GAS_PRICE: u64 = 1; - - // Given - let mut node_config = Config::local_node(); - node_config.min_gas_price = MIN_GAS_PRICE; - node_config.starting_gas_price = 0; - let srv = FuelService::new_node(node_config.clone()).await.unwrap(); - let client = FuelClient::from(srv.bound_address); - - // When - let result = client.estimate_gas_price(10).await.unwrap(); - - // Then - let actual = result.gas_price.0; - assert_eq!(MIN_GAS_PRICE, actual) -} - #[tokio::test(flavor = "multi_thread")] async fn latest_gas_price__if_node_restarts_gets_latest_value() { // given @@ -275,7 +371,7 @@ async fn latest_gas_price__if_node_restarts_gets_latest_value() { "0", ]; let driver = FuelCoreDriver::spawn(&args).await.unwrap(); - let starting = driver.node.shared.config.starting_gas_price; + let starting = driver.node.shared.config.starting_exec_gas_price; let arb_blocks_to_produce = 10; for _ in 0..arb_blocks_to_produce { driver.client.produce_blocks(1, None).await.unwrap(); @@ -398,6 +494,7 @@ async fn startup__can_override_gas_price_values_by_changing_config() { .produce_blocks(1, None) .await .unwrap(); + tokio::time::sleep(Duration::from_millis(10)).await; let new_height = 2; let recovered_database = &recovered_driver.node.shared.database; @@ -410,9 +507,387 @@ async fn startup__can_override_gas_price_values_by_changing_config() { .deref() .clone(); - let V0Metadata { + let V1Metadata { l2_block_height, .. } = new_metadata.try_into().unwrap(); assert_eq!(l2_block_height, new_height); recovered_driver.kill().await; } + +#[test] +fn produce_block__l1_committed_block_affects_gas_price() { + let rt = tokio::runtime::Runtime::new().unwrap(); + // set up chain with single unrecorded block + let mut args = vec![ + "--debug", + "--poa-instant", + "true", + "--min-da-gas-price", + "100", + ]; + + let mut default_args = args.clone(); + // Start without da gas price updates + default_args.extend([ + "--da-gas-price-p-component", + "0", + "--da-gas-price-d-component", + "0", + "--starting-gas-price", + "0", + "--gas-price-change-percent", + "0", + ]); + + let (first_gas_price, temp_dir) = rt.block_on(async { + let driver = FuelCoreDriver::spawn(&default_args).await.unwrap(); + driver.client.produce_blocks(1, None).await.unwrap(); + let first_gas_price: u64 = driver + .client + .estimate_gas_price(0) + .await + .unwrap() + .gas_price + .into(); + tokio::time::sleep(Duration::from_millis(100)).await; + let temp_dir = driver.kill().await; + (first_gas_price, temp_dir) + }); + + assert_eq!(100u64, first_gas_price); + + let mut mock = FakeServer::new(); + let url = mock.url(); + let costs = RawDaBlockCosts { + id: 1, + start_height: 1, + end_height: 1, + da_block_height: DaBlockHeight(100), + cost: 100, + size: 100, + }; + mock.add_response(costs); + + // add the da committer url to the args, as well as set da parameters to modify the gas price + args.extend(&[ + "--da-committer-url", + url.as_str(), + "--da-poll-interval", + "1ms", + "--da-gas-price-p-component", + "1", + "--gas-price-change-percent", + "100", + ]); + + // when + let new_gas_price = rt + .block_on(async { + let driver = FuelCoreDriver::spawn_with_directory(temp_dir, &args) + .await + .unwrap(); + tokio::time::sleep(Duration::from_millis(20)).await; + // Won't accept DA costs until l2_height is > 1 + driver.client.produce_blocks(1, None).await.unwrap(); + // Wait for DaBlockCosts to be accepted + tokio::time::sleep(Duration::from_millis(2)).await; + // Produce new block to update gas price + driver.client.produce_blocks(1, None).await.unwrap(); + tokio::time::sleep(Duration::from_millis(20)).await; + let gas_price = driver.client.estimate_gas_price(0).await.unwrap().gas_price; + // cleanup + driver.kill().await; + gas_price + }) + .into(); + + // then + assert!(first_gas_price < new_gas_price); + rt.shutdown_timeout(tokio::time::Duration::from_millis(100)); +} + +#[test] +fn run__if_metadata_is_behind_l2_then_will_catch_up() { + // given + // produce 100 blocks + let args = vec![ + "--debug", + "--poa-instant", + "true", + "--min-da-gas-price", + "100", + ]; + let rt = tokio::runtime::Runtime::new().unwrap(); + let temp_dir = rt.block_on(async { + let driver = FuelCoreDriver::spawn(&args).await.unwrap(); + driver.client.produce_blocks(100, None).await.unwrap(); + tokio::time::sleep(Duration::from_millis(100)).await; + driver.kill().await + }); + + // rollback 50 blocks + let temp_dir = rt.block_on(async { + let driver = FuelCoreDriver::spawn_with_directory(temp_dir, &args) + .await + .unwrap(); + for _ in 0..50 { + driver + .node + .shared + .database + .gas_price() + .rollback_last_block() + .unwrap(); + let gas_price_db_height = driver + .node + .shared + .database + .gas_price() + .latest_height() + .unwrap(); + tracing::info!("gas price db height: {:?}", gas_price_db_height); + } + driver.kill().await + }); + + // when + // restart node + rt.block_on(async { + let driver = FuelCoreDriver::spawn_with_directory(temp_dir, &args) + .await + .unwrap(); + let onchain_db_height = driver + .node + .shared + .database + .on_chain() + .latest_height_from_metadata() + .unwrap() + .unwrap(); + let gas_price_db_height = driver + .node + .shared + .database + .gas_price() + .latest_height() + .unwrap(); + assert_eq!(onchain_db_height, gas_price_db_height); + }); +} + +fn node_config_with_da_committer_url(url: url::Url) -> Config { + let block_gas_limit = 3_000_000; + let chain_config = ChainConfig { + consensus_parameters: ConsensusParameters::V1(ConsensusParametersV1 { + block_gas_limit, + ..Default::default() + }), + ..ChainConfig::local_testnet() + }; + let mut node_config = + Config::local_node_with_configs(chain_config, StateConfig::local_testnet()); + 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); + node_config.da_poll_interval = Some(Duration::from_millis(100)); + node_config.da_gas_price_p_component = 123_456; + node_config.da_gas_price_d_component = 1_234_567; + node_config.block_activity_threshold = 0; + node_config +} + +#[test] +fn produce_block__algorithm_recovers_from_divergent_profit() { + let mut rng = rand::rngs::StdRng::seed_from_u64(2322u64); + + // given + let mut mock = FakeServer::new(); + let url = mock.url(); + let rt = tokio::runtime::Runtime::new().unwrap(); + let node_config = node_config_with_da_committer_url(url); + let block_delay = 110; + + let (srv, client) = rt.block_on(async { + let srv = FuelService::new_node(node_config.clone()).await.unwrap(); + let client = FuelClient::from(srv.bound_address); + + for _b in 0..block_delay { + produce_a_block(&client, &mut rng).await; + } + let _ = client.produce_blocks(1, None).await.unwrap(); + + let height = srv.shared.database.gas_price().latest_height().unwrap(); + let metadata = srv + .shared + .database + .gas_price() + .get_metadata(&height) + .unwrap() + .and_then(|x| x.v1().cloned()) + .unwrap(); + tracing::info!("metadata: {:?}", metadata); + assert_ne!(metadata.last_profit, 0); + (srv, client) + }); + + let half_of_blocks = block_delay as u32 / 2; + let count = half_of_blocks; + let block_bytes = 1000; + let total_size_bytes = block_bytes * count as u32; + let gas = 16 * total_size_bytes as u128; + let cost_gwei = gas * 1; // blob gas price 1 gwei + let cost = cost_gwei * 1_000_000_000; // Wei + mock.add_response(RawDaBlockCosts { + id: 1, + start_height: 1, + end_height: half_of_blocks, + da_block_height: DaBlockHeight(100), + cost, + size: total_size_bytes, + }); + + let mut profits = Vec::new(); + let mut gas_prices = Vec::new(); + rt.block_on(async { + tokio::time::sleep(Duration::from_millis(200)).await; + client.produce_blocks(1, None).await.unwrap(); + client.produce_blocks(1, None).await.unwrap(); + let height = srv.shared.database.gas_price().latest_height().unwrap(); + let metadata = srv + .shared + .database + .gas_price() + .get_metadata(&height) + .unwrap() + .and_then(|x| x.v1().cloned()) + .unwrap(); + tracing::info!("metadata: {:?}", metadata); + profits.push(metadata.last_profit); + gas_prices.push(metadata.new_scaled_da_gas_price / metadata.gas_price_factor); + }); + + let tries = 1000; + + let mut success = false; + let mut success_iteration = i32::MAX; + rt.block_on(async { + for i in 0..tries { + produce_a_block(&client, &mut rng).await; + let metadata = srv + .shared + .database + .gas_price() + .get_metadata(&srv.shared.database.gas_price().latest_height().unwrap()) + .unwrap() + .and_then(|x| x.v1().cloned()) + .unwrap(); + let profit = metadata.last_profit; + tracing::info!("metadata: {:?}", metadata); + profits.push(profit); + gas_prices.push(metadata.new_scaled_da_gas_price / metadata.gas_price_factor); + if profit > 0 && !success { + success = true; + success_iteration = i as i32; + } + } + }); + let changes = profits.windows(2).map(|x| x[1] - x[0]).collect::>(); + let gas_price_changes = gas_prices + .windows(2) + .map(|x| x[1] as i128 - x[0] as i128) + .collect::>(); + if !success { + panic!( + "Could not recover from divergent profit after {} tries.\n Profits: {:?}.\n Changes: {:?}.\n Gas prices: {:?}\n Gas price changes: {:?}", + tries, profits, changes, gas_prices, gas_price_changes + ); + } +} + +async fn produce_a_block(client: &FuelClient, rng: &mut R) { + let arb_tx_count = 2; + for i in 0..arb_tx_count { + let large_fee_limit = u32::MAX as u64 - i; + let tx = arb_small_tx(large_fee_limit, rng); + let _status = client.submit(&tx).await.unwrap(); + } + let _ = client.produce_blocks(1, None).await.unwrap(); +} + +#[test] +fn produce_block__costs_from_da_are_properly_recorded_in_metadata() { + let mut rng = rand::rngs::StdRng::seed_from_u64(2322u64); + + // given + let mut mock = FakeServer::new(); + let url = mock.url(); + let rt = tokio::runtime::Runtime::new().unwrap(); + let node_config = node_config_with_da_committer_url(url); + let l2_blocks = 1000; + let da_blocks = l2_blocks / 2; + + let (srv, client) = rt.block_on(async { + let srv = FuelService::new_node(node_config.clone()).await.unwrap(); + let client = FuelClient::from(srv.bound_address); + + for _b in 0..l2_blocks { + produce_a_block(&client, &mut rng).await; + } + let _ = client.produce_blocks(1, None).await.unwrap(); + + let height = srv.shared.database.gas_price().latest_height().unwrap(); + let metadata = srv + .shared + .database + .gas_price() + .get_metadata(&height) + .unwrap() + .and_then(|x| x.v1().cloned()) + .unwrap(); + tracing::info!("metadata: {:?}", metadata); + assert_eq!(metadata.latest_known_total_da_cost_excess, 0); + (srv, client) + }); + + // Add multiple cost responses that add up to `da_cost` + let blob_count = 5; + let mut total_cost = 0; + for i in 0..blob_count { + let blob_size = da_blocks / blob_count; + let cost = rng.gen_range(10_000_000..100_000_000); + let costs = RawDaBlockCosts { + id: i + 1, + start_height: blob_size * i + 1, + end_height: blob_size * i + blob_size, + da_block_height: DaBlockHeight(999999999 + i as u64), + cost, + size: 100, + }; + total_cost += cost; + mock.add_response(costs); + + rt.block_on(async { + tokio::time::sleep(Duration::from_millis(200)).await; + // when + let _ = client.produce_blocks(1, None).await.unwrap(); + tokio::time::sleep(Duration::from_millis(100)).await; + + let height = srv.shared.database.gas_price().latest_height().unwrap(); + let metadata = srv + .shared + .database + .gas_price() + .get_metadata(&height) + .unwrap() + .and_then(|x| x.v1().cloned()) + .unwrap(); + + // then + assert_eq!(metadata.latest_known_total_da_cost_excess, total_cost); + }); + } +} diff --git a/tests/tests/regenesis.rs b/tests/tests/regenesis.rs index 67e62fda911..3afc3cc6855 100644 --- a/tests/tests/regenesis.rs +++ b/tests/tests/regenesis.rs @@ -448,6 +448,10 @@ async fn test_regenesis_message_proofs_are_preserved() -> anyhow::Result<()> { snapshot_dir.path().to_str().unwrap(), "--native-executor-version", latest_state_transition_version.as_str(), + "--starting-gas-price", + "0", + "--gas-price-change-percent", + "0", ]) .await?; @@ -729,6 +733,16 @@ async fn starting_empty_node_with_overwritten_poa_works() -> anyhow::Result<()> tmp_path.to_str().unwrap(), "--consensus-key", original_secret_key.to_string().as_str(), + "--min-da-gas-price", + "0", + "--da-gas-price-p-component", + "0", + "--da-gas-price-d-component", + "0", + "--starting-gas-price", + "0", + "--gas-price-change-percent", + "0", ], ) .await; diff --git a/version-compatibility/forkless-upgrade/Cargo.toml b/version-compatibility/forkless-upgrade/Cargo.toml index 0792f3c6fc7..ec5c4feb2c2 100644 --- a/version-compatibility/forkless-upgrade/Cargo.toml +++ b/version-compatibility/forkless-upgrade/Cargo.toml @@ -29,6 +29,8 @@ latest-fuel-core-type = { path = "../../crates/types", package = "fuel-core-type "test-helpers", ] } latest-fuel-core-client = { path = "../../crates/client", package = "fuel-core-client" } +latest-fuel-core-gas-price-service = { path = "../../crates/services/gas_price_service", package = "fuel-core-gas-price-service" } +latest-fuel-core-storage = { path = "../../crates/storage", package = "fuel-core-storage" } latest-fuel-core-upgradable-executor = { path = "../../crates/services/upgradable-executor", package = "fuel-core-upgradable-executor", features = [ "wasm-executor", ] } @@ -48,3 +50,5 @@ version-36-fuel-core-bin = { version = "0.36.0", package = "fuel-core-bin", feat ] } version-36-fuel-core-client = { version = "0.36.0", package = "fuel-core-client" } version-36-fuel-core-services = { version = "0.36.0", package = "fuel-core-services" } +version-36-fuel-core-gas-price-service = { version = "0.36.0", package = "fuel-core-gas-price-service" } +version-36-fuel-core-storage = { version = "0.36.0", package = "fuel-core-storage" } diff --git a/version-compatibility/forkless-upgrade/src/gas_price_algo_compatibility.rs b/version-compatibility/forkless-upgrade/src/gas_price_algo_compatibility.rs new file mode 100644 index 00000000000..6f35cdc78ca --- /dev/null +++ b/version-compatibility/forkless-upgrade/src/gas_price_algo_compatibility.rs @@ -0,0 +1,158 @@ +#![allow(unused_imports)] + +use crate::tests_helper::{ + default_multiaddr, + LatestFuelCoreDriver, + Version36FuelCoreDriver, + IGNITION_TESTNET_SNAPSHOT, + POA_SECRET_KEY, +}; +use latest_fuel_core_gas_price_service::{ + common::{ + fuel_core_storage_adapter::storage::GasPriceMetadata as NewGasPriceMetadata, + updater_metadata::UpdaterMetadata as NewUpdaterMetadata, + }, + ports::GasPriceData as NewGasPriceData, + v1::metadata::V1Metadata, +}; +use latest_fuel_core_storage::{ + transactional::AtomicView as NewAtomicView, + StorageAsRef as NewStorageAsRef, +}; +use libp2p::{ + identity::{ + secp256k1::Keypair as SecpKeypair, + Keypair, + }, + PeerId, +}; +use std::{ + ops::Deref, + time::Duration, +}; +use version_36_fuel_core_gas_price_service::fuel_gas_price_updater::{ + fuel_core_storage_adapter::storage::GasPriceMetadata as OldGasPriceMetadata, + UpdaterMetadata as OldUpdaterMetadata, + V0Metadata, +}; +use version_36_fuel_core_storage::{ + transactional::{ + AtomicView as OldAtomicView, + HistoricalView as OldHistoricalView, + }, + StorageAsRef as OldStorageAsRef, +}; + +#[tokio::test] +async fn v1_gas_price_metadata_updates_successfully_from_v0() { + // Given + let genesis_keypair = SecpKeypair::generate(); + let hexed_secret = hex::encode(genesis_keypair.secret().to_bytes()); + let genesis_port = "30333"; + let starting_gas_price = 987; + let old_driver = Version36FuelCoreDriver::spawn(&[ + "--service-name", + "GenesisProducer", + "--debug", + "--poa-instant", + "true", + "--consensus-key", + POA_SECRET_KEY, + "--snapshot", + IGNITION_TESTNET_SNAPSHOT, + "--enable-p2p", + "--keypair", + hexed_secret.as_str(), + "--peering-port", + genesis_port, + "--starting-gas-price", + starting_gas_price.to_string().as_str(), + ]) + .await + .unwrap(); + + old_driver + .client + .produce_blocks(BLOCKS_TO_PRODUCE, None) + .await + .unwrap(); + + tokio::time::sleep(Duration::from_secs(1)).await; + + let db = &old_driver.node.shared.database; + let latest_height = db.gas_price().latest_height().unwrap(); + let view = db.gas_price().latest_view().unwrap(); + let v0_metadata = match OldStorageAsRef::storage::(&view) + .get(&latest_height) + .unwrap() + .unwrap() + .deref() + .clone() + { + OldUpdaterMetadata::V0(v0) => v0, + }; + + let public_key = Keypair::from(genesis_keypair).public(); + let genesis_peer_id = PeerId::from_public_key(&public_key); + let genesis_multiaddr = default_multiaddr(genesis_port, genesis_peer_id); + drop(view); + let temp_dir = old_driver.kill().await; + + // Starting node that uses latest fuel core. + let latest_keypair = SecpKeypair::generate(); + let hexed_secret = hex::encode(latest_keypair.secret().to_bytes()); + let latest_node = LatestFuelCoreDriver::spawn_with_directory( + temp_dir, + &[ + "--service-name", + "LatestValidator", + "--debug", + "--poa-instant", + "false", + "--snapshot", + IGNITION_TESTNET_SNAPSHOT, + "--enable-p2p", + "--keypair", + hexed_secret.as_str(), + "--reserved-nodes", + genesis_multiaddr.as_str(), + "--peering-port", + "0", + ], + ) + .await + .unwrap(); + + // When + const BLOCKS_TO_PRODUCE: u32 = 1; + latest_node + .client + .produce_blocks(BLOCKS_TO_PRODUCE, None) + .await + .unwrap(); + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + + // Then + let db = &latest_node.node.shared.database; + let latest_height = db.gas_price().latest_height().unwrap(); + let view = db.gas_price().latest_view().unwrap(); + let v1_metadata = V1Metadata::try_from( + NewStorageAsRef::storage::(&view) + .get(&latest_height.into()) + .unwrap() + .unwrap() + .deref() + .clone(), + ) + .unwrap(); + + assert_eq!( + v0_metadata.l2_block_height + BLOCKS_TO_PRODUCE, + v1_metadata.l2_block_height + ); + // Assert that v1 behaves differently from v0. + assert_ne!( + v0_metadata.new_exec_price, + v1_metadata.new_scaled_exec_price * v1_metadata.gas_price_factor.get() + ); +} diff --git a/version-compatibility/forkless-upgrade/src/genesis.rs b/version-compatibility/forkless-upgrade/src/genesis.rs index 702eaf0e075..6374e069f79 100644 --- a/version-compatibility/forkless-upgrade/src/genesis.rs +++ b/version-compatibility/forkless-upgrade/src/genesis.rs @@ -44,4 +44,4 @@ async fn test__genesis_block__hash() { ) .unwrap() ) -} \ No newline at end of file +} diff --git a/version-compatibility/forkless-upgrade/src/lib.rs b/version-compatibility/forkless-upgrade/src/lib.rs index ae7415e13d0..f2170f0043b 100644 --- a/version-compatibility/forkless-upgrade/src/lib.rs +++ b/version-compatibility/forkless-upgrade/src/lib.rs @@ -5,6 +5,10 @@ mod backward_compatibility; #[cfg(test)] mod forward_compatibility; + +#[cfg(test)] +mod gas_price_algo_compatibility; + #[cfg(test)] mod genesis; #[cfg(test)] diff --git a/version-compatibility/forkless-upgrade/src/tests_helper.rs b/version-compatibility/forkless-upgrade/src/tests_helper.rs index c48a1b512a1..d60e47e53e8 100644 --- a/version-compatibility/forkless-upgrade/src/tests_helper.rs +++ b/version-compatibility/forkless-upgrade/src/tests_helper.rs @@ -52,12 +52,17 @@ macro_rules! define_core_driver { impl $name { pub async fn spawn(extra_args: &[&str]) -> anyhow::Result { - use clap::Parser; use tempfile::tempdir; - - // Generate temp params let db_dir = tempdir()?; + Self::spawn_with_directory(db_dir, extra_args).await + } + pub async fn spawn_with_directory( + db_dir: tempfile::TempDir, + extra_args: &[&str], + ) -> anyhow::Result { + use clap::Parser; + let mut args = vec![ "_IGNORED_", "--db-path", @@ -103,6 +108,16 @@ define_core_driver!( true ); +impl Version36FuelCoreDriver { + pub async fn kill(self) -> tempfile::TempDir { + self.node + .send_stop_signal_and_await_shutdown() + .await + .expect("Failed to stop the node"); + self._db_dir + } +} + define_core_driver!( latest_fuel_core_bin, LatestFuelService, @@ -111,6 +126,16 @@ define_core_driver!( true ); +impl LatestFuelCoreDriver { + pub async fn kill(self) -> tempfile::TempDir { + self.node + .send_stop_signal_and_await_shutdown() + .await + .expect("Failed to stop the node"); + self._db_dir + } +} + pub const IGNITION_TESTNET_SNAPSHOT: &str = "./chain-configurations/ignition"; pub const V36_TESTNET_SNAPSHOT: &str = "./chain-configurations/v36"; pub const POA_SECRET_KEY: &str =