Skip to content

feat: fee algo performance metrics #215

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

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
272 changes: 5 additions & 267 deletions fee_algo_simulation/src/handlers.rs
Original file line number Diff line number Diff line change
@@ -1,267 +1,5 @@
use std::time::Duration;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Mostly moved into a separate file.

use actix_web::{HttpResponse, Responder, ResponseError, web};
use anyhow::Result;
use eth::HttpClient;
use services::{
fee_metrics_tracker::service::calculate_blob_tx_fee,
fees::{Api, FeesAtHeight, SequentialBlockFees, cache::CachingApi},
state_committer::{AlgoConfig, SmaFeeAlgo},
types::{DateTime, Utc},
};
use thiserror::Error;
use tracing::{error, info};

use super::{
models::{FeeDataPoint, FeeParams, FeeResponse, FeeStats},
state::AppState,
utils::last_n_blocks,
};

#[derive(Error, Debug)]
pub enum FeeError {
#[error("Internal Server Error: {0}")]
InternalError(String),

#[error("Bad Request: {0}")]
BadRequest(String),
}

impl ResponseError for FeeError {
fn error_response(&self) -> HttpResponse {
match self {
FeeError::InternalError(message) => {
HttpResponse::InternalServerError().body(message.clone())
}
FeeError::BadRequest(message) => HttpResponse::BadRequest().body(message.clone()),
}
}
}

pub async fn index_html() -> impl Responder {
let contents = include_str!("index.html");
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(contents)
}

struct FeeHandler {
state: web::Data<AppState>,
params: FeeParams,
config: AlgoConfig,
seq_fees: SequentialBlockFees,
last_block_height: u64,
last_block_time: DateTime<Utc>,
sma_algo: SmaFeeAlgo<CachingApi<HttpClient>>,
}

impl FeeHandler {
async fn new(state: web::Data<AppState>, params: FeeParams) -> Result<Self, FeeError> {
let ending_height = Self::resolve_ending_height(&state, &params).await?;
let start_height = ending_height.saturating_sub(params.amount_of_blocks);
let config = Self::parse_config(&params)?;
let seq_fees = Self::fetch_fees(&state, start_height, ending_height).await?;
let last_block = Self::get_last_block_info(&state, &seq_fees).await?;
let sma_algo = SmaFeeAlgo::new(state.fee_api.clone(), config);

Ok(Self {
state,
params,
config,
seq_fees,
last_block_height: last_block.0,
last_block_time: last_block.1,
sma_algo,
})
}

async fn get_fees_response(&self) -> Result<FeeResponse, FeeError> {
let data = self.calculate_fee_data().await?;
let stats = self.calculate_statistics(&data);
Ok(FeeResponse { data, stats })
}

async fn resolve_ending_height(
state: &web::Data<AppState>,
params: &FeeParams,
) -> Result<u64, FeeError> {
if let Some(val) = params.ending_height {
Ok(val)
} else {
state.fee_api.current_height().await.map_err(|e| {
error!("Error fetching current height: {:?}", e);
FeeError::InternalError("Failed to fetch current height".into())
})
}
}

fn parse_config(params: &FeeParams) -> Result<AlgoConfig, FeeError> {
AlgoConfig::try_from(params.clone()).map_err(|e| {
error!("Error parsing config: {:?}", e);
FeeError::BadRequest("Invalid configuration parameters".into())
})
}

async fn fetch_fees(
state: &web::Data<AppState>,
start: u64,
end: u64,
) -> Result<SequentialBlockFees, FeeError> {
state.fee_api.fees(start..=end).await.map_err(|e| {
error!("Error fetching sequential fees: {:?}", e);
FeeError::InternalError("Failed to fetch sequential fees".into())
})
}

async fn get_last_block_info(
state: &web::Data<AppState>,
seq_fees: &SequentialBlockFees,
) -> Result<(u64, DateTime<Utc>), FeeError> {
let last_block = seq_fees.last();
let last_block_time = state
.fee_api
.inner()
.get_block_time(last_block.height)
.await
.map_err(|e| {
error!("Error fetching last block time: {:?}", e);
FeeError::InternalError("Failed to fetch last block time".into())
})?
.ok_or_else(|| {
error!("Last block time not found");
FeeError::InternalError("Last block time not found".into())
})?;
info!("Last block time: {}", last_block_time);
Ok((last_block.height, last_block_time))
}

async fn calculate_fee_data(&self) -> Result<Vec<FeeDataPoint>, FeeError> {
let mut data = Vec::with_capacity(self.seq_fees.len());

for block_fee in self.seq_fees.iter() {
let fee_data = self.process_block_fee(block_fee).await?;
data.push(fee_data);
}

Ok(data)
}

async fn process_block_fee(&self, block_fee: &FeesAtHeight) -> Result<FeeDataPoint, FeeError> {
let current_fee_wei = calculate_blob_tx_fee(self.params.num_blobs, &block_fee.fees);
let short_fee_wei = self
.fetch_fee(block_fee.height, self.config.sma_periods.short)
.await?;
let long_fee_wei = self
.fetch_fee(block_fee.height, self.config.sma_periods.long)
.await?;

let acceptable = self
.sma_algo
.fees_acceptable(
self.params.num_blobs,
self.params.num_l2_blocks_behind,
block_fee.height,
)
.await
.map_err(|e| {
error!("Error determining fee acceptability: {:?}", e);
FeeError::InternalError("Failed to determine fee acceptability".into())
})?;

let block_gap = self.last_block_height - block_fee.height;
let block_time = self.last_block_time - Duration::from_secs(12 * block_gap);

let convert = |wei| format!("{:.4}", (wei as f64) / 1e18);

Ok(FeeDataPoint {
block_height: block_fee.height,
block_time: block_time.to_rfc3339(),
current_fee: convert(current_fee_wei),
short_fee: convert(short_fee_wei),
long_fee: convert(long_fee_wei),
acceptable,
})
}

async fn fetch_fee(
&self,
current_height: u64,
period: std::num::NonZeroU64,
) -> Result<u128, FeeError> {
let fees = self
.state
.fee_api
.fees(last_n_blocks(current_height, period))
.await
.map_err(|e| {
error!("Error fetching fees for period: {:?}", e);
FeeError::InternalError("Failed to fetch fees".into())
})?
.mean();
Ok(calculate_blob_tx_fee(self.params.num_blobs, &fees))
}

fn calculate_statistics(&self, data: &[FeeDataPoint]) -> FeeStats {
let total_blocks = data.len() as f64;
let acceptable_blocks = data.iter().filter(|d| d.acceptable).count() as f64;
let percentage_acceptable = if total_blocks > 0.0 {
(acceptable_blocks / total_blocks) * 100.0
} else {
0.0
};

let gap_sizes = self.compute_gap_sizes(data);
let percentile_95_gap_size = Self::calculate_percentile(&gap_sizes, 0.95);
let longest_unacceptable_streak = gap_sizes.into_iter().max().unwrap_or(0);

FeeStats {
percentage_acceptable,
percentile_95_gap_size,
longest_unacceptable_streak,
}
}

fn compute_gap_sizes(&self, data: &[FeeDataPoint]) -> Vec<u64> {
let mut gap_sizes = Vec::new();
let mut current_gap = 0;

for d in data {
if !d.acceptable {
current_gap += 1;
} else if current_gap > 0 {
gap_sizes.push(current_gap);
current_gap = 0;
}
}

if current_gap > 0 {
gap_sizes.push(current_gap);
}

gap_sizes
}

fn calculate_percentile(gaps: &[u64], percentile: f64) -> u64 {
if gaps.is_empty() {
return 0;
}

let mut sorted_gaps = gaps.to_vec();
sorted_gaps.sort_unstable();

let index = ((sorted_gaps.len() as f64) * percentile).ceil() as usize - 1;
sorted_gaps[index.min(sorted_gaps.len() - 1)]
}
}

pub async fn get_fees(state: web::Data<AppState>, params: web::Query<FeeParams>) -> impl Responder {
let handler = match FeeHandler::new(state.clone(), params.into_inner()).await {
Ok(h) => h,
Err(e) => return e.error_response(),
};

match handler.get_fees_response().await {
Ok(response) => HttpResponse::Ok().json(response),
Err(e) => e.error_response(),
}
}
pub mod block_time_info;
pub mod error;
pub mod fee;
pub mod index;
pub mod simulate;
30 changes: 30 additions & 0 deletions fee_algo_simulation/src/handlers/block_time_info.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use actix_web::HttpResponse;
use serde_json::json;
use services::fees::Api;
use tracing::error;

use crate::{state::AppState, utils::ETH_BLOCK_TIME};
pub async fn get_block_time_info(state: actix_web::web::Data<AppState>) -> HttpResponse {
let current_height = match state.fee_api.current_height().await {
Ok(h) => h,
Err(e) => {
error!("Error fetching current height: {:?}", e);
return HttpResponse::InternalServerError()
.body("Could not fetch current block height");
}
};

let last_block_time = match state.fee_api.inner().get_block_time(current_height).await {
Ok(Some(t)) => t,
_ => {
return HttpResponse::InternalServerError()
.body("Last block time not found".to_string());
}
};

HttpResponse::Ok().json(json!({
"last_block_height": current_height,
"last_block_time": last_block_time.to_rfc3339(),
"block_interval": ETH_BLOCK_TIME
}))
}
20 changes: 20 additions & 0 deletions fee_algo_simulation/src/handlers/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use actix_web::{HttpResponse, ResponseError};
use thiserror::Error;
use tracing::error;

#[derive(Error, Debug)]
pub enum FeeError {
#[error("Internal Server Error: {0}")]
InternalError(String),
#[error("Bad Request: {0}")]
BadRequest(String),
}

impl ResponseError for FeeError {
fn error_response(&self) -> HttpResponse {
match self {
FeeError::InternalError(msg) => HttpResponse::InternalServerError().body(msg.clone()),
FeeError::BadRequest(msg) => HttpResponse::BadRequest().body(msg.clone()),
}
}
}
Loading
Loading