Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use gas prices from actual blocks to calculate estimate gas prices #2501

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- [2511](https://github.com/FuelLabs/fuel-core/pull/2511): Fix backward compatibility of V0Metadata in gas price db.

### Changed
- [2501](https://github.com/FuelLabs/fuel-core/pull/2501): Use gas price from block for estimating future gas prices
- [2468](https://github.com/FuelLabs/fuel-core/pull/2468): Abstract unrecorded blocks concept for V1 algorithm, create new storage impl. Introduce `TransactionableStorage` trait to allow atomic changes to the storage.
- [2295](https://github.com/FuelLabs/fuel-core/pull/2295): `CombinedDb::from_config` now respects `state_rewind_policy` with tmp RocksDB.
- [2378](https://github.com/FuelLabs/fuel-core/pull/2378): Use cached hash of the topic instead of calculating it on each publishing gossip message.
Expand Down Expand Up @@ -484,7 +485,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
The change has many minor improvements in different areas related to the state transition bytecode:
- The state transition bytecode lies in its own file(`state_transition_bytecode.wasm`) along with the chain config file. The `ChainConfig` loads it automatically when `ChainConfig::load` is called and pushes it back when `ChainConfig::write` is called.
- The `fuel-core` release bundle also contains the `fuel-core-wasm-executor.wasm` file of the corresponding executor version.
- The regenesis process now considers the last block produced by the previous network. When we create a (re)genesis block of a new network, it has the `height = last_block_of_old_network + 1`. It continues the old network and doesn't overlap blocks(before, we had `old_block.height == new_genesis_block.height`).
- The regenesis process now considers the last block produced by the previous network. When we create a (re)genesis block of a new network, it has the `height = last_block_of_old_netowkr + 1`. It continues the old network and doesn't overlap blocks(before, we had `old_block.height == new_genesis_block.height`).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

merge issue here

- Along with the new block height, the regenesis process also increases the state transition bytecode and consensus parameters versions. It guarantees that a new network doesn't use values from the previous network and allows us not to migrate `StateTransitionBytecodeVersions` and `ConsensusParametersVersions` tables.
- Added a new CLI argument, `native-executor-version,` that allows overriding of the default version of the native executor. It can be useful for side rollups that have their own history of executor upgrades.
- Replaced:
Expand Down
7 changes: 7 additions & 0 deletions crates/fuel-core/proptest-regressions/service/adapters.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 87eb4d9c7c90c2a0ee11edef3ee1b01303db0a6ba1fd34d7fc6146a2c36387f4 # shrinks to gas_price = 1, starting_height = 0, block_horizon = 1, percentage = 100
186 changes: 172 additions & 14 deletions crates/fuel-core/src/service/adapters.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
use crate::{
database::{
database_description::relayer::Relayer,
Database,
},
fuel_core_graphql_api::ports::GasPriceEstimate,
service::{
sub_services::{
BlockProducerService,
TxPoolSharedState,
},
vm_pool::MemoryPool,
},
};
use fuel_core_consensus_module::{
block_verifier::Verifier,
RelayerConsensusConfig,
};
use fuel_core_executor::executor::OnceTransactionsSource;
use fuel_core_gas_price_service::v1::service::LatestGasPrice;
use fuel_core_importer::ImporterResult;
use fuel_core_poa::{
ports::BlockSigner,
Expand All @@ -19,6 +34,7 @@ use fuel_core_types::{
consensus::Consensus,
},
fuel_tx::Transaction,
fuel_types::BlockHeight,
services::{
block_importer::SharedImportResult,
block_producer::Components,
Expand All @@ -32,20 +48,6 @@ use fuel_core_types::{
use fuel_core_upgradable_executor::executor::Executor;
use std::sync::Arc;

use crate::{
database::{
database_description::relayer::Relayer,
Database,
},
service::{
sub_services::{
BlockProducerService,
TxPoolSharedState,
},
vm_pool::MemoryPool,
},
};

pub mod block_importer;
pub mod consensus_module;
pub mod consensus_parameters_provider;
Expand Down Expand Up @@ -85,6 +87,162 @@ impl StaticGasPrice {
}
}

#[cfg(test)]
mod arc_gas_price_estimate_tests {
#![allow(non_snake_case)]

use super::*;
use proptest::proptest;

async fn _worst_case__correctly_calculates_value(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this function is called later on

Suggested change
async fn _worst_case__correctly_calculates_value(
async fn worst_case__correctly_calculates_value(

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. It's the actual test code, but I don't like writing it inside the prop_test macro because the IDE has a hard time parsing it:

proptest! {
    #[test]
    fn worst_case__correctly_calculates_value(
        price: u64,
        starting_height: u32,
        block_horizon in 0..10_000u32,
        percentage: u64
    ) {
        _worst_case__correctly_calculates_value(price, starting_height, block_horizon, percentage);
    }
}

So this is my naming convention for that case.

gas_price: u64,
starting_height: u32,
block_horizon: u32,
percentage: u16,
) {
// given
let subject = ArcGasPriceEstimate::new(starting_height, gas_price, percentage);

// when
let target_height = starting_height.saturating_add(block_horizon);
let estimated = subject
.worst_case_gas_price(target_height.into())
.await
.unwrap();

// then
let mut actual = gas_price;

for _ in 0..block_horizon {
let change_amount =
actual.saturating_mul(percentage as u64).saturating_div(100);
actual = actual.saturating_add(change_amount);
}

assert!(estimated >= actual);
}

proptest! {
#[test]
fn worst_case_gas_price__correctly_calculates_value(
gas_price: u64,
starting_height: u32,
block_horizon in 0..10_000u32,
percentage: u16,
) {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(_worst_case__correctly_calculates_value(
gas_price,
starting_height,
block_horizon,
percentage,
));
}
}

proptest! {
#[test]
fn worst_case_gas_price__never_overflows(
gas_price: u64,
starting_height: u32,
block_horizon in 0..10_000u32,
percentage: u16
) {
let rt = tokio::runtime::Runtime::new().unwrap();

// given
let subject = ArcGasPriceEstimate::new(starting_height, gas_price, percentage);

// when
let target_height = starting_height.saturating_add(block_horizon);

let _ = rt.block_on(subject.worst_case_gas_price(target_height.into()));

// then
// doesn't panic with an overflow
}
}
}

/// Allows communication from other service with more recent gas price data
/// `Height` refers to the height of the block at which the gas price was last updated
/// `GasPrice` refers to the gas price at the last updated block
#[allow(dead_code)]
pub struct ArcGasPriceEstimate<Height, GasPrice> {
Copy link
Contributor

@acerone85 acerone85 Dec 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it be possible to have a bit of documentation here?
In particular, I'd like to understand what the "height" and "price" refer to here, since later they are named as "best_height" and "best_price"

/// Shared state of latest gas price data
latest_gas_price: LatestGasPrice<Height, GasPrice>,
/// The max percentage the gas price can increase per block
percentage: u16,
}

impl<Height, GasPrice> ArcGasPriceEstimate<Height, GasPrice> {
#[cfg(test)]
pub fn new(height: Height, price: GasPrice, percentage: u16) -> Self {
let latest_gas_price = LatestGasPrice::new(height, price);
Self {
latest_gas_price,
percentage,
}
}

pub fn new_from_inner(
inner: LatestGasPrice<Height, GasPrice>,
percentage: u16,
) -> Self {
Self {
latest_gas_price: inner,
percentage,
}
}
}

impl<Height: Copy, GasPrice: Copy> ArcGasPriceEstimate<Height, GasPrice> {
fn get_height_and_gas_price(&self) -> (Height, GasPrice) {
self.latest_gas_price.get()
}
}

#[async_trait::async_trait]
impl GasPriceEstimate for ArcGasPriceEstimate<u32, u64> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this have to be async?

async fn worst_case_gas_price(&self, height: BlockHeight) -> Option<u64> {
Copy link
Contributor

@acerone85 acerone85 Dec 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so what I get here is that we assume that the price is going to raise by the specified percentage every block until height is reached, is that correct?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct.

let (best_height, best_gas_price) = self.get_height_and_gas_price();
let percentage = self.percentage;

let worst = cumulative_percentage_change(
best_gas_price,
best_height,
percentage as u64,
height.into(),
);
Some(worst)
}
}

#[allow(clippy::cast_possible_truncation)]
pub(crate) fn cumulative_percentage_change(
start_gas_price: u64,
best_height: u32,
percentage: u64,
target_height: u32,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: you can pass directly the difference of target_height and best_height to this function and get rid of one parameter, but I don't have a strong opinion here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with this for now.

) -> u64 {
let blocks = target_height.saturating_sub(best_height) as f64;
let percentage_as_decimal = percentage as f64 / 100.0;
let multiple = (1.0f64 + percentage_as_decimal).powf(blocks);
let mut approx = start_gas_price as f64 * multiple;
// Account for rounding errors and take a slightly higher value
// Around the `ROUNDING_ERROR_CUTOFF` the rounding errors will cause the estimate to be too low.
// We increase by `ROUNDING_ERROR_COMPENSATION` to account for this.
// This is an unlikely situation in practice, but we want to guarantee that the actual
// gas price is always equal or less than the estimate given here
const ROUNDING_ERROR_CUTOFF: f64 = 16948547188989277.0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you put a comment about how this value has been obtained?

if approx > ROUNDING_ERROR_CUTOFF {
const ROUNDING_ERROR_COMPENSATION: f64 = 2000.0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

approx += ROUNDING_ERROR_COMPENSATION;
}
// `f64` over `u64::MAX` are cast to `u64::MAX`
approx.ceil() as u64
}

#[derive(Clone)]
pub struct PoAAdapter {
shared_state: Option<fuel_core_poa::service::SharedState>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ pub fn get_block_info(
block_gas_capacity: block_gas_limit,
block_bytes: Postcard::encode(block).len() as u64,
block_fees: fee,
gas_price,
};
Ok(info)
}
Expand Down
2 changes: 2 additions & 0 deletions crates/services/gas_price_service/src/common/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,7 @@ pub enum BlockInfo {
block_bytes: u64,
// The fees the block has collected
block_fees: u64,
// The gas price used in the block
gas_price: u64,
},
}
1 change: 1 addition & 0 deletions crates/services/gas_price_service/src/v0/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ mod tests {
block_gas_capacity: 100,
block_bytes: 100,
block_fees: 100,
gas_price: 100,
};

let (l2_block_sender, l2_block_receiver) = mpsc::channel(1);
Expand Down
2 changes: 2 additions & 0 deletions crates/services/gas_price_service/src/v0/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ async fn next_gas_price__affected_by_new_l2_block() {
block_gas_capacity: 100,
block_bytes: 100,
block_fees: 100,
gas_price: 100,
};
let (l2_block_sender, l2_block_receiver) = tokio::sync::mpsc::channel(1);
let l2_block_source = FakeL2BlockSource {
Expand Down Expand Up @@ -200,6 +201,7 @@ async fn next__new_l2_block_saves_old_metadata() {
block_gas_capacity: 100,
block_bytes: 100,
block_fees: 100,
gas_price: 100,
};
let (l2_block_sender, l2_block_receiver) = tokio::sync::mpsc::channel(1);
let l2_block_source = FakeL2BlockSource {
Expand Down
Loading
Loading