Skip to content

Commit

Permalink
Deal with negative feed back loop in DA gas price (#2364)
Browse files Browse the repository at this point in the history
  • Loading branch information
MitchTurner authored Oct 24, 2024
1 parent 481d4bb commit a14922e
Show file tree
Hide file tree
Showing 5 changed files with 508 additions and 58 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

### Added
- [2321](https://github.com/FuelLabs/fuel-core/pull/2321): New metrics for the txpool: "The size of transactions in the txpool" (`txpool_tx_size`), "The time spent by a transaction in the txpool in seconds" (`txpool_tx_time_in_txpool_seconds`), The number of transactions in the txpool (`txpool_number_of_transactions`), "The number of transactions pending verification before entering the txpool" (`txpool_number_of_transactions_pending_verification`), "The number of executable transactions in the txpool" (`txpool_number_of_executable_transactions`), "The time it took to select transactions for inclusion in a block in nanoseconds" (`txpool_select_transaction_time_nanoseconds`), The time it took to insert a transaction in the txpool in milliseconds (`txpool_insert_transaction_time_milliseconds`).
- [2347](https://github.com/FuelLabs/fuel-core/pull/2364): Add activity concept in order to protect against infinitely increasing DA gas price scenarios
- [2362](https://github.com/FuelLabs/fuel-core/pull/2362): Added a new request_response protocol version `/fuel/req_res/0.0.2`. In comparison with `/fuel/req/0.0.1`, which returns an empty response when a request cannot be fulfilled, this version returns more meaningful error codes. Nodes still support the version `0.0.1` of the protocol to guarantee backward compatibility with fuel-core nodes. Empty responses received from nodes using the old protocol `/fuel/req/0.0.1` are automatically converted into an error `ProtocolV1EmptyResponse` with error code 0, which is also the only error code implemented. More specific error codes will be added in the future.

## [Version 0.40.0]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use super::*;
use fuel_gas_price_algorithm::v1::AlgorithmUpdaterV1;
use fuel_gas_price_algorithm::v1::{
AlgorithmUpdaterV1,
L2ActivityTracker,
};
use std::{
collections::BTreeMap,
num::NonZeroU64,
Expand Down Expand Up @@ -83,6 +86,7 @@ impl Simulator {
) -> AlgorithmUpdaterV1 {
// Scales the gas price internally, value is arbitrary
let gas_price_factor = 100;
let always_normal_activity = L2ActivityTracker::new_always_normal();
let updater = AlgorithmUpdaterV1 {
min_exec_gas_price: 10,
min_da_gas_price: 10,
Expand All @@ -109,6 +113,7 @@ impl Simulator {
da_d_component,
last_profit: 0,
second_to_last_profit: 0,
l2_activity: always_normal_activity,
};
updater
}
Expand Down
174 changes: 161 additions & 13 deletions crates/fuel-gas-price-algorithm/src/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ pub enum Error {
L2BlockExpectedNotFound(u32),
}

// TODO: separate exec gas price and DA gas price into newtypes for clarity
// https://github.com/FuelLabs/fuel-core/issues/2382
#[derive(Debug, Clone, PartialEq)]
pub struct AlgorithmV1 {
/// The gas price for to cover the execution of the next block
Expand Down Expand Up @@ -118,13 +120,8 @@ pub struct AlgorithmUpdaterV1 {
/// This is a percentage of the total capacity of the L2 block
pub l2_block_fullness_threshold_percent: ClampedPercentage,
// DA
/// The gas price for the DA portion of the last block. This can be used to calculate
/// the DA portion of the next block
// pub last_da_gas_price: u64,

/// The gas price (scaled by the `gas_price_factor`) to cover the DA commitment of the next block
pub new_scaled_da_gas_price: u64,

/// Scale factor for the gas price.
pub gas_price_factor: NonZeroU64,
/// The lowest the algorithm allows the da gas price to go
Expand All @@ -151,13 +148,136 @@ pub struct AlgorithmUpdaterV1 {
pub second_to_last_profit: i128,
/// The latest known cost per byte for recording blocks on the DA chain
pub latest_da_cost_per_byte: u128,

/// Activity of L2
pub l2_activity: L2ActivityTracker,
/// The unrecorded blocks that are used to calculate the projected cost of recording blocks
pub unrecorded_blocks: BTreeMap<Height, Bytes>,
}

/// A value that represents a value between 0 and 100. Higher values are clamped to 100
/// The `L2ActivityTracker` tracks the chain activity to determine a safety mode for setting the DA price.
///
/// Because the DA gas price can increase even when no-one is using the network, there is a potential
/// for a negative feedback loop to occur where the gas price increases, further decreasing activity
/// and increasing the gas price. The `L2ActivityTracker` is used to moderate changes to the DA
/// gas price based on the activity of the L2 chain.
///
/// The chain activity is a cumulative measure, updated whenever a new block is processed.
/// For each L2 block, the block usage is a percentage of the block capacity used. If the
/// block usage is below a certain threshold, the chain activity is decreased, if above the threshold,
/// the activity is increased The chain activity exists on a scale
/// between 0 and the sum of the normal, capped, and decrease buffers.
///
/// e.g. if the decrease activity threshold is 20, the capped activity threshold is 80, and the max activity is 120,
/// we'd have the following ranges:
///
/// 0 <-- decrease range -->20<-- capped range -->80<-- normal range -->120
///
/// The current chain activity determines the behavior of the DA gas price.
///
/// For healthy behavior, the activity should be in the `normal` range.
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
pub struct L2ActivityTracker {
/// The maximum value the chain activity can hit
max_activity: u16,
/// The threshold if the block activity is below, the DA gas price will be held when it would otherwise be increased
capped_activity_threshold: u16,
/// If the chain activity falls below this value, the DA gas price will be decreased when it would otherwise be increased
decrease_activity_threshold: u16,
/// The current activity of the L2 chain
chain_activity: u16,
/// The threshold of block activity below which the chain activity will be decreased,
/// above or equal it will always increase
block_activity_threshold: ClampedPercentage,
}

/// Designates the intended behavior of the DA gas price based on the activity of the L2 chain
pub enum DAGasPriceSafetyMode {
/// Should increase DA gas price freely
Normal,
/// Should not increase the DA gas price
Capped,
/// Should decrease the DA gas price always
AlwaysDecrease,
}

impl L2ActivityTracker {
pub fn new_full(
normal_range_size: u16,
capped_range_size: u16,
decrease_range_size: u16,
block_activity_threshold: ClampedPercentage,
) -> Self {
let decrease_activity_threshold = decrease_range_size;
let capped_activity_threshold =
decrease_range_size.saturating_add(capped_range_size);
let max_activity = capped_activity_threshold.saturating_add(normal_range_size);
let chain_activity = max_activity;
Self {
max_activity,
capped_activity_threshold,
decrease_activity_threshold,
chain_activity,
block_activity_threshold,
}
}

pub fn new(
normal_range_size: u16,
capped_range_size: u16,
decrease_range_size: u16,
activity: u16,
block_activity_threshold: ClampedPercentage,
) -> Self {
let mut tracker = Self::new_full(
normal_range_size,
capped_range_size,
decrease_range_size,
block_activity_threshold,
);
tracker.chain_activity = activity.min(tracker.max_activity);
tracker
}

pub fn new_always_normal() -> Self {
let normal_range_size = 100;
let capped_range_size = 0;
let decrease_range_size = 0;
let percentage = ClampedPercentage::new(0);
Self::new(
normal_range_size,
capped_range_size,
decrease_range_size,
100,
percentage,
)
}

pub fn safety_mode(&self) -> DAGasPriceSafetyMode {
if self.chain_activity > self.capped_activity_threshold {
DAGasPriceSafetyMode::Normal
} else if self.chain_activity > self.decrease_activity_threshold {
DAGasPriceSafetyMode::Capped
} else {
DAGasPriceSafetyMode::AlwaysDecrease
}
}

pub fn update(&mut self, block_usage: ClampedPercentage) {
if block_usage < self.block_activity_threshold {
self.chain_activity = self.chain_activity.saturating_sub(1);
} else {
self.chain_activity =
self.chain_activity.saturating_add(1).min(self.max_activity);
}
}

pub fn current_activity(&self) -> u16 {
self.chain_activity
}
}

/// A value that represents a value between 0 and 100. Higher values are clamped to 100
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, PartialOrd)]
pub struct ClampedPercentage {
value: u8,
}
Expand Down Expand Up @@ -226,6 +346,9 @@ impl AlgorithmUpdaterV1 {
let last_profit = rewards.saturating_sub(projected_total_da_cost);
self.update_last_profit(last_profit);

// activity
self.update_activity(used, capacity);

// gas prices
self.update_exec_gas_price(used, capacity);
self.update_da_gas_price();
Expand All @@ -236,6 +359,12 @@ impl AlgorithmUpdaterV1 {
}
}

fn update_activity(&mut self, used: u64, capacity: NonZeroU64) {
let block_activity = used.saturating_mul(100).div(capacity);
let usage = ClampedPercentage::new(block_activity.try_into().unwrap_or(100));
self.l2_activity.update(usage);
}

fn update_da_rewards(&mut self, fee_wei: u128) {
let block_da_reward = self.da_portion_of_fee(fee_wei);
self.total_da_rewards_excess =
Expand Down Expand Up @@ -309,7 +438,8 @@ impl AlgorithmUpdaterV1 {
fn update_da_gas_price(&mut self) {
let p = self.p();
let d = self.d();
let da_change = self.da_change(p, d);
let maybe_da_change = self.da_change(p, d);
let da_change = self.da_change_accounting_for_activity(maybe_da_change);
let maybe_new_scaled_da_gas_price = i128::from(self.new_scaled_da_gas_price)
.checked_add(da_change)
.and_then(|x| u64::try_from(x).ok())
Expand All @@ -326,6 +456,20 @@ impl AlgorithmUpdaterV1 {
);
}

fn da_change_accounting_for_activity(&self, maybe_da_change: i128) -> i128 {
if maybe_da_change > 0 {
match self.l2_activity.safety_mode() {
DAGasPriceSafetyMode::Normal => maybe_da_change,
DAGasPriceSafetyMode::Capped => 0,
DAGasPriceSafetyMode::AlwaysDecrease => {
self.max_change().saturating_mul(-1)
}
}
} else {
maybe_da_change
}
}

fn min_scaled_da_gas_price(&self) -> u64 {
self.min_da_gas_price
.saturating_mul(self.gas_price_factor.into())
Expand All @@ -348,14 +492,18 @@ impl AlgorithmUpdaterV1 {

fn da_change(&self, p: i128, d: i128) -> i128 {
let pd_change = p.saturating_add(d);
let max_change = self.max_change();
let clamped_change = pd_change.saturating_abs().min(max_change);
pd_change.signum().saturating_mul(clamped_change)
}

// Should always be positive
fn max_change(&self) -> i128 {
let upcast_percent = self.max_da_gas_price_change_percent.into();
let max_change = self
.new_scaled_da_gas_price
self.new_scaled_da_gas_price
.saturating_mul(upcast_percent)
.saturating_div(100)
.into();
let clamped_change = pd_change.saturating_abs().min(max_change);
pd_change.signum().saturating_mul(clamped_change)
.into()
}

fn exec_change(&self, principle: u64) -> u64 {
Expand Down
13 changes: 12 additions & 1 deletion crates/fuel-gas-price-algorithm/src/v1/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
#![allow(clippy::arithmetic_side_effects)]
#![allow(clippy::cast_possible_truncation)]

use crate::v1::AlgorithmUpdaterV1;
use crate::v1::{
AlgorithmUpdaterV1,
L2ActivityTracker,
};

#[cfg(test)]
mod algorithm_v1_tests;
Expand Down Expand Up @@ -40,6 +43,7 @@ pub struct UpdaterBuilder {
last_profit: i128,
second_to_last_profit: i128,
da_gas_price_factor: u64,
l2_activity: L2ActivityTracker,
}

impl UpdaterBuilder {
Expand Down Expand Up @@ -67,6 +71,7 @@ impl UpdaterBuilder {
last_profit: 0,
second_to_last_profit: 0,
da_gas_price_factor: 1,
l2_activity: L2ActivityTracker::new_always_normal(),
}
}

Expand Down Expand Up @@ -159,6 +164,11 @@ impl UpdaterBuilder {
self
}

fn with_activity(mut self, l2_activity: L2ActivityTracker) -> Self {
self.l2_activity = l2_activity;
self
}

fn build(self) -> AlgorithmUpdaterV1 {
AlgorithmUpdaterV1 {
min_exec_gas_price: self.min_exec_gas_price,
Expand Down Expand Up @@ -190,6 +200,7 @@ impl UpdaterBuilder {
.da_gas_price_factor
.try_into()
.expect("Should never be non-zero"),
l2_activity: self.l2_activity,
}
}
}
Expand Down
Loading

0 comments on commit a14922e

Please sign in to comment.