diff --git a/CHANGELOG.md b/CHANGELOG.md index b1ed1379f1a..6a6417dc33b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added - [#1898](https://github.com/FuelLabs/fuel-core/pull/1898): Enforce increasing of the `Executor::VERSION` on each release. +- [#1889](https://github.com/FuelLabs/fuel-core/pull/1889): Add new `FuelGasPriceProvider` ### Changed diff --git a/Cargo.lock b/Cargo.lock index 34e023f56c3..bab4ab7b927 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -920,6 +920,12 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c" +[[package]] +name = "bytemuck" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5" + [[package]] name = "byteorder" version = "1.5.0" @@ -1058,8 +1064,10 @@ checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-targets 0.52.5", ] @@ -1249,6 +1257,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.1" @@ -1405,6 +1419,42 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "core-text" +version = "20.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9d2790b5c08465d49f8dc05c8bcae9fea467855947db39b0f8145c091aaced5" +dependencies = [ + "core-foundation", + "core-graphics", + "foreign-types", + "libc", +] + [[package]] name = "core2" version = "0.4.0" @@ -1701,6 +1751,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "cstr" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68523903c8ae5aacfa32a0d9ae60cadeb764e1da14ee0d26b1f3089f13a54636" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "ct-logs" version = "0.8.0" @@ -2121,6 +2181,15 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -2151,6 +2220,18 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" +[[package]] +name = "dwrote" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439a1c2ba5611ad3ed731280541d36d2e9c4ac5e7fb818a27b604bdc5a6aa65b" +dependencies = [ + "lazy_static", + "libc", + "winapi", + "wio", +] + [[package]] name = "ecdsa" version = "0.16.9" @@ -2741,6 +2822,15 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +[[package]] +name = "fdeflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +dependencies = [ + "simd-adler32", +] + [[package]] name = "ff" version = "0.13.0" @@ -2806,12 +2896,70 @@ dependencies = [ "num-traits", ] +[[package]] +name = "float-ord" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "font-kit" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2845a73bbd781e691ab7c2a028c579727cd254942e8ced57ff73e0eafd60de87" +dependencies = [ + "bitflags 2.5.0", + "byteorder", + "core-foundation", + "core-graphics", + "core-text", + "dirs-next", + "dwrote", + "float-ord", + "freetype-sys", + "lazy_static", + "libc", + "log", + "pathfinder_geometry", + "pathfinder_simd", + "walkdir", + "winapi", + "yeslogic-fontconfig-sys", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -2827,6 +2975,17 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" +[[package]] +name = "freetype-sys" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7edc5b9669349acfda99533e9e0bcf26a51862ab43b08ee7745c55d28eb134" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "fs2" version = "0.4.3" @@ -2880,6 +3039,7 @@ dependencies = [ "fuel-core-types", "fuel-core-upgradable-executor", "futures", + "gas-price-algorithm", "hex", "hyper", "indicatif", @@ -3748,6 +3908,15 @@ dependencies = [ "byteorder", ] +[[package]] +name = "gas-price-algorithm" +version = "0.26.0" +dependencies = [ + "plotters", + "rand", + "rand_distr", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -3780,6 +3949,16 @@ dependencies = [ "polyval", ] +[[package]] +name = "gif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.28.1" @@ -4298,6 +4477,20 @@ dependencies = [ "xmltree", ] +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "jpeg-decoder", + "num-traits", + "png", +] + [[package]] name = "impl-codec" version = "0.6.0" @@ -4531,6 +4724,12 @@ dependencies = [ "libc", ] +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" + [[package]] name = "js-sys" version = "0.3.69" @@ -5441,6 +5640,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" dependencies = [ "adler", + "simd-adler32", ] [[package]] @@ -6063,6 +6263,25 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42" +[[package]] +name = "pathfinder_geometry" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3" +dependencies = [ + "log", + "pathfinder_simd", +] + +[[package]] +name = "pathfinder_simd" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebf45976c56919841273f2a0fc684c28437e2f304e264557d9c72be5d5a718be" +dependencies = [ + "rustc_version", +] + [[package]] name = "pbkdf2" version = "0.11.0" @@ -6269,9 +6488,16 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" dependencies = [ + "chrono", + "font-kit", + "image", + "lazy_static", "num-traits", + "pathfinder_geometry", "plotters-backend", + "plotters-bitmap", "plotters-svg", + "ttf-parser", "wasm-bindgen", "web-sys", ] @@ -6282,6 +6508,17 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7" +[[package]] +name = "plotters-bitmap" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7e7f6fb8302456d7c264a94dada86f76d76e1a03e2294ee86ca7da92983b0a6" +dependencies = [ + "gif", + "image", + "plotters-backend", +] + [[package]] name = "plotters-svg" version = "0.3.6" @@ -6291,6 +6528,19 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "png" +version = "0.17.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "2.8.0" @@ -6807,6 +7057,16 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand", +] + [[package]] name = "rand_xorshift" version = "0.3.0" @@ -7742,6 +8002,12 @@ dependencies = [ "rand_core", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "similar" version = "2.5.0" @@ -8740,6 +9006,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "ttf-parser" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" + [[package]] name = "tungstenite" version = "0.20.1" @@ -9301,6 +9573,12 @@ version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + [[package]] name = "widestring" version = "1.1.0" @@ -9533,6 +9811,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wio" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" +dependencies = [ + "winapi", +] + [[package]] name = "ws_stream_wasm" version = "0.7.4" @@ -9659,6 +9946,18 @@ dependencies = [ "time", ] +[[package]] +name = "yeslogic-fontconfig-sys" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb6b23999a8b1a997bf47c7bb4d19ad4029c3327bb3386ebe0a5ff584b33c7a" +dependencies = [ + "cstr", + "dlib", + "once_cell", + "pkg-config", +] + [[package]] name = "zerocopy" version = "0.7.34" diff --git a/Cargo.toml b/Cargo.toml index ac5b57de7ee..2adca1d7c13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "crates/client", "crates/database", "crates/fuel-core", + "crates/gas-price-algorithm", "crates/keygen", "crates/metrics", "crates/services", @@ -79,6 +80,7 @@ fuel-core-tests = { version = "0.0.0", path = "./tests" } fuel-core-upgradable-executor = { version = "0.26.0", path = "./crates/services/upgradable-executor" } fuel-core-wasm-executor = { version = "0.26.0", path = "./crates/services/upgradable-executor/wasm-executor", default-features = false } fuel-core-xtask = { version = "0.0.0", path = "./xtask" } +gas-price-algorithm = { path = "crates/gas-price-algorithm" } # Fuel dependencies fuel-vm-private = { version = "0.50.0", package = "fuel-vm", default-features = false } diff --git a/bin/fuel-core/chainspec/local-testnet/state_transition_bytecode.wasm b/bin/fuel-core/chainspec/local-testnet/state_transition_bytecode.wasm index 450d7f7d2b0..09f091e8318 100755 Binary files a/bin/fuel-core/chainspec/local-testnet/state_transition_bytecode.wasm and b/bin/fuel-core/chainspec/local-testnet/state_transition_bytecode.wasm differ diff --git a/crates/fuel-core/Cargo.toml b/crates/fuel-core/Cargo.toml index 5390c3eac0e..27b2f9f7bef 100644 --- a/crates/fuel-core/Cargo.toml +++ b/crates/fuel-core/Cargo.toml @@ -10,6 +10,10 @@ name = "fuel-core" repository = { workspace = true } version = { workspace = true } +[[bench]] +name = "gas_price_algo" +harness = false + [dependencies] anyhow = { workspace = true } async-graphql = { version = "4.0", features = [ @@ -37,6 +41,7 @@ fuel-core-txpool = { workspace = true } fuel-core-types = { workspace = true, features = ["serde"] } fuel-core-upgradable-executor = { workspace = true } futures = { workspace = true } +gas-price-algorithm = { workspace = true } hex = { version = "0.4", features = ["serde"] } hyper = { workspace = true } indicatif = { workspace = true, default-features = true } diff --git a/crates/fuel-core/benches/gas_price_algo.rs b/crates/fuel-core/benches/gas_price_algo.rs new file mode 100644 index 00000000000..8eeca562e7c --- /dev/null +++ b/crates/fuel-core/benches/gas_price_algo.rs @@ -0,0 +1,10 @@ +use criterion::{ + criterion_group, + criterion_main, + Criterion, +}; +// TODO: Move from `gas-price-algorithm` +fn gas_price_algo(_c: &mut Criterion) {} + +criterion_group!(benches, gas_price_algo); +criterion_main!(benches); diff --git a/crates/fuel-core/src/service/adapters.rs b/crates/fuel-core/src/service/adapters.rs index bfe9db100fa..9b919d8d8b5 100644 --- a/crates/fuel-core/src/service/adapters.rs +++ b/crates/fuel-core/src/service/adapters.rs @@ -36,6 +36,8 @@ pub mod relayer; pub mod sync; pub mod txpool; +pub mod fuel_gas_price_provider; + #[derive(Debug, Clone)] pub struct ConsensusParametersProvider { shared_state: consensus_parameters_provider::SharedState, 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 new file mode 100644 index 00000000000..f27735916c7 --- /dev/null +++ b/crates/fuel-core/src/service/adapters/fuel_gas_price_provider.rs @@ -0,0 +1,225 @@ +use crate::service::adapters::fuel_gas_price_provider::ports::{ + GasPriceAlgorithm, + GasPriceHistory, + GasPrices, +}; +use fuel_core_producer::block_producer::gas_price::{ + GasPriceParams, + GasPriceProvider, +}; +use fuel_core_types::fuel_types::BlockHeight; +use ports::{ + DARecordingCostHistory, + FuelBlockHistory, +}; +use std::cell::RefCell; + +pub mod ports; + +pub mod algorithm_adapter; + +use ports::{ + Error, + Result, +}; + +#[cfg(test)] +mod tests; + +/// Gives the gas price for a given block height, and calculates the gas price if not yet committed. +pub struct FuelGasPriceProvider { + profitablility_totals: ProfitablilityTotals, + + block_history: FB, + da_recording_cost_history: DA, + algorithm: A, + gas_price_history: GP, +} + +impl FuelGasPriceProvider { + pub fn new( + block_history: FB, + da_recording_cost_history: DA, + algorithm: A, + gas_price_history: GP, + ) -> Self { + Self { + profitablility_totals: ProfitablilityTotals::default(), + block_history, + da_recording_cost_history, + algorithm, + gas_price_history, + } + } +} + +struct ProfitablilityTotals { + totaled_block_height: RefCell, + total_reward: RefCell, + total_cost: RefCell, +} + +impl Default for ProfitablilityTotals { + fn default() -> Self { + Self { + totaled_block_height: RefCell::new(0.into()), + total_reward: RefCell::new(0), + total_cost: RefCell::new(0), + } + } +} + +impl ProfitablilityTotals { + #[allow(dead_code)] + pub fn new(block_height: BlockHeight, reward: u64, cost: u64) -> Self { + Self { + totaled_block_height: RefCell::new(block_height), + total_reward: RefCell::new(reward), + total_cost: RefCell::new(cost), + } + } + + fn update(&self, block_height: BlockHeight, reward: u64, cost: u64) { + let mut totaled_block_height = self.totaled_block_height.borrow_mut(); + let mut total_reward = self.total_reward.borrow_mut(); + let mut total_cost = self.total_cost.borrow_mut(); + *totaled_block_height = block_height; + *total_reward += reward; + *total_cost += cost; + } + + fn totaled_block_height(&self) -> BlockHeight { + *self.totaled_block_height.borrow() + } +} + +impl FuelGasPriceProvider +where + FB: FuelBlockHistory, + DA: DARecordingCostHistory, + A: GasPriceAlgorithm, + GP: GasPriceHistory, +{ + fn inner_gas_price(&self, requested_block_height: BlockHeight) -> Result { + let latest_block = self + .block_history + .latest_height() + .map_err(Error::UnableToGetLatestBlockHeight)?; + if latest_block > requested_block_height { + let gas_prices = self + .gas_price_history + .gas_prices(requested_block_height) + .map_err(Error::UnableToGetGasPrices)? + .ok_or(Error::GasPricesNotFoundForBlockHeight( + requested_block_height, + ))?; + Ok(gas_prices.total()) + } else if Self::asking_for_next_block(latest_block, requested_block_height) { + let new_gas_prices = self.calculate_new_gas_price(latest_block)?; + self.gas_price_history + .store_gas_prices(requested_block_height, &new_gas_prices) + .map_err(Error::UnableToStoreGasPrices)?; + Ok(new_gas_prices.total()) + } else { + Err(Error::RequestedBlockHeightTooHigh { + requested: requested_block_height, + latest: latest_block, + }) + } + } + + fn asking_for_next_block( + latest_block: BlockHeight, + block_height: BlockHeight, + ) -> bool { + *latest_block + 1 == *block_height + } + + fn calculate_new_gas_price( + &self, + latest_block_height: BlockHeight, + ) -> Result { + let previous_gas_prices = self + .gas_price_history + .gas_prices(latest_block_height) + .map_err(Error::UnableToGetGasPrices)? + .ok_or(Error::GasPricesNotFoundForBlockHeight(latest_block_height))?; + let latest_da_recorded_block = self + .da_recording_cost_history + .latest_height() + .map_err(Error::UnableToGetLatestBlockHeight)?; + + let new_da_gas_price = if latest_da_recorded_block + <= self.profitablility_totals.totaled_block_height() + { + previous_gas_prices.da_gas_price + } else { + self.update_totals(latest_block_height)?; + self.algorithm.calculate_da_gas_price( + previous_gas_prices.da_gas_price, + self.total_reward(), + self.total_cost(), + ) + }; + + let block_fullness = self + .block_history + .block_fullness(latest_block_height) + .map_err(Error::UnableToGetBlockFullness)? + .ok_or(Error::BlockFullnessNotFoundForBlockHeight( + latest_block_height, + ))?; + + let new_exec_gas_prices = self.algorithm.calculate_execution_gas_price( + previous_gas_prices.execution_gas_price, + block_fullness, + ); + + let gas_prices = GasPrices::new(new_exec_gas_prices, new_da_gas_price); + Ok(gas_prices) + } + + fn total_reward(&self) -> u64 { + *self.profitablility_totals.total_reward.borrow() + } + + fn total_cost(&self) -> u64 { + *self.profitablility_totals.total_cost.borrow() + } + + fn totaled_block_height(&self) -> BlockHeight { + *self.profitablility_totals.totaled_block_height.borrow() + } + + fn update_totals(&self, latest_block_height: BlockHeight) -> Result<()> { + while self.totaled_block_height() < latest_block_height { + let block_height = (*self.totaled_block_height() + 1).into(); + let reward = self + .block_history + .production_reward(block_height) + .map_err(Error::UnableToGetProductionReward)? + .ok_or(Error::ProductionRewardNotFoundForBlockHeight(block_height))?; + let cost = self + .da_recording_cost_history + .recording_cost(block_height) + .map_err(Error::UnableToGetRecordingCost)? + .ok_or(Error::RecordingCostNotFoundForBlockHeight(block_height))?; + self.profitablility_totals + .update(block_height, reward, cost); + } + Ok(()) + } +} + +impl GasPriceProvider for FuelGasPriceProvider +where + FB: FuelBlockHistory, + DA: DARecordingCostHistory, + A: GasPriceAlgorithm, + GP: GasPriceHistory, +{ + fn gas_price(&self, params: GasPriceParams) -> anyhow::Result { + self.inner_gas_price(params.block_height()) + .map_err(|e| anyhow::anyhow!("Failed to query gas price: {e:?}")) + } +} diff --git a/crates/fuel-core/src/service/adapters/fuel_gas_price_provider/algorithm_adapter.rs b/crates/fuel-core/src/service/adapters/fuel_gas_price_provider/algorithm_adapter.rs new file mode 100644 index 00000000000..e6e625f52d1 --- /dev/null +++ b/crates/fuel-core/src/service/adapters/fuel_gas_price_provider/algorithm_adapter.rs @@ -0,0 +1,184 @@ +use crate::service::adapters::fuel_gas_price_provider::ports::{ + BlockFullness, + GasPriceAlgorithm, + GasPrices, +}; +use gas_price_algorithm::AlgorithmV1; + +pub enum FuelGasPriceAlgorithm { + V1(AlgorithmV1), +} + +impl FuelGasPriceAlgorithm { + pub fn new(_target_profitability: f32, _max_price_change_percentage: f32) -> Self { + todo!() + } +} + +impl GasPriceAlgorithm for FuelGasPriceAlgorithm { + fn calculate_da_gas_price( + &self, + previous_da_gas_prices: u64, + total_production_reward: u64, + total_da_recording_cost: u64, + ) -> u64 { + match self { + FuelGasPriceAlgorithm::V1(v1) => v1.calculate_da_gas_price( + previous_da_gas_prices, + total_production_reward, + total_da_recording_cost, + ), + } + } + + fn calculate_execution_gas_price( + &self, + previous_execution_gas_price: u64, + block_fullness: BlockFullness, + ) -> u64 { + match self { + FuelGasPriceAlgorithm::V1(v1) => v1.calculate_exec_gas_price( + previous_execution_gas_price, + block_fullness.used(), + block_fullness.capacity(), + ), + } + } + + fn maximum_next_gas_prices(&self, _previous_gas_price: GasPrices) -> GasPrices { + todo!() + } +} + +// TODO: These tests need to be updated. Also the specific behavior should probably be managed by +// the specific algorithm _version_ we are using. +#[cfg(test)] +mod tests { + #![allow(non_snake_case)] + use super::*; + + #[test] + fn calculate_gas_price__above_50_percent_increases_gas_price() { + // given + let old_da_gas_price = 444; + let old_exec_gas_price = 333; + let total_production_reward = 100; + let total_da_recording_cost = total_production_reward; + let min_da_price = 10; + let min_exec_price = 10; + let p_value_factor = 4_000; + let d_value_factor = 100; + let moving_average_window = 10; + let max_change_percent = 15; + let exec_change_amount = 10; + // 60% full + let block_fullness = BlockFullness::new(60, 100); + let v1 = AlgorithmV1::new( + min_da_price, + min_exec_price, + p_value_factor, + d_value_factor, + moving_average_window, + max_change_percent, + exec_change_amount, + ); + let algo = FuelGasPriceAlgorithm::V1(v1); + // when + let new_da_gas_price = algo.calculate_da_gas_price( + old_da_gas_price, + total_production_reward, + total_da_recording_cost, + ); + let new_exec_gas_price = + algo.calculate_execution_gas_price(old_exec_gas_price, block_fullness); + let new_gas_price = new_exec_gas_price + new_da_gas_price; + let old_gas_price = old_exec_gas_price + old_da_gas_price; + + // then + assert!(new_gas_price > old_gas_price); + } + + #[test] + fn calculate_gas_price__below_50_but_not_profitable_increase_gas_price() { + // given + let old_da_gas_price = 444; + let old_exec_gas_price = 333; + let total_production_reward = 100; + let total_da_recording_cost = total_production_reward + 1; + let min_da_price = 10; + let min_exec_price = 10; + let p_value_factor = 4_000; + let d_value_factor = 100; + let moving_average_window = 10; + let max_change_percent = 15; + let exec_change_amount = 10; + // 40% full + let block_fullness = BlockFullness::new(40, 100); + let v1 = AlgorithmV1::new( + min_da_price, + min_exec_price, + p_value_factor, + d_value_factor, + moving_average_window, + max_change_percent, + exec_change_amount, + ); + let algo = FuelGasPriceAlgorithm::V1(v1); + + // when + let new_da_gas_price = algo.calculate_da_gas_price( + old_da_gas_price, + total_production_reward, + total_da_recording_cost, + ); + let new_exec_gas_price = + algo.calculate_execution_gas_price(old_exec_gas_price, block_fullness); + let new_gas_price = new_exec_gas_price + new_da_gas_price; + let old_gas_price = old_exec_gas_price + old_da_gas_price; + + // then + assert!(new_gas_price > old_gas_price); + } + + #[test] + fn calculate_gas_price__below_50_and_profitable_decrease_gas_price() { + // given + let old_da_gas_price = 444; + let old_exec_gas_price = 333; + let total_production_reward = 100; + let total_da_recording_cost = total_production_reward - 1; + let min_da_price = 10; + let min_exec_price = 10; + let p_value_factor = 4_000; + let d_value_factor = 100; + let moving_average_window = 10; + let max_change_percent = 15; + let exec_change_amount = 10; + // 40% full + let block_fullness = BlockFullness::new(40, 100); + let v1 = AlgorithmV1::new( + min_da_price, + min_exec_price, + p_value_factor, + d_value_factor, + moving_average_window, + max_change_percent, + exec_change_amount, + ); + let algo = FuelGasPriceAlgorithm::V1(v1); + + // when + let new_da_gas_price = algo.calculate_da_gas_price( + old_da_gas_price, + total_production_reward, + total_da_recording_cost, + ); + let new_exec_gas_price = + algo.calculate_execution_gas_price(old_exec_gas_price, block_fullness); + let new_gas_price = new_exec_gas_price + new_da_gas_price; + let old_gas_price = old_exec_gas_price + old_da_gas_price; + + // then + assert!(new_gas_price < old_gas_price); + } +} diff --git a/crates/fuel-core/src/service/adapters/fuel_gas_price_provider/ports.rs b/crates/fuel-core/src/service/adapters/fuel_gas_price_provider/ports.rs new file mode 100644 index 00000000000..be75da9b2b5 --- /dev/null +++ b/crates/fuel-core/src/service/adapters/fuel_gas_price_provider/ports.rs @@ -0,0 +1,126 @@ +use fuel_core_types::fuel_types::BlockHeight; +pub type Result = std::result::Result; + +pub type ForeignResult = std::result::Result; + +type ForeignError = Box; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Requested block ({requested}) is too high, latest block is {latest}")] + RequestedBlockHeightTooHigh { + requested: BlockHeight, + latest: BlockHeight, + }, + #[error("Unable to get latest block height: {0:?}")] + UnableToGetLatestBlockHeight(ForeignError), + #[error("Unable to get execution gas price: {0:?}")] + UnableToGetGasPrices(ForeignError), + #[error("Unable to store gas prices: {0:?}")] + UnableToStoreGasPrices(ForeignError), + #[error("Execution gas price not found for block height: {0:?}")] + GasPricesNotFoundForBlockHeight(BlockHeight), + #[error("Unable to get block fullness: {0:?}")] + UnableToGetBlockFullness(ForeignError), + #[error("Block fullness not found for block height: {0:?}")] + BlockFullnessNotFoundForBlockHeight(BlockHeight), + #[error("Unable to get production reward: {0:?}")] + UnableToGetProductionReward(ForeignError), + #[error("Production reward not found for block height: {0:?}")] + ProductionRewardNotFoundForBlockHeight(BlockHeight), + #[error("Unable to get recording cost: {0:?}")] + UnableToGetRecordingCost(ForeignError), + #[error("Recording cost not found for block height: {0:?}")] + RecordingCostNotFoundForBlockHeight(BlockHeight), + #[error("Could not convert block usage to percentage: {0}")] + CouldNotConvertBlockUsageToPercentage(String), +} + +#[derive(Debug, Clone, Copy)] +pub struct BlockFullness { + used: u64, + capacity: u64, +} + +impl BlockFullness { + pub fn new(used: u64, capacity: u64) -> Self { + Self { used, capacity } + } + + pub fn used(&self) -> u64 { + self.used + } + + pub fn capacity(&self) -> u64 { + self.capacity + } +} + +pub trait FuelBlockHistory { + fn latest_height(&self) -> ForeignResult; + + fn block_fullness(&self, height: BlockHeight) + -> ForeignResult>; + + fn production_reward(&self, height: BlockHeight) -> ForeignResult>; +} + +pub trait DARecordingCostHistory { + fn latest_height(&self) -> ForeignResult; + fn recording_cost(&self, height: BlockHeight) -> ForeignResult>; +} + +#[derive(Debug, Clone, Copy)] +pub struct GasPrices { + pub execution_gas_price: u64, + pub da_gas_price: u64, +} + +impl GasPrices { + pub fn new(execution_gas_price: u64, da_gas_price: u64) -> Self { + Self { + execution_gas_price, + da_gas_price, + } + } + + pub fn execution_gas_price(&self) -> u64 { + self.execution_gas_price + } + + pub fn da_gas_price(&self) -> u64 { + self.da_gas_price + } + pub fn total(&self) -> u64 { + self.execution_gas_price + self.da_gas_price + } +} + +pub trait GasPriceHistory { + fn gas_prices(&self, height: BlockHeight) -> ForeignResult>; + // TODO: Can we make this mutable? The problem is that the `GasPriceProvider` isn't mut, so + // we can't hold mutable references. Might be able to make `GasPriceProvider` `&mut self` but will + // take some finangling + fn store_gas_prices( + &self, + height: BlockHeight, + gas_price: &GasPrices, + ) -> ForeignResult<()>; +} + +pub trait GasPriceAlgorithm { + fn calculate_da_gas_price( + &self, + previous_da_gas_prices: u64, + total_production_reward: u64, + total_da_recording_cost: u64, + ) -> u64; + + fn calculate_execution_gas_price( + &self, + previous_execution_gas_prices: u64, + block_fullness: BlockFullness, + ) -> u64; + + fn maximum_next_gas_prices(&self, previous_gas_prices: GasPrices) -> GasPrices; +} diff --git a/crates/fuel-core/src/service/adapters/fuel_gas_price_provider/tests.rs b/crates/fuel-core/src/service/adapters/fuel_gas_price_provider/tests.rs new file mode 100644 index 00000000000..6a45eb5b409 --- /dev/null +++ b/crates/fuel-core/src/service/adapters/fuel_gas_price_provider/tests.rs @@ -0,0 +1,256 @@ +#![allow(non_snake_case)] + +use super::*; +use crate::service::adapters::{ + fuel_gas_price_provider::ports::{ + BlockFullness, + ForeignResult, + }, + BlockHeight, +}; +use std::collections::HashMap; + +#[cfg(test)] +mod producer_gas_price_tests; + +struct FakeBlockHistory { + latest_height: BlockHeight, + production_rewards: HashMap, + block_fullness: HashMap, +} + +impl FuelBlockHistory for FakeBlockHistory { + fn latest_height(&self) -> ForeignResult { + Ok(self.latest_height) + } + + fn block_fullness( + &self, + height: BlockHeight, + ) -> ForeignResult> { + Ok(self.block_fullness.get(&height).copied()) + } + + fn production_reward(&self, height: BlockHeight) -> ForeignResult> { + Ok(self.production_rewards.get(&height).copied()) + } +} + +struct FakeGasPriceHistory { + gas_prices: RefCell>, +} + +impl GasPriceHistory for FakeGasPriceHistory { + fn gas_prices(&self, height: BlockHeight) -> ForeignResult> { + Ok(self.gas_prices.borrow().get(&height).copied()) + } + + fn store_gas_prices( + &self, + height: BlockHeight, + gas_price: &GasPrices, + ) -> ForeignResult<()> { + self.gas_prices + .borrow_mut() + .insert(height, gas_price.clone()); + Ok(()) + } +} + +struct FakeDARecordingCostHistory { + costs: HashMap, +} + +impl DARecordingCostHistory for FakeDARecordingCostHistory { + fn latest_height(&self) -> ForeignResult { + let max_height = self.costs.iter().map(|(h, _)| *h).max().unwrap_or(0.into()); + Ok(max_height) + } + + fn recording_cost(&self, height: BlockHeight) -> ForeignResult> { + Ok(self.costs.get(&height).copied()) + } +} + +pub struct SimpleGasPriceAlgorithm { + flat_price_change: u64, + max_price_change: u64, +} + +impl Default for SimpleGasPriceAlgorithm { + fn default() -> Self { + Self { + flat_price_change: 1, + max_price_change: u64::MAX, + } + } +} + +impl GasPriceAlgorithm for SimpleGasPriceAlgorithm { + fn calculate_da_gas_price( + &self, + previous_da_gas_price: u64, + total_production_reward: u64, + total_da_recording_cost: u64, + ) -> u64 { + if total_production_reward < total_da_recording_cost { + previous_da_gas_price.saturating_add(self.flat_price_change) + } else { + previous_da_gas_price + } + } + + fn calculate_execution_gas_price( + &self, + previous_execution_gas_prices: u64, + _block_fullness: BlockFullness, + ) -> u64 { + previous_execution_gas_prices + } + + fn maximum_next_gas_prices(&self, previous_gas_price: GasPrices) -> GasPrices { + let GasPrices { + execution_gas_price, + da_gas_price, + } = previous_gas_price; + let da_gas_price = da_gas_price.saturating_add(self.max_price_change); + GasPrices { + execution_gas_price, + da_gas_price, + } + } +} + +struct ProviderBuilder { + latest_height: BlockHeight, + historical_gas_price: HashMap, + historical_production_rewards: HashMap, + historical_block_fullness: HashMap, + da_recording_costs: HashMap, + totaled_block_height: BlockHeight, + total_reward: u64, + total_cost: u64, + algorithm: SimpleGasPriceAlgorithm, +} + +impl ProviderBuilder { + fn new() -> Self { + Self { + latest_height: 0.into(), + historical_gas_price: HashMap::new(), + historical_production_rewards: HashMap::new(), + historical_block_fullness: HashMap::new(), + da_recording_costs: HashMap::new(), + totaled_block_height: 0.into(), + total_reward: 0, + total_cost: 0, + algorithm: SimpleGasPriceAlgorithm { + flat_price_change: 1, + max_price_change: u64::MAX, + }, + } + } + + fn with_latest_height(mut self, latest_height: BlockHeight) -> Self { + self.latest_height = latest_height; + self + } + + fn with_historical_gas_price( + mut self, + block_height: BlockHeight, + gas_prices: GasPrices, + ) -> Self { + self.historical_gas_price.insert(block_height, gas_prices); + self + } + + fn with_historical_production_reward( + mut self, + block_height: BlockHeight, + reward: u64, + ) -> Self { + self.historical_production_rewards + .insert(block_height, reward); + self + } + + fn with_historical_da_recording_cost( + mut self, + block_height: BlockHeight, + cost: u64, + ) -> Self { + self.da_recording_costs.insert(block_height, cost); + self + } + + fn with_historical_block_fullness( + mut self, + block_height: BlockHeight, + fullness: BlockFullness, + ) -> Self { + self.historical_block_fullness + .insert(block_height, fullness); + self + } + + fn with_total_as_of_block( + mut self, + block_height: BlockHeight, + reward: u64, + cost: u64, + ) -> Self { + self.totaled_block_height = block_height; + self.total_reward = reward; + self.total_cost = cost; + self + } + + fn build( + self, + ) -> FuelGasPriceProvider< + FakeBlockHistory, + FakeDARecordingCostHistory, + SimpleGasPriceAlgorithm, + FakeGasPriceHistory, + > { + let Self { + latest_height, + historical_gas_price, + historical_production_rewards, + historical_block_fullness, + da_recording_costs, + totaled_block_height, + total_reward, + total_cost, + algorithm, + } = self; + + let block_history = FakeBlockHistory { + latest_height, + production_rewards: historical_production_rewards, + block_fullness: historical_block_fullness, + }; + let da_recording_cost_history = FakeDARecordingCostHistory { + costs: da_recording_costs, + }; + let gas_price_history = FakeGasPriceHistory { + gas_prices: historical_gas_price.into(), + }; + FuelGasPriceProvider { + profitablility_totals: ProfitablilityTotals::new( + totaled_block_height, + total_reward, + total_cost, + ), + block_history, + da_recording_cost_history, + algorithm, + gas_price_history, + } + } +} + +#[ignore] +#[test] +fn dummy() {} diff --git a/crates/fuel-core/src/service/adapters/fuel_gas_price_provider/tests/producer_gas_price_tests.rs b/crates/fuel-core/src/service/adapters/fuel_gas_price_provider/tests/producer_gas_price_tests.rs new file mode 100644 index 00000000000..7376d18c185 --- /dev/null +++ b/crates/fuel-core/src/service/adapters/fuel_gas_price_provider/tests/producer_gas_price_tests.rs @@ -0,0 +1,153 @@ +use super::*; + +#[test] +fn gas_price__can_get_a_historical_gas_price() { + // given + let block_height = 432; + let latest_height = (432 + 2).into(); + let expected_da_gas_price = 123; + let expected_exec_gas_price = 0; + let expected_gas_prices = + GasPrices::new(expected_da_gas_price, expected_exec_gas_price); + let gas_price_provider = ProviderBuilder::new() + .with_latest_height(latest_height) + .with_historical_gas_price(block_height.into(), expected_gas_prices) + .build(); + + // when + let params = GasPriceParams::new(block_height.into()); + let actual = gas_price_provider.gas_price(params).unwrap(); + + // then + assert_eq!(actual, expected_gas_prices.total()); +} + +#[test] +fn gas_price__if_requested_block_height_too_high_return_error() { + // given + let latest_height = 432; + let too_new_height = (latest_height + 2).into(); + let gas_price_provider = ProviderBuilder::new() + .with_latest_height(latest_height.into()) + .build(); + + // when + let params = GasPriceParams::new(too_new_height); + let maybe_price = gas_price_provider.gas_price(params); + + // then + assert!(maybe_price.is_err()); +} + +#[test] +fn gas_price__next_block_calls_algorithm_function() { + // given + let latest_height = 432; + let latest_da_gas_price = 123; + let latest_exec_gas_price = 0; + let latest_gas_prices = GasPrices::new(latest_exec_gas_price, latest_da_gas_price); + let next_height = (latest_height + 1).into(); + let cost = 100; + let reward = cost - 1; + let block_fullness = BlockFullness::new(1, 1); + let gas_price_provider = ProviderBuilder::new() + .with_historical_gas_price(latest_height.into(), latest_gas_prices) + .with_latest_height(latest_height.into()) + .with_historical_block_fullness(latest_height.into(), block_fullness) + .with_historical_da_recording_cost(next_height, cost) + .with_total_as_of_block(latest_height.into(), reward, cost) + .build(); + + // when + let params = GasPriceParams::new(next_height); + let maybe_price = gas_price_provider.gas_price(params); + + // then + let new_da_gas_price = SimpleGasPriceAlgorithm::default().calculate_da_gas_price( + latest_da_gas_price, + reward, + cost, + ); + let new_exec_gas_price = SimpleGasPriceAlgorithm::default() + .calculate_execution_gas_price(latest_exec_gas_price, block_fullness); + let expected = new_da_gas_price + new_exec_gas_price; + let actual = maybe_price.unwrap(); + assert_eq!(actual, expected); +} + +// TODO: Change to prop test, and generalize to simplify readability (use a loop or something) +#[test] +fn gas_price__if_total_is_for_old_block_then_update_to_latest_block() { + // given + let latest_height = 432; + let total_block_height = latest_height - 2; + let latest_da_gas_price = 123; + let latest_exec_gas_price = 0; + let latest_gas_prices = GasPrices::new(latest_exec_gas_price, latest_da_gas_price); + let next_height = (latest_height + 1).into(); + let cost = 100; + let reward = cost - 1; + let block_fullness = BlockFullness::new(1, 1); + let gas_price_provider = ProviderBuilder::new() + .with_historical_gas_price((latest_height - 1).into(), latest_gas_prices) + .with_historical_production_reward((latest_height - 1).into(), reward) + .with_historical_da_recording_cost((latest_height - 1).into(), cost) + .with_historical_gas_price(latest_height.into(), latest_gas_prices) + .with_historical_production_reward(latest_height.into(), reward) + .with_historical_da_recording_cost(latest_height.into(), cost) + .with_latest_height(latest_height.into()) + .with_historical_block_fullness(latest_height.into(), block_fullness) + .with_total_as_of_block(total_block_height.into(), reward, cost) + .build(); + + // when + let params = GasPriceParams::new(next_height); + let maybe_price = gas_price_provider.gas_price(params); + + // then + let new_da_gas_price = SimpleGasPriceAlgorithm::default().calculate_da_gas_price( + latest_da_gas_price, + reward, + cost, + ); + let new_exec_gas_price = SimpleGasPriceAlgorithm::default() + .calculate_execution_gas_price(latest_exec_gas_price, block_fullness); + let expected = new_da_gas_price + new_exec_gas_price; + let actual = maybe_price.unwrap(); + assert_eq!(actual, expected); +} + +#[test] +fn gas_price__if_da_behind_fuel_block_then_do_not_update_da_gas_price() { + // given + let latest_fuel_height = 432; + let arb_previous_height_diff = 12; + let latest_da_height = latest_fuel_height - arb_previous_height_diff; + let latest_da_gas_price = 123; + let latest_exec_gas_price = 321; + let latest_gas_prices = GasPrices::new(latest_exec_gas_price, latest_da_gas_price); + let next_height = (latest_fuel_height + 1).into(); + let cost = 100; + let reward = cost - 1; + let block_fullness = BlockFullness::new(3, 4); + let gas_price_provider = ProviderBuilder::new() + .with_historical_gas_price(latest_fuel_height.into(), latest_gas_prices) + .with_latest_height(latest_fuel_height.into()) + .with_historical_block_fullness(latest_fuel_height.into(), block_fullness) + .with_historical_da_recording_cost(latest_da_height.into(), cost) + .with_total_as_of_block(latest_da_height.into(), reward, cost) + .build(); + + // when + let params = GasPriceParams::new(next_height); + let actual = gas_price_provider.gas_price(params).unwrap(); + + // then + let algo = SimpleGasPriceAlgorithm::default(); + let new_da_gas_price = latest_da_gas_price; + let new_exec_gas_price = + algo.calculate_execution_gas_price(latest_exec_gas_price, block_fullness); + let expected = new_da_gas_price + new_exec_gas_price; + + assert_eq!(actual, expected); +} diff --git a/crates/fuel-core/src/service/adapters/producer.rs b/crates/fuel-core/src/service/adapters/producer.rs index 8cda90780a6..a8f2ef9b12f 100644 --- a/crates/fuel-core/src/service/adapters/producer.rs +++ b/crates/fuel-core/src/service/adapters/producer.rs @@ -225,8 +225,8 @@ impl fuel_core_producer::ports::BlockProducerDatabase for Database { } impl GasPriceProvider for StaticGasPrice { - fn gas_price(&self, _block_height: GasPriceParams) -> Option { - Some(self.gas_price) + fn gas_price(&self, _block_height: GasPriceParams) -> anyhow::Result { + Ok(self.gas_price) } } diff --git a/crates/gas-price-algorithm/Cargo.toml b/crates/gas-price-algorithm/Cargo.toml new file mode 100644 index 00000000000..7aaa9a33a35 --- /dev/null +++ b/crates/gas-price-algorithm/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "gas-price-algorithm" +authors = { workspace = true } +categories = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +keywords = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +version = { workspace = true } + +[lib] +name = "gas_price_algorithm" +path = "src/lib.rs" + +[[bin]] +name = "main" +path = "src/main.rs" + +[dependencies] +plotters = "0.3.5" +rand = "0.8.5" +rand_distr = "0.4.3" diff --git a/crates/gas-price-algorithm/src/lib.rs b/crates/gas-price-algorithm/src/lib.rs new file mode 100644 index 00000000000..211a9159250 --- /dev/null +++ b/crates/gas-price-algorithm/src/lib.rs @@ -0,0 +1,110 @@ +use std::{ + cell::RefCell, + cmp::{ + max, + min, + }, +}; + +pub struct AlgorithmV1 { + // DA + da_max_change_percent: u8, + min_da_price: u64, + da_p_value_factor: i64, + da_d_value_factor: i64, + moving_average_profit: RefCell, + last_profit: RefCell, + profit_slope: RefCell, + moving_average_window: i64, + // EXEC + exec_change_amount: u64, + min_exec_price: u64, +} + +impl AlgorithmV1 { + pub fn new( + da_max_change_percent: u8, + min_da_price: u64, + da_p_value_factor: i64, + da_d_value_factor: i64, + moving_average_window: i64, + exec_change_amount: u64, + min_exec_price: u64, + ) -> Self { + Self { + da_max_change_percent, + min_da_price, + da_p_value_factor, + da_d_value_factor, + moving_average_profit: RefCell::new(0), + last_profit: RefCell::new(0), + profit_slope: RefCell::new(0), + moving_average_window, + exec_change_amount, + min_exec_price, + } + } + + pub fn calculate_da_gas_price( + &self, + old_da_gas_price: u64, + total_production_reward: u64, + total_da_recording_cost: u64, + ) -> u64 { + let new_profit = total_production_reward as i64 - total_da_recording_cost as i64; + self.calculate_new_moving_average(new_profit); + self.calculate_profit_slope(*self.moving_average_profit.borrow()); + let avg_profit = *self.moving_average_profit.borrow(); + let slope = *self.profit_slope.borrow(); + + let max_change = (old_da_gas_price + .saturating_mul(self.da_max_change_percent as u64) + / 100) as i64; + + // if p > 0 and dp/db > 0, decrease + // if p > 0 and dp/db < 0, hold/moderate + // if p < 0 and dp/db < 0, increase + // if p < 0 and dp/db > 0, hold/moderate + let p_comp = avg_profit / self.da_p_value_factor; + let d_comp = slope / self.da_d_value_factor; + let pd_change = p_comp + d_comp; + let change = min(max_change, pd_change.abs()); + let sign = pd_change.signum(); + let change = change * sign; + let new_da_gas_price = old_da_gas_price as i64 - change; + max(new_da_gas_price, self.min_da_price as i64) as u64 + } + + pub fn calculate_exec_gas_price( + &self, + old_exec_gas_price: u64, + used: u64, + capacity: u64, + ) -> u64 { + // TODO: This could be more sophisticated, e.g. have target fullness, rather than hardcoding 50%. + let new = match used.cmp(&(capacity / 2)) { + std::cmp::Ordering::Greater => { + old_exec_gas_price.saturating_add(self.exec_change_amount) + } + std::cmp::Ordering::Less => { + old_exec_gas_price.saturating_sub(self.exec_change_amount) + } + std::cmp::Ordering::Equal => old_exec_gas_price, + }; + max(new, self.min_exec_price) + } + + fn calculate_new_moving_average(&self, new_profit: i64) { + let old = *self.moving_average_profit.borrow(); + *self.moving_average_profit.borrow_mut() = (old + * (self.moving_average_window - 1)) + .saturating_add(new_profit) + .saturating_div(self.moving_average_window); + } + + fn calculate_profit_slope(&self, new_profit: i64) { + let old_profit = *self.last_profit.borrow(); + *self.profit_slope.borrow_mut() = new_profit - old_profit; + *self.last_profit.borrow_mut() = new_profit; + } +} diff --git a/crates/gas-price-algorithm/src/main.rs b/crates/gas-price-algorithm/src/main.rs new file mode 100644 index 00000000000..4f304d7c5e5 --- /dev/null +++ b/crates/gas-price-algorithm/src/main.rs @@ -0,0 +1,456 @@ +use plotters::prelude::*; +use rand::{ + rngs::StdRng, + Rng, + SeedableRng, +}; + +use plotters::coord::Shift; + +use gas_price_algorithm::AlgorithmV1; + +fn gen_noisy_signal(input: f64, components: &[f64]) -> f64 { + components + .iter() + .fold(0f64, |acc, &c| acc + f64::sin(input / c)) + / components.len() as f64 +} + +fn noisy_cost>(input: T) -> f64 +where + >::Error: core::fmt::Debug, +{ + const COMPONENTS: &[f64] = &[50.0, 100.0, 300.0, 1000.0, 500.0]; + let input = input.try_into().unwrap(); + gen_noisy_signal(input, COMPONENTS) +} + +fn arb_cost_signal(size: u32) -> Vec { + let mut rng = StdRng::seed_from_u64(999); + (0u32..size) + .map(noisy_cost) + .map(|x| x * 10_000. + 20_000.) + .map(|x| { + let val = rng.gen_range(-19_000.0..10_000.0); + x + val + }) + .map(|x| x as u64) + .collect() +} + +fn noisy_fullness>(input: T) -> f64 +where + >::Error: core::fmt::Debug, +{ + const COMPONENTS: &[f64] = &[-30.0, 40.0, 700.0, -340.0, 400.0]; + let input = input.try_into().unwrap(); + gen_noisy_signal(input, COMPONENTS) +} + +fn arb_fullness_signal(size: u32, capacity: u64) -> Vec<(u64, u64)> { + let mut rng = StdRng::seed_from_u64(888); + (0u32..size) + .map(noisy_fullness) + .map(|x| (0.5 * x + 0.4) * capacity as f64) + .map(|x| { + let val = rng.gen_range(-0.25 * capacity as f64..0.25 * capacity as f64); + x + val + }) + .map(|x| f64::min(x, capacity as f64)) + .map(|x| f64::max(x, 5.0)) + .map(|x| (x as u64, capacity)) + .collect() +} + +fn arbitrary_da_values() -> Vec<(i64, i64, u8)> { + let mut rng = StdRng::seed_from_u64(777); + (0..10_000) + .map(|_| { + let p = rng.gen_range(-20_000..20_000); + let d = rng.gen_range(-500..500); + // let max_change = rng.gen_range(1..255); + let max_change = 50; + (p, d, max_change) + }) + .filter(|(p, d, _)| *p != 0 && *d != 0) + .collect() +} + +fn main() { + // Try to find a good p and d value + let (vals, results, _error) = arbitrary_da_values() + .into_iter() + .map(|(p, d, max_change)| { + let results = run_simulation(p, d, max_change); + let total_profits = &results.total_profits; + + let total_profits_abs: Vec<_> = + total_profits.iter().map(|x| x.abs()).collect(); + let sum = total_profits_abs.iter().fold(0i64, |acc, val| { + let sum = acc.saturating_add(*val); + sum + }) as f64; + let len = total_profits_abs.len() as f64; + let average_profit_error = sum / len; + ((p, d, max_change), results, average_profit_error) + }) + .fold(None, |acc, (vals, result, err)| { + if let Some((_, _, best_err)) = acc { + if err < best_err { + Some((vals, result, err)) + } else { + acc + } + } else { + Some((vals, result, err)) + } + }) + .unwrap(); + + println!("Best p and d values: {:?}", vals); + + let SimulationResults { + da_recording_cost, + exec_fullness, + da_gas_prices, + exec_gas_prices, + total_gas_prices, + total_profits, + da_rewards, + } = results; + + // Plotting code starts here + let plot_width = 640 * 2; + let plot_height = 480 * 3; + + let root = BitMapBackend::new("gas_prices.png", (plot_width, plot_height)) + .into_drawing_area(); + root.fill(&WHITE).unwrap(); + let (upper, lower) = root.split_vertically(plot_height / 3); + let (middle, bottom) = lower.split_vertically(plot_height / 3); + + draw_da_chart( + &upper, + &total_profits, + &da_recording_cost, + &da_rewards, + &da_gas_prices, + vals.0, + vals.1, + vals.2, + ); + draw_exec_chart(&middle, &total_profits, &exec_fullness, &exec_gas_prices); + draw_total_gas_price(&bottom, &da_gas_prices, &exec_gas_prices, &total_gas_prices); + + root.present().unwrap(); +} + +struct SimulationResults { + da_recording_cost: Vec, + exec_fullness: Vec<(u64, u64)>, + da_gas_prices: Vec, + exec_gas_prices: Vec, + total_gas_prices: Vec, + total_profits: Vec, + da_rewards: Vec, +} + +fn run_simulation( + p_value_factor: i64, + d_value_factor: i64, + max_change_percent: u8, +) -> SimulationResults { + let min_da_price = 10; + let min_exec_price = 10; + let moving_average_window = 10; + // // TODO: This value is large because it only changes once per `da_record_frequency` blocks. + // // Is it possible to decrease if we get the p and d values tuned better? We should be able + // // to solve for the lower granularity still, looking at how much we overshoot. + // let max_change_percent = 200; + let exec_change_amount = 10; + let algo = AlgorithmV1::new( + max_change_percent, + min_da_price, + p_value_factor, + d_value_factor, + moving_average_window, + min_exec_price, + exec_change_amount, + ); + + let capacity = 400; + let simulation_size = 1_000; + + // Run simulation + let da_recording_cost = arb_cost_signal(simulation_size); + let exec_fullness = arb_fullness_signal(simulation_size, capacity); + let mut da_gas_price: u64 = 100; + let mut da_gas_prices = vec![da_gas_price as i64]; + let mut exec_gas_price = 0; + let mut exec_gas_prices = vec![exec_gas_price as i64]; + let mut total_gas_prices = vec![(da_gas_price + exec_gas_price) as i64]; + let mut total_profits = vec![0i64]; + let mut da_rewards = vec![]; + let mut total_da_cost = 0; + let mut total_da_reward: u64 = 0; + + let da_record_frequency = 12; + let mut da_record_counter = 0; + + for (da_cost, (used, capacity)) in da_recording_cost.iter().zip(exec_fullness.iter()) + { + total_da_cost += da_cost; + let da_reward = da_gas_price.saturating_mul(*used); + da_rewards.push(da_reward); + total_da_reward = total_da_reward.saturating_add(da_reward); + let total_profit = total_da_reward as i64 - total_da_cost as i64; + total_profits.push(total_profit); + exec_gas_price = algo.calculate_exec_gas_price(exec_gas_price, *used, *capacity); + // Only update the da gas price every da_record_frequency blocks + if da_record_counter % da_record_frequency == 0 { + da_gas_price = + algo.calculate_da_gas_price(da_gas_price, total_da_reward, total_da_cost); + } + da_record_counter += 1; + + da_gas_prices.push(da_gas_price as i64); + exec_gas_prices.push(exec_gas_price as i64); + total_gas_prices.push((da_gas_price + exec_gas_price) as i64); + } + + SimulationResults { + da_recording_cost, + exec_fullness, + da_gas_prices, + exec_gas_prices, + total_gas_prices, + total_profits, + da_rewards, + } +} + +fn draw_total_gas_price( + drawing_area: &DrawingArea, + da_gas_prices: &Vec, + exec_gas_prices: &Vec, + total_gas_prices: &Vec, +) { + const DA_COLOR: RGBColor = BLUE; + const EXEC_COLOR: RGBColor = RED; + const TOTAL_COLOR: RGBColor = BLACK; + + let min = 0; + let max = *total_gas_prices.iter().max().unwrap(); + + let mut total_gas_price_chart = ChartBuilder::on(drawing_area) + .caption("Total Gas Price", ("sans-serif", 50).into_font()) + .margin(5) + .x_label_area_size(40) + .y_label_area_size(60) + .right_y_label_area_size(40) + .build_cartesian_2d(0..total_gas_prices.len(), min..max) + .unwrap(); + + total_gas_price_chart + .configure_mesh() + .y_desc("Gas Price") + .x_desc("Block") + .draw() + .unwrap(); + + total_gas_price_chart + .draw_series(LineSeries::new( + da_gas_prices.iter().enumerate().map(|(x, y)| (x, *y)), + &DA_COLOR, + )) + .unwrap() + .label("DA Gas Price") + .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &DA_COLOR)); + + total_gas_price_chart + .draw_series(LineSeries::new( + exec_gas_prices.iter().enumerate().map(|(x, y)| (x, *y)), + &EXEC_COLOR, + )) + .unwrap() + .label("Exec Gas Price") + .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &EXEC_COLOR)); + + total_gas_price_chart + .draw_series(LineSeries::new( + total_gas_prices.iter().enumerate().map(|(x, y)| (x, *y)), + &TOTAL_COLOR, + )) + .unwrap() + .label("Total Gas Price") + .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &TOTAL_COLOR)); + + total_gas_price_chart + .configure_series_labels() + .background_style(&WHITE.mix(0.8)) + .border_style(&BLACK) + .draw() + .unwrap(); +} + +fn draw_exec_chart( + drawing_area: &DrawingArea, + total_profits: &Vec, + exec_fullness: &Vec<(u64, u64)>, + exec_gas_prices: &Vec, +) { + const PRICE_COLOR: RGBColor = BLACK; + const FULLNESS_COLOR: RGBColor = RED; + + let exec_min = 0; + let exec_max = 100; + + let mut exec_chart = ChartBuilder::on(drawing_area) + .caption("Execution", ("sans-serif", 50).into_font()) + .margin(5) + .x_label_area_size(40) + .y_label_area_size(60) + .right_y_label_area_size(40) + .build_cartesian_2d(0..total_profits.len(), exec_min..exec_max) + .unwrap() + .set_secondary_coord( + 0..exec_gas_prices.len(), + 0..*exec_gas_prices.iter().max().unwrap() + 9, + ); + + exec_chart + .configure_mesh() + .y_desc("Fullness Percentage") + .x_desc("Block") + .draw() + .unwrap(); + + exec_chart + .configure_secondary_axes() + .y_desc("Gas Price") + .draw() + .unwrap(); + + exec_chart + .draw_series(LineSeries::new( + exec_fullness + .iter() + .map(|(x, y)| (*x as f64 / *y as f64) * 100.) + .map(|x| x as i32) + .enumerate(), + &FULLNESS_COLOR, + )) + .unwrap() + .label("Fullness Percentage") + .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &FULLNESS_COLOR)); + + exec_chart + .draw_secondary_series(LineSeries::new( + exec_gas_prices.iter().enumerate().map(|(x, y)| (x, *y)), + &PRICE_COLOR, + )) + .unwrap() + .label("Gas Price") + .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &PRICE_COLOR)); + + exec_chart + .configure_series_labels() + .background_style(&WHITE.mix(0.8)) + .border_style(&BLACK) + .draw() + .unwrap(); +} + +fn draw_da_chart( + drawing_area: &DrawingArea, + total_profits: &Vec, + da_recording_cost: &Vec, + da_rewards: &Vec, + da_gas_prices: &Vec, + p_value: i64, + d_value: i64, + max_change: u8, +) { + let da_min = *total_profits.iter().min().unwrap() - 10_000; + let da_max = *total_profits.iter().max().unwrap() as i64 + 10_000; + + let mut da_chart = ChartBuilder::on(drawing_area) + .caption( + &format!("DA Recording Costs (p: {p_value:?}, d: {d_value:?}, max_change: {max_change:?}%)"), + ("sans-serif", 50).into_font(), + ) + .margin(5) + .x_label_area_size(40) + .y_label_area_size(60) + .right_y_label_area_size(40) + .build_cartesian_2d(0..total_profits.len(), da_min..da_max) + .unwrap() + .set_secondary_coord( + 0..da_gas_prices.len(), + 0..*da_gas_prices.iter().max().unwrap(), + ); + + da_chart + .configure_mesh() + .y_desc("Profit/Cost/Reward") + .x_desc("Block") + .draw() + .unwrap(); + + da_chart + .configure_secondary_axes() + .y_desc("Gas Price") + .draw() + .unwrap(); + + const PRICE_COLOR: RGBColor = BLACK; + const PROFIT_COLOR: RGBColor = BLUE; + const REWARD_COLOR: RGBColor = GREEN; + const COST_COLOR: RGBColor = RED; + + da_chart + .draw_series(LineSeries::new( + total_profits.iter().enumerate().map(|(x, y)| (x, *y)), + &PROFIT_COLOR, + )) + .unwrap() + .label("Profit") + .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &PROFIT_COLOR)); + + da_chart + .draw_series(LineSeries::new( + da_recording_cost + .into_iter() + .enumerate() + .map(|(x, y)| (x, *y as i64)), + &COST_COLOR, + )) + .unwrap() + .label("Cost") + .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &COST_COLOR)); + + da_chart + .draw_series(LineSeries::new( + da_rewards.iter().enumerate().map(|(x, y)| (x, *y as i64)), + &REWARD_COLOR, + )) + .unwrap() + .label("Reward") + .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &REWARD_COLOR)); + + da_chart + .draw_secondary_series(LineSeries::new( + da_gas_prices.iter().enumerate().map(|(x, y)| (x, *y)), + &PRICE_COLOR, + )) + .unwrap() + .label("Gas Price") + .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &PRICE_COLOR)); + + da_chart + .configure_series_labels() + .background_style(&WHITE.mix(0.8)) + .border_style(&BLACK) + .draw() + .unwrap(); +} diff --git a/crates/services/producer/src/block_producer.rs b/crates/services/producer/src/block_producer.rs index 57f0a715447..b43529b6b1a 100644 --- a/crates/services/producer/src/block_producer.rs +++ b/crates/services/producer/src/block_producer.rs @@ -124,7 +124,7 @@ where let gas_price = self .gas_price_provider .gas_price(height.into()) - .ok_or(anyhow!("No gas price found for block {height:?}"))?; + .map_err(|e| anyhow!("No gas price found for block {height:?}: {e:?}"))?; let component = Components { header_to_produce: header, @@ -221,7 +221,7 @@ where let gas_price = self .gas_price_provider .gas_price(height.into()) - .ok_or(anyhow!("No gas price found for height {height:?}"))?; + .map_err(|e| anyhow!("No gas price found for height {height:?}: {e:?}"))?; // The dry run execution should use the state of the blockchain based on the // last available block, not on the upcoming one. It means that we need to diff --git a/crates/services/producer/src/block_producer/gas_price.rs b/crates/services/producer/src/block_producer/gas_price.rs index 59245999f90..86720992836 100644 --- a/crates/services/producer/src/block_producer/gas_price.rs +++ b/crates/services/producer/src/block_producer/gas_price.rs @@ -29,7 +29,7 @@ impl From for GasPriceParams { /// Interface for retrieving the gas price for a block pub trait GasPriceProvider { /// The gas price for all transactions in the block. - fn gas_price(&self, params: GasPriceParams) -> Option; + fn gas_price(&self, params: GasPriceParams) -> anyhow::Result; } /// Interface for retrieving the consensus parameters. diff --git a/gas_prices.png b/gas_prices.png new file mode 100644 index 00000000000..a335fbec7d5 Binary files /dev/null and b/gas_prices.png differ