From 3e9660563d1dc54bb355c1925bdc4596f395d2a5 Mon Sep 17 00:00:00 2001 From: Dmitriy Khomitskiy Date: Thu, 14 Nov 2024 16:53:17 -0600 Subject: [PATCH 1/5] RPC-468 refactoring code to make it simpler to make changes to individual calculations --- src/errors.rs | 19 + src/lib.rs | 9 + src/main.rs | 15 +- src/model.rs | 110 ++++ src/priority_fee.rs | 922 ++++++++++---------------------- src/priority_fee_calculation.rs | 613 +++++++++++++++++++++ src/rpc_server.rs | 146 ++--- 7 files changed, 1103 insertions(+), 731 deletions(-) create mode 100644 src/lib.rs create mode 100644 src/model.rs create mode 100644 src/priority_fee_calculation.rs diff --git a/src/errors.rs b/src/errors.rs index 5fff181..1820fe1 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,5 +1,24 @@ use jsonrpsee::types::{error::INVALID_PARAMS_CODE, ErrorObjectOwned}; +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum TransactionValidationError { + TransactionFailed, + TransactionMissing, + MessageMissing, + InvalidAccount, +} + +impl Into<&str> for TransactionValidationError { + fn into(self) -> &'static str { + match self { + TransactionValidationError::TransactionFailed => "txn_failed", + TransactionValidationError::TransactionMissing => "txn_missing", + TransactionValidationError::MessageMissing => "message_missing", + TransactionValidationError::InvalidAccount => "invalid_pubkey", + } + } +} + pub fn invalid_request(reason: &str) -> ErrorObjectOwned { ErrorObjectOwned::owned( INVALID_PARAMS_CODE, diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..df04f5b --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,9 @@ +pub mod errors; +pub mod grpc_consumer; +pub mod grpc_geyser; +pub mod model; +pub mod priority_fee; +pub mod priority_fee_calculation; +pub mod rpc_server; +pub mod slot_cache; +pub mod solana; diff --git a/src/main.rs b/src/main.rs index 45b2b37..6086b95 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,24 +1,15 @@ use std::{env, net::UdpSocket, sync::Arc}; -use crate::rpc_server::AtlasPriorityFeeEstimatorRpcServer; use cadence::{BufferedUdpMetricSink, QueuingMetricSink, StatsdClient}; use cadence_macros::set_global_default; use figment::{providers::Env, Figment}; -use grpc_geyser::GrpcGeyserImpl; use jsonrpsee::server::ServerBuilder; use jsonrpsee::server::middleware::http::ProxyGetRequestLayer; -use priority_fee::PriorityFeeTracker; -use rpc_server::AtlasPriorityFeeEstimator; use serde::Deserialize; use tracing::{error, info}; - -mod errors; -mod grpc_consumer; -mod grpc_geyser; -mod priority_fee; -mod rpc_server; -mod slot_cache; -mod solana; +use atlas_priority_fee_estimator::grpc_geyser::GrpcGeyserImpl; +use atlas_priority_fee_estimator::priority_fee::PriorityFeeTracker; +use atlas_priority_fee_estimator::rpc_server::{AtlasPriorityFeeEstimator, AtlasPriorityFeeEstimatorRpcServer}; #[derive(Debug, Deserialize, Clone)] struct EstimatorEnv { diff --git a/src/model.rs b/src/model.rs new file mode 100644 index 0000000..de17286 --- /dev/null +++ b/src/model.rs @@ -0,0 +1,110 @@ +use dashmap::DashMap; +use serde::{Deserialize, Serialize}; +use solana_sdk::clock::Slot; +use solana_sdk::pubkey::Pubkey; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub enum PriorityLevel { + Min, // 0th percentile + Low, // 25th percentile + Medium, // 50th percentile + High, // 75th percentile + VeryHigh, // 95th percentile + UnsafeMax, // 100th percentile + Default, // 50th percentile +} + +impl From for PriorityLevel { + fn from(s: String) -> Self { + match s.trim().to_uppercase().as_str() { + "NONE" => PriorityLevel::Min, + "LOW" => PriorityLevel::Low, + "MEDIUM" => PriorityLevel::Medium, + "HIGH" => PriorityLevel::High, + "VERY_HIGH" => PriorityLevel::VeryHigh, + "UNSAFE_MAX" => PriorityLevel::UnsafeMax, + _ => PriorityLevel::Default, + } + } +} + +impl Into for PriorityLevel { + fn into(self) -> Percentile { + match self { + PriorityLevel::Min => 0, + PriorityLevel::Low => 25, + PriorityLevel::Medium => 50, + PriorityLevel::High => 75, + PriorityLevel::VeryHigh => 95, + PriorityLevel::UnsafeMax => 100, + PriorityLevel::Default => 50, + } + } +} + +pub type Percentile = usize; + +#[derive(Deserialize, Serialize, Debug, Clone, Default)] +#[serde(rename_all(serialize = "camelCase", deserialize = "camelCase"))] +pub struct MicroLamportPriorityFeeEstimates { + pub min: f64, + pub low: f64, + pub medium: f64, + pub high: f64, + pub very_high: f64, + pub unsafe_max: f64, +} + +#[derive(Debug, Clone)] +pub struct Fees { + pub(crate) non_vote_fees: Vec, + pub(crate) vote_fees: Vec, +} + +impl Fees { + pub fn new(fee: f64, is_vote: bool) -> Self { + if is_vote { + Self { + vote_fees: vec![fee], + non_vote_fees: vec![], + } + } else { + Self { + vote_fees: vec![], + non_vote_fees: vec![fee], + } + } + } + + pub fn add_fee(&mut self, fee: f64, is_vote: bool) { + if is_vote { + self.vote_fees.push(fee); + } else { + self.non_vote_fees.push(fee); + } + } +} + +#[derive(Debug, Clone)] +pub struct SlotPriorityFees { + pub(crate) slot: Slot, + pub(crate) fees: Fees, + pub(crate) account_fees: DashMap, +} + +impl SlotPriorityFees { + pub(crate) fn new(slot: Slot, accounts: Vec, priority_fee: u64, is_vote: bool) -> Self { + let account_fees = DashMap::new(); + let fees = Fees::new(priority_fee as f64, is_vote); + for account in accounts { + account_fees.insert(account, fees.clone()); + } + if is_vote { + Self { slot, fees, account_fees } + } else { + Self { slot, fees, account_fees } + } + } +} + +pub type PriorityFeesBySlot = DashMap; diff --git a/src/priority_fee.rs b/src/priority_fee.rs index 516dbbf..30b29a6 100644 --- a/src/priority_fee.rs +++ b/src/priority_fee.rs @@ -1,126 +1,32 @@ +use crate::errors::TransactionValidationError; use crate::grpc_consumer::GrpcConsumer; +use crate::model::{Fees, MicroLamportPriorityFeeEstimates, PriorityFeesBySlot, SlotPriorityFees}; +use crate::priority_fee_calculation::Calculations; +use crate::priority_fee_calculation::Calculations::Calculation1; use crate::rpc_server::get_recommended_fee; use crate::slot_cache::SlotCache; use cadence_macros::statsd_count; use cadence_macros::statsd_gauge; use dashmap::DashMap; -use serde::Deserialize; -use serde::Serialize; use solana::storage::confirmed_block::Message; use solana_program_runtime::compute_budget::ComputeBudget; use solana_program_runtime::prioritization_fee::PrioritizationFeeDetails; use solana_sdk::instruction::CompiledInstruction; use solana_sdk::transaction::TransactionError; use solana_sdk::{pubkey::Pubkey, slot_history::Slot}; -use std::collections::HashMap; use std::sync::Arc; -use std::time::{Duration, Instant}; -use tokio::sync::mpsc::{channel, Receiver, Sender}; -use tracing::{debug, error}; +use std::time::Duration; +use tracing::error; use yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof; use yellowstone_grpc_proto::geyser::{SubscribeUpdate, SubscribeUpdateTransactionInfo}; use yellowstone_grpc_proto::prelude::{MessageHeader, Transaction, TransactionStatusMeta}; use yellowstone_grpc_proto::solana; -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -pub enum PriorityLevel { - Min, // 0th percentile - Low, // 25th percentile - Medium, // 50th percentile - High, // 75th percentile - VeryHigh, // 95th percentile - UnsafeMax, // 100th percentile - Default, // 50th percentile -} - -impl From for PriorityLevel { - fn from(s: String) -> Self { - match s.as_str() { - "NONE" => PriorityLevel::Min, - "LOW" => PriorityLevel::Low, - "MEDIUM" => PriorityLevel::Medium, - "HIGH" => PriorityLevel::High, - "VERY_HIGH" => PriorityLevel::VeryHigh, - "UNSAFE_MAX" => PriorityLevel::UnsafeMax, - _ => PriorityLevel::Default, - } - } -} - -impl Into for PriorityLevel { - fn into(self) -> Percentile { - match self { - PriorityLevel::Min => 0, - PriorityLevel::Low => 25, - PriorityLevel::Medium => 50, - PriorityLevel::High => 75, - PriorityLevel::VeryHigh => 95, - PriorityLevel::UnsafeMax => 100, - PriorityLevel::Default => 50, - } - } -} - -type Percentile = usize; - -#[derive(Deserialize, Serialize, Debug, Clone, Default)] -#[serde(rename_all(serialize = "camelCase", deserialize = "camelCase"))] -pub struct MicroLamportPriorityFeeEstimates { - pub min: f64, - pub low: f64, - pub medium: f64, - pub high: f64, - pub very_high: f64, - pub unsafe_max: f64, -} - -#[derive(Debug, Clone)] -struct SlotPriorityFees { - fees: Fees, - account_fees: DashMap, -} -type PriorityFeesBySlot = DashMap; - -impl SlotPriorityFees { - fn new(accounts: Vec, priority_fee: u64, is_vote: bool) -> Self { - let account_fees = DashMap::new(); - let fees = Fees::new(priority_fee as f64, is_vote); - for account in accounts { - account_fees.insert(account, fees.clone()); - } - if is_vote { - Self { fees, account_fees } - } else { - Self { fees, account_fees } - } - } -} - #[derive(Debug, Clone)] pub struct PriorityFeeTracker { priority_fees: Arc, compute_budget: ComputeBudget, slot_cache: SlotCache, - sampling_sender: Sender<(Vec, bool, bool, Option)>, -} - -#[derive(Debug, Copy, Clone, PartialEq)] -enum TransactionValidationError { - TransactionFailed, - TransactionMissing, - MessageMissing, - InvalidAccount, -} - -impl Into<&str> for TransactionValidationError { - fn into(self) -> &'static str { - match self { - TransactionValidationError::TransactionFailed => "txn_failed", - TransactionValidationError::TransactionMissing => "txn_missing", - TransactionValidationError::MessageMissing => "message_missing", - TransactionValidationError::InvalidAccount => "invalid_pubkey", - } - } } fn extract_from_meta( @@ -159,7 +65,7 @@ fn extract_from_transaction( .ok_or(TransactionValidationError::MessageMissing)?; let is_vote = transaction.is_vote; - Result::Ok((message, writable_accounts, is_vote)) + Ok((message, writable_accounts, is_vote)) } fn extract_from_message( @@ -299,199 +205,66 @@ impl GrpcConsumer for PriorityFeeTracker { impl PriorityFeeTracker { pub fn new(slot_cache_length: usize) -> Self { - let (sampling_txn, sampling_rxn) = channel::<(Vec, bool, bool, Option)>(100); - let tracker = Self { priority_fees: Arc::new(DashMap::new()), slot_cache: SlotCache::new(slot_cache_length), compute_budget: ComputeBudget::default(), - sampling_sender: sampling_txn, }; - tracker.poll_fees(sampling_rxn); + tracker.poll_fees(); tracker } - fn poll_fees(&self, mut sampling_rxn: Receiver<(Vec, bool, bool, Option)>) { - { - let priority_fee_tracker = self.clone(); - // task to run global fee comparison every 1 second - tokio::spawn(async move { - loop { - tokio::time::sleep(Duration::from_millis(1_000)).await; - priority_fee_tracker.record_general_fees(); - } - }); - } - - { - let priority_fee_tracker = self.clone(); - // task to poll the queue and run comparison to see what is the diff between algos - tokio::spawn(async move { - loop { - match sampling_rxn.recv().await { - Some((accounts, include_vote, include_empty_slots, lookback_period)) => priority_fee_tracker - .record_specific_fees(accounts, include_vote, include_empty_slots, lookback_period), - _ => {} - } - } - }); - } + fn poll_fees(&self) { + let priority_fee_tracker = self.clone(); + // task to run global fee comparison every 1 second + tokio::spawn(async move { + loop { + tokio::time::sleep(Duration::from_millis(1_000)).await; + priority_fee_tracker.record_general_fees(); + } + }); } fn record_general_fees(&self) { - let global_fees = self.calculation1(&vec![], false, false, &None); - statsd_gauge!( - "min_priority_fee", - global_fees.min as u64, - "account" => "none" - ); - statsd_gauge!("low_priority_fee", global_fees.low as u64, "account" => "none"); - statsd_gauge!( - "medium_priority_fee", - global_fees.medium as u64, - "account" => "none" - ); - statsd_gauge!( - "high_priority_fee", - global_fees.high as u64, - "account" => "none" - ); - statsd_gauge!( - "very_high_priority_fee", - global_fees.very_high as u64, - "account" => "none" - ); - statsd_gauge!( - "unsafe_max_priority_fee", - global_fees.unsafe_max as u64, - "account" => "none" - ); - statsd_gauge!( - "recommended_priority_fee", - get_recommended_fee(global_fees) as u64, - "account" => "none" - ); - } - - fn record_specific_fees( - &self, - accounts: Vec, - include_vote: bool, - include_empty_slots: bool, - lookback_period: Option, - ) { - let old_fee = self.calculation1(&accounts, include_vote, include_empty_slots, &lookback_period); - let new_fee = self.calculation2(&accounts, include_vote, include_empty_slots, &lookback_period); - let new_fee_last = self.calculation2(&accounts, include_vote, include_empty_slots, &Some(1)); - - statsd_gauge!( - "min_priority_fee", - old_fee.min, - "account" => "spec" - ); - statsd_gauge!( - "min_priority_fee_new", - new_fee.min, - "account" => "spec" - ); - statsd_gauge!( - "min_priority_fee_last", - new_fee_last.min, - "account" => "spec" - ); - - statsd_gauge!("low_priority_fee", - old_fee.low, - "account" => "spec" - ); - statsd_gauge!("low_priority_fee_new", - new_fee.low, - "account" => "spec" - ); - statsd_gauge!("low_priority_fee_last", - new_fee_last.low, - "account" => "spec" - ); - - statsd_gauge!( - "medium_priority_fee", - old_fee.medium, - "account" => "spec" - ); - statsd_gauge!( - "medium_priority_fee_new", - new_fee.medium, - "account" => "spec" - ); - statsd_gauge!( - "medium_priority_fee_last", - new_fee_last.medium, - "account" => "spec" - ); - - statsd_gauge!( - "high_priority_fee", - old_fee.high, - "account" => "spec" - ); - statsd_gauge!( - "high_priority_fee_new", - new_fee.high, - "account" => "spec" - ); - statsd_gauge!( - "high_priority_fee_last", - new_fee_last.high, - "account" => "spec" - ); - - statsd_gauge!( - "very_high_priority_fee", - old_fee.very_high, - "account" => "spec" - ); - statsd_gauge!( - "very_high_priority_fee_new", - new_fee.very_high, - "account" => "spec" - ); - statsd_gauge!( - "very_high_priority_fee_last", - new_fee_last.very_high, - "account" => "spec" - ); - - statsd_gauge!( - "unsafe_max_priority_fee", - old_fee.unsafe_max, - "account" => "spec" - ); - statsd_gauge!( - "unsafe_max_priority_fee_new", - new_fee.unsafe_max, - "account" => "spec" - ); - statsd_gauge!( - "very_high_priority_fee_last", - new_fee_last.unsafe_max, - "account" => "spec" - ); - - statsd_gauge!( - "recommended_priority_fee", - get_recommended_fee(old_fee), - "account" => "spec" - ); - statsd_gauge!( - "recommended_priority_fee_new", - get_recommended_fee(new_fee), - "account" => "spec" - ); - statsd_gauge!( - "recommended_priority_fee_last", - get_recommended_fee(new_fee_last), - "account" => "spec" - ); + let global_fees = self.calculate_priority_fee_details(&Calculation1 { + accounts: &vec![], + include_vote: false, + include_empty_slots: false, + lookback_period: &None, + }); + if let Ok(global_fees) = global_fees { + statsd_gauge!( + "min_priority_fee", + global_fees.min as u64, + "account" => "none" + ); + statsd_gauge!("low_priority_fee", global_fees.low as u64, "account" => "none"); + statsd_gauge!( + "medium_priority_fee", + global_fees.medium as u64, + "account" => "none" + ); + statsd_gauge!( + "high_priority_fee", + global_fees.high as u64, + "account" => "none" + ); + statsd_gauge!( + "very_high_priority_fee", + global_fees.very_high as u64, + "account" => "none" + ); + statsd_gauge!( + "unsafe_max_priority_fee", + global_fees.unsafe_max as u64, + "account" => "none" + ); + statsd_gauge!( + "recommended_priority_fee", + get_recommended_fee(global_fees) as u64, + "account" => "none" + ); + } } pub fn push_priority_fee_for_txn( @@ -505,8 +278,10 @@ impl PriorityFeeTracker { // for removal later let slot_to_remove = self.slot_cache.push_pop(slot); if !self.priority_fees.contains_key(&slot) { - self.priority_fees - .insert(slot, SlotPriorityFees::new(accounts, priority_fee, is_vote)); + self.priority_fees.insert( + slot, + SlotPriorityFees::new(slot, accounts, priority_fee, is_vote), + ); } else { // update the slot priority fees self.priority_fees.entry(slot).and_modify(|priority_fees| { @@ -525,299 +300,14 @@ impl PriorityFeeTracker { } } - // TODO: DKH - both algos should probably be in some enum (like algo1, algo2) and be passed to - // this method instead of sending a bool flag. I'll refactor this in next iteration. already too many changes - pub fn get_priority_fee_estimates( - &self, - accounts: Vec, - include_vote: bool, - include_empty_slots: bool, - lookback_period: Option, - calculation1: bool, - ) -> MicroLamportPriorityFeeEstimates { - let start = Instant::now(); - let micro_lamport_priority_fee_estimates = if calculation1 { - self.calculation1(&accounts, include_vote, include_empty_slots, &lookback_period) - } else { - self.calculation2(&accounts, include_vote, include_empty_slots, &lookback_period) - }; - - let version = if calculation1 { "v1" } else { "v2" }; - statsd_gauge!( - "get_priority_fee_estimates_time", - start.elapsed().as_nanos() as u64, - "version" => &version - ); - statsd_count!( - "get_priority_fee_calculation_version", - 1, - "version" => &version - ); - if let Err(e) = self.sampling_sender.try_send(( - accounts.to_owned(), - include_vote, - include_empty_slots, - lookback_period.to_owned(), - )) { - debug!("Did not add sample for calculation, {:?}", e); - } - - micro_lamport_priority_fee_estimates - } - - /* - Algo1: given the list of accounts the algorithm will: - 1. collect all the transactions fees over n slots - 2. collect all the transaction fees for all the accounts specified over n slots - 3. will calculate the percentile distributions for each of two groups - 4. will choose the highest value from each percentile between two groups - */ - fn calculation1( - &self, - accounts: &Vec, - include_vote: bool, - include_empty_slots: bool, - lookback_period: &Option, - ) -> MicroLamportPriorityFeeEstimates { - let mut account_fees = vec![]; - let mut transaction_fees = vec![]; - for (i, slot_priority_fees) in self.priority_fees.iter().enumerate() { - if let Some(lookback_period) = lookback_period { - if i >= *lookback_period as usize { - break; - } - } - if include_vote { - transaction_fees.extend_from_slice(&slot_priority_fees.fees.vote_fees); - } - transaction_fees.extend_from_slice(&slot_priority_fees.fees.non_vote_fees); - for account in accounts { - let account_priority_fees = slot_priority_fees.account_fees.get(account); - if let Some(account_priority_fees) = account_priority_fees { - if include_vote { - account_fees.extend_from_slice(&account_priority_fees.vote_fees); - } - account_fees.extend_from_slice(&account_priority_fees.non_vote_fees); - } - } - } - if include_empty_slots { - let lookback = lookback_period.map(|v| v as usize).unwrap_or(self.priority_fees.len()); - let account_max_size = account_fees.len().max(lookback); - let transaction_max_size = transaction_fees.len().max(lookback); - // if there are less data than number of slots - append 0s for up to number of slots so we don't overestimate the values - account_fees.resize(account_max_size, 0f64); - transaction_fees.resize(transaction_max_size, 0f64); - } - - let micro_lamport_priority_fee_estimates = MicroLamportPriorityFeeEstimates { - min: max_percentile(&mut account_fees, &mut transaction_fees, 0), - low: max_percentile(&mut account_fees, &mut transaction_fees, 25), - medium: max_percentile(&mut account_fees, &mut transaction_fees, 50), - high: max_percentile(&mut account_fees, &mut transaction_fees, 75), - very_high: max_percentile(&mut account_fees, &mut transaction_fees, 95), - unsafe_max: max_percentile(&mut account_fees, &mut transaction_fees, 100), - }; - - micro_lamport_priority_fee_estimates - } - - /* - Algo2: given the list of accounts the algorithm will: - 1. collect all the transactions fees over n slots - 2. for each specified account collect the fees and calculate the percentiles - 4. choose maximum values for each percentile between all transactions and each account - */ - fn calculation2( + pub fn calculate_priority_fee_details( &self, - accounts: &Vec, - include_vote: bool, - include_empty_slots: bool, - lookback_period: &Option, - ) -> MicroLamportPriorityFeeEstimates { - let mut slots_vec = Vec::with_capacity(self.slot_cache.len()); - self.slot_cache.copy_slots(&mut slots_vec); - slots_vec.sort(); - slots_vec.reverse(); - - let slots_vec = slots_vec; - - let lookback = calculate_lookback_size(&lookback_period, slots_vec.len()); - - let mut fees = Vec::with_capacity(slots_vec.len()); - let mut micro_lamport_priority_fee_estimates = MicroLamportPriorityFeeEstimates::default(); - - for slot in &slots_vec[..lookback] { - if let Some(slot_priority_fees) = self.priority_fees.get(slot) { - if include_vote { - fees.extend_from_slice(&slot_priority_fees.fees.vote_fees); - } - fees.extend_from_slice(&slot_priority_fees.fees.non_vote_fees); - } - } - micro_lamport_priority_fee_estimates = - estimate_max_values(&mut fees, micro_lamport_priority_fee_estimates); - - for account in accounts { - fees.clear(); - let mut zero_slots: usize = 0; - for slot in &slots_vec[..lookback] { - if let Some(slot_priority_fees) = self.priority_fees.get(slot) { - let account_priority_fees = slot_priority_fees.account_fees.get(account); - if let Some(account_priority_fees) = account_priority_fees { - if include_vote { - fees.extend_from_slice(&account_priority_fees.vote_fees); - } - fees.extend_from_slice(&account_priority_fees.non_vote_fees); - } - else { - zero_slots += 1; - } - } - else { - zero_slots += 1; - } - } - if include_empty_slots { - // for all empty slot we need to add a 0 - fees.resize(fees.len() + zero_slots, 0f64); - } - micro_lamport_priority_fee_estimates = - estimate_max_values(&mut fees, micro_lamport_priority_fee_estimates); - } - micro_lamport_priority_fee_estimates + calculation: &Calculations, + ) -> anyhow::Result { + calculation.get_priority_fee_estimates(&self.priority_fees) } } -fn estimate_max_values( - mut fees: &mut Vec, - mut estimates: MicroLamportPriorityFeeEstimates, -) -> MicroLamportPriorityFeeEstimates { - let vals: HashMap = percentile( - &mut fees, - &[ - PriorityLevel::Min.into(), - PriorityLevel::Low.into(), - PriorityLevel::Medium.into(), - PriorityLevel::High.into(), - PriorityLevel::VeryHigh.into(), - PriorityLevel::UnsafeMax.into(), - ], - ); - estimates.min = vals - .get(&PriorityLevel::Min.into()) - .unwrap_or(&estimates.min) - .max(estimates.min); - estimates.low = vals - .get(&PriorityLevel::Low.into()) - .unwrap_or(&estimates.low) - .max(estimates.low); - estimates.medium = vals - .get(&PriorityLevel::Medium.into()) - .unwrap_or(&estimates.medium) - .max(estimates.medium); - estimates.high = vals - .get(&PriorityLevel::High.into()) - .unwrap_or(&estimates.high) - .max(estimates.high); - estimates.very_high = vals - .get(&PriorityLevel::VeryHigh.into()) - .unwrap_or(&estimates.very_high) - .max(estimates.very_high); - estimates.unsafe_max = vals - .get(&PriorityLevel::UnsafeMax.into()) - .unwrap_or(&estimates.unsafe_max) - .max(estimates.unsafe_max); - estimates -} - -fn max(a: f64, b: f64) -> f64 { - if a > b { - a - } else { - b - } -} - -fn calculate_lookback_size(pref_num_slots: &Option, max_available_slots: usize) -> usize { - max_available_slots.min( - pref_num_slots - .map(|v| v as usize) - .unwrap_or(max_available_slots), - ) -} - -#[derive(Debug, Clone)] -pub struct Fees { - non_vote_fees: Vec, - vote_fees: Vec, -} - -impl Fees { - pub fn new(fee: f64, is_vote: bool) -> Self { - if is_vote { - Self { - vote_fees: vec![fee], - non_vote_fees: vec![], - } - } else { - Self { - vote_fees: vec![], - non_vote_fees: vec![fee], - } - } - } - - pub fn add_fee(&mut self, fee: f64, is_vote: bool) { - if is_vote { - self.vote_fees.push(fee); - } else { - self.non_vote_fees.push(fee); - } - } -} - -fn max_percentile( - account_fees: &mut Vec, - transaction_fees: &mut Vec, - percentile_value: Percentile, -) -> f64 { - let results1 = percentile(account_fees, &[percentile_value]); - let results2 = percentile(transaction_fees, &[percentile_value]); - max( - *results1.get(&percentile_value).unwrap_or(&0f64), - *results2.get(&percentile_value).unwrap_or(&0f64), - ) -} - -// pulled from here - https://www.calculatorsoup.com/calculators/statistics/percentile-calculator.php -// couldn't find any good libraries that worked -fn percentile(values: &mut Vec, percentiles: &[Percentile]) -> HashMap { - if values.is_empty() { - return HashMap::with_capacity(0); - } - - values.sort_by(|a, b| a.partial_cmp(b).unwrap()); - let n = values.len() as f64; - - percentiles.into_iter().fold( - HashMap::with_capacity(percentiles.len()), - |mut data, &percentile| { - let r = (percentile as f64 / 100.0) * (n - 1.0) + 1.0; - - let val = if r.fract() == 0.0 { - values[r as usize - 1] - } else { - let ri = r.trunc() as usize - 1; - let rf = r.fract(); - values[ri] + rf * (values[ri + 1] - values[ri]) - }; - data.insert(percentile, val); - data - }, - ) -} - #[cfg(test)] mod tests { use super::*; @@ -835,6 +325,42 @@ mod tests { set_global_default(client) } + fn calculation1( + accounts: &Vec, + include_vote: bool, + include_empty_slot: bool, + lookback_period: &Option, + tracker: &PriorityFeeTracker, + ) -> MicroLamportPriorityFeeEstimates { + let calc = Calculations::new_calculation1( + accounts, + include_vote, + include_empty_slot, + lookback_period, + ); + tracker + .calculate_priority_fee_details(&calc) + .expect(format!("estimates for calc1 to be valid with {:?}", calc).as_str()) + } + + fn calculation2( + accounts: &Vec, + include_vote: bool, + include_empty_slot: bool, + lookback_period: &Option, + tracker: &PriorityFeeTracker, + ) -> MicroLamportPriorityFeeEstimates { + let calc = Calculations::new_calculation2( + accounts, + include_vote, + include_empty_slot, + lookback_period, + ); + tracker + .calculate_priority_fee_details(&calc) + .expect(format!("estimates for calc2 to be valid with {:?}", calc).as_str()) + } + #[tokio::test] async fn test_specific_fee_estimates() { init_metrics(); @@ -857,7 +383,13 @@ mod tests { } // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = tracker.calculation1(&vec![accounts.get(0).unwrap().clone()], false, false, &None); + let estimates = calculation1( + &vec![accounts.get(0).unwrap().clone()], + false, + false, + &None, + &tracker, + ); // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value let expected_min_fee = 0.0; let expected_low_fee = 25.0; @@ -872,9 +404,14 @@ mod tests { assert_eq!(estimates.very_high, expected_very_high_fee); assert_eq!(estimates.unsafe_max, expected_max_fee); - // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = tracker.calculation1(&vec![accounts.get(0).unwrap().clone()], false, false, &Some(150)); + let estimates = calculation1( + &vec![accounts.get(0).unwrap().clone()], + false, + false, + &Some(150), + &tracker, + ); // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value let expected_min_fee = 0.0; let expected_low_fee = 25.0; @@ -890,7 +427,13 @@ mod tests { assert_eq!(estimates.unsafe_max, expected_max_fee); // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = tracker.calculation1(&vec![accounts.get(0).unwrap().clone()], false, true, &None); + let estimates = calculation1( + &vec![accounts.get(0).unwrap().clone()], + false, + true, + &None, + &tracker, + ); // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value let expected_min_fee = 0.0; let expected_low_fee = 25.0; @@ -905,9 +448,14 @@ mod tests { assert_eq!(estimates.very_high, expected_very_high_fee); assert_eq!(estimates.unsafe_max, expected_max_fee); - // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = tracker.calculation1(&vec![accounts.get(0).unwrap().clone()], false, true, &Some(150)); + let estimates = calculation1( + &vec![accounts.get(0).unwrap().clone()], + false, + true, + &Some(150), + &tracker, + ); // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value let expected_min_fee = 0.0; let expected_low_fee = 0.0; @@ -945,7 +493,13 @@ mod tests { } // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = tracker.calculation2(&vec![accounts.get(0).unwrap().clone()], false, false, &None); + let estimates = calculation2( + &vec![accounts.get(0).unwrap().clone()], + false, + false, + &None, + &tracker, + ); // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value let expected_min_fee = 0.0; let expected_low_fee = 25.0; @@ -960,9 +514,14 @@ mod tests { assert_eq!(estimates.very_high, expected_very_high_fee); assert_eq!(estimates.unsafe_max, expected_max_fee); - // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = tracker.calculation2(&vec![accounts.get(0).unwrap().clone()], false, false, &Some(150)); + let estimates = calculation2( + &vec![accounts.get(0).unwrap().clone()], + false, + false, + &Some(150), + &tracker, + ); // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value let expected_min_fee = 0.0; let expected_low_fee = 25.0; @@ -978,7 +537,13 @@ mod tests { assert_eq!(estimates.unsafe_max, expected_max_fee); // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = tracker.calculation2(&vec![accounts.get(0).unwrap().clone()], false, true, &None); + let estimates = calculation2( + &vec![accounts.get(0).unwrap().clone()], + false, + true, + &None, + &tracker, + ); // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value let expected_min_fee = 0.0; let expected_low_fee = 25.0; @@ -993,9 +558,14 @@ mod tests { assert_eq!(estimates.very_high, expected_very_high_fee); assert_eq!(estimates.unsafe_max, expected_max_fee); - // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = tracker.calculation2(&vec![accounts.get(0).unwrap().clone()], false, true, &Some(150)); + let estimates = calculation2( + &vec![accounts.get(0).unwrap().clone()], + false, + true, + &Some(150), + &tracker, + ); // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value let expected_min_fee = 0.0; let expected_low_fee = 25.0; @@ -1035,7 +605,13 @@ mod tests { } // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = tracker.calculation1(&vec![accounts.get(0).unwrap().clone()], false, false, &None); + let estimates = calculation1( + &vec![accounts.get(0).unwrap().clone()], + false, + false, + &None, + &tracker, + ); let expected_min_fee = 0.0; let expected_low_fee = 25.0; let expected_medium_fee = 50.0; @@ -1049,9 +625,14 @@ mod tests { assert_eq!(estimates.very_high, expected_very_high_fee); assert_eq!(estimates.unsafe_max, expected_max_fee); - // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = tracker.calculation1(&vec![accounts.get(0).unwrap().clone()], false, false, &Some(150)); + let estimates = calculation1( + &vec![accounts.get(0).unwrap().clone()], + false, + false, + &Some(150), + &tracker, + ); let expected_min_fee = 0.0; let expected_low_fee = 25.0; let expected_medium_fee = 50.0; @@ -1065,9 +646,14 @@ mod tests { assert_eq!(estimates.very_high, expected_very_high_fee); assert_eq!(estimates.unsafe_max, expected_max_fee); - // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = tracker.calculation1(&vec![accounts.get(0).unwrap().clone()], false, true, &None); + let estimates = calculation1( + &vec![accounts.get(0).unwrap().clone()], + false, + true, + &None, + &tracker, + ); let expected_min_fee = 0.0; let expected_low_fee = 25.0; let expected_medium_fee = 50.0; @@ -1081,9 +667,14 @@ mod tests { assert_eq!(estimates.very_high, expected_very_high_fee); assert_eq!(estimates.unsafe_max, expected_max_fee); - // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = tracker.calculation1(&vec![accounts.get(0).unwrap().clone()], false, true, &Some(150)); + let estimates = calculation1( + &vec![accounts.get(0).unwrap().clone()], + false, + true, + &Some(150), + &tracker, + ); let expected_min_fee = 0.0; let expected_low_fee = 0.0; let expected_medium_fee = 25.5; @@ -1121,7 +712,13 @@ mod tests { } // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = tracker.calculation2(&vec![accounts.get(0).unwrap().clone()], false, false, &None); + let estimates = calculation2( + &vec![accounts.get(0).unwrap().clone()], + false, + false, + &None, + &tracker, + ); let expected_min_fee = 0.0; let expected_low_fee = 25.0; let expected_medium_fee = 50.0; @@ -1135,9 +732,14 @@ mod tests { assert_eq!(estimates.very_high, expected_very_high_fee); assert_eq!(estimates.unsafe_max, expected_max_fee); - // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = tracker.calculation2(&vec![accounts.get(0).unwrap().clone()], false, false, &Some(150)); + let estimates = calculation2( + &vec![accounts.get(0).unwrap().clone()], + false, + false, + &Some(150), + &tracker, + ); let expected_min_fee = 0.0; let expected_low_fee = 25.0; let expected_medium_fee = 50.0; @@ -1151,9 +753,14 @@ mod tests { assert_eq!(estimates.very_high, expected_very_high_fee); assert_eq!(estimates.unsafe_max, expected_max_fee); - // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = tracker.calculation2(&vec![accounts.get(0).unwrap().clone()], false, true, &None); + let estimates = calculation2( + &vec![accounts.get(0).unwrap().clone()], + false, + true, + &None, + &tracker, + ); let expected_min_fee = 0.0; let expected_low_fee = 25.0; let expected_medium_fee = 50.0; @@ -1167,9 +774,14 @@ mod tests { assert_eq!(estimates.very_high, expected_very_high_fee); assert_eq!(estimates.unsafe_max, expected_max_fee); - // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = tracker.calculation2(&vec![accounts.get(0).unwrap().clone()], false, true, &Some(150)); + let estimates = calculation2( + &vec![accounts.get(0).unwrap().clone()], + false, + true, + &Some(150), + &tracker, + ); let expected_min_fee = 0.0; let expected_low_fee = 25.0; let expected_medium_fee = 50.0; @@ -1182,7 +794,6 @@ mod tests { assert_eq!(estimates.high, expected_high_fee); assert_eq!(estimates.very_high, expected_very_high_fee); assert_eq!(estimates.unsafe_max, expected_max_fee); - } #[tokio::test] @@ -1208,15 +819,25 @@ mod tests { // Simulate adding the fixed fees as both account-specific and transaction fees for (i, fee) in fees.clone().into_iter().enumerate() { if 0 == i.rem_euclid(10usize) { - tracker.push_priority_fee_for_txn(i as Slot, vec![account_unrelated], fee as u64, false); - } - else { + tracker.push_priority_fee_for_txn( + i as Slot, + vec![account_unrelated], + fee as u64, + false, + ); + } else { tracker.push_priority_fee_for_txn(i as Slot, accounts.clone(), fee as u64, false); } } // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = tracker.calculation1(&vec![accounts.get(0).unwrap().clone()], false, false, &None); + let estimates = calculation1( + &vec![accounts.get(0).unwrap().clone()], + false, + false, + &None, + &tracker, + ); let expected_low_fee = 25.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; @@ -1227,7 +848,13 @@ mod tests { assert_ne!(estimates.very_high, expected_very_high_fee); // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = tracker.calculation1(&vec![accounts.get(0).unwrap().clone()], false, false, &Some(150)); + let estimates = calculation1( + &vec![accounts.get(0).unwrap().clone()], + false, + false, + &Some(150), + &tracker, + ); let expected_low_fee = 25.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; @@ -1238,7 +865,13 @@ mod tests { assert_ne!(estimates.very_high, expected_very_high_fee); // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = tracker.calculation1(&vec![accounts.get(0).unwrap().clone()], false, true, &None); + let estimates = calculation1( + &vec![accounts.get(0).unwrap().clone()], + false, + true, + &None, + &tracker, + ); let expected_low_fee = 25.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; @@ -1249,7 +882,13 @@ mod tests { assert_ne!(estimates.very_high, expected_very_high_fee); // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = tracker.calculation1(&vec![accounts.get(0).unwrap().clone()], false, true, &Some(150)); + let estimates = calculation1( + &vec![accounts.get(0).unwrap().clone()], + false, + true, + &Some(150), + &tracker, + ); let expected_low_fee = 25.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; @@ -1260,7 +899,6 @@ mod tests { assert_ne!(estimates.very_high, expected_very_high_fee); } - #[tokio::test] async fn test_with_many_slots_broken_v2() { // same test as above but with an extra slot to throw off the value @@ -1286,15 +924,25 @@ mod tests { // Simulate adding the fixed fees as both account-specific and transaction fees for (i, fee) in fees.clone().into_iter().enumerate() { if 0 == i.rem_euclid(10usize) { - tracker.push_priority_fee_for_txn(i as Slot, vec![account_unrelated], fee as u64, false); - } - else { + tracker.push_priority_fee_for_txn( + i as Slot, + vec![account_unrelated], + fee as u64, + false, + ); + } else { tracker.push_priority_fee_for_txn(i as Slot, accounts.clone(), fee as u64, false); } } // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = tracker.calculation2(&vec![accounts.get(0).unwrap().clone()], false, false, &None); + let estimates = calculation2( + &vec![accounts.get(0).unwrap().clone()], + false, + false, + &None, + &tracker, + ); let expected_low_fee = 25.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; @@ -1305,7 +953,13 @@ mod tests { assert_ne!(estimates.very_high, expected_very_high_fee); // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = tracker.calculation2(&vec![accounts.get(0).unwrap().clone()], false, false, &Some(150)); + let estimates = calculation2( + &vec![accounts.get(0).unwrap().clone()], + false, + false, + &Some(150), + &tracker, + ); let expected_low_fee = 25.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; @@ -1316,7 +970,13 @@ mod tests { assert_ne!(estimates.very_high, expected_very_high_fee); // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = tracker.calculation2(&vec![accounts.get(0).unwrap().clone()], false, true, &None); + let estimates = calculation2( + &vec![accounts.get(0).unwrap().clone()], + false, + true, + &None, + &tracker, + ); let expected_low_fee = 25.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; @@ -1327,7 +987,13 @@ mod tests { assert_ne!(estimates.very_high, expected_very_high_fee); // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = tracker.calculation2(&vec![accounts.get(0).unwrap().clone()], false, true, &Some(150)); + let estimates = calculation2( + &vec![accounts.get(0).unwrap().clone()], + false, + true, + &Some(150), + &tracker, + ); let expected_low_fee = 25.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; @@ -1336,7 +1002,6 @@ mod tests { assert_ne!(estimates.medium, expected_medium_fee); assert_ne!(estimates.high, expected_high_fee); assert_ne!(estimates.very_high, expected_very_high_fee); - } #[tokio::test] @@ -1364,7 +1029,7 @@ mod tests { // Now test the fee estimates for a known priority level, let's say medium (50th percentile) let v = vec![account_1]; - let estimates = tracker.calculation1(&v, false, false, &None); + let estimates = calculation1(&v, false, false, &None, &tracker); // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value let expected_min_fee = 0.0; let expected_low_fee = 25.0; @@ -1379,8 +1044,7 @@ mod tests { assert_eq!(estimates.very_high, expected_very_high_fee); assert_eq!(estimates.unsafe_max, expected_max_fee); - - let estimates = tracker.calculation1(&v, false, false, &Some(150)); + let estimates = calculation1(&v, false, false, &Some(150), &tracker); // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value let expected_min_fee = 0.0; let expected_low_fee = 25.0; @@ -1395,8 +1059,7 @@ mod tests { assert_eq!(estimates.very_high, expected_very_high_fee); assert_eq!(estimates.unsafe_max, expected_max_fee); - - let estimates = tracker.calculation1(&v, false, true, &None); + let estimates = calculation1(&v, false, true, &None, &tracker); // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value let expected_min_fee = 0.0; let expected_low_fee = 25.0; @@ -1411,8 +1074,7 @@ mod tests { assert_eq!(estimates.very_high, expected_very_high_fee); assert_eq!(estimates.unsafe_max, expected_max_fee); - - let estimates = tracker.calculation1(&v, false, true, &Some(150)); + let estimates = calculation1(&v, false, true, &Some(150), &tracker); // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value let expected_min_fee = 0.0; let expected_low_fee = 0.0; @@ -1453,7 +1115,7 @@ mod tests { // Now test the fee estimates for a known priority level, let's say medium (50th percentile) let v = vec![account_1]; - let estimates = tracker.calculation2(&v, false, false, &None); + let estimates = calculation2(&v, false, false, &None, &tracker); // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value let expected_min_fee = 0.0; let expected_low_fee = 25.0; @@ -1468,8 +1130,7 @@ mod tests { assert_eq!(estimates.very_high, expected_very_high_fee); assert_eq!(estimates.unsafe_max, expected_max_fee); - - let estimates = tracker.calculation2(&v, false, false, &Some(150)); + let estimates = calculation2(&v, false, false, &Some(150), &tracker); // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value let expected_min_fee = 0.0; let expected_low_fee = 25.0; @@ -1484,8 +1145,7 @@ mod tests { assert_eq!(estimates.very_high, expected_very_high_fee); assert_eq!(estimates.unsafe_max, expected_max_fee); - - let estimates = tracker.calculation2(&v, false, true, &None); + let estimates = calculation2(&v, false, true, &None, &tracker); // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value let expected_min_fee = 0.0; let expected_low_fee = 25.0; @@ -1500,8 +1160,7 @@ mod tests { assert_eq!(estimates.very_high, expected_very_high_fee); assert_eq!(estimates.unsafe_max, expected_max_fee); - - let estimates = tracker.calculation2(&v, false, true, &Some(150)); + let estimates = calculation2(&v, false, true, &Some(150), &tracker); // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value let expected_min_fee = 0.0; let expected_low_fee = 25.0; @@ -1515,7 +1174,6 @@ mod tests { assert_eq!(estimates.high, expected_high_fee); assert_eq!(estimates.very_high, expected_very_high_fee); assert_eq!(estimates.unsafe_max, expected_max_fee); - } #[test] diff --git a/src/priority_fee_calculation.rs b/src/priority_fee_calculation.rs new file mode 100644 index 0000000..6c52e0b --- /dev/null +++ b/src/priority_fee_calculation.rs @@ -0,0 +1,613 @@ +use crate::model::{ + MicroLamportPriorityFeeEstimates, Percentile, PriorityFeesBySlot, PriorityLevel, +}; +use crate::priority_fee_calculation::Calculations::Calculation1; +use cadence_macros::{statsd_count, statsd_gauge}; +use solana_sdk::pubkey::Pubkey; +use std::collections::HashMap; +use std::time::Instant; +use Calculations::Calculation2; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub enum Calculations<'a> { + Calculation1 { + accounts: &'a Vec, + include_vote: bool, + include_empty_slots: bool, + lookback_period: &'a Option, + }, + Calculation2 { + accounts: &'a Vec, + include_vote: bool, + include_empty_slots: bool, + lookback_period: &'a Option, + }, +} + +impl<'a> Calculations<'a> { + pub fn new_calculation1( + accounts: &'a Vec, + include_vote: bool, + include_empty_slots: bool, + lookback_period: &'a Option, + ) -> Calculations<'a> { + Calculation1 { + accounts, + include_vote, + include_empty_slots, + lookback_period, + } + } + + pub fn new_calculation2( + accounts: &'a Vec, + include_vote: bool, + include_empty_slots: bool, + lookback_period: &'a Option, + ) -> Calculations<'a> { + Calculation2 { + accounts, + include_vote, + include_empty_slots, + lookback_period, + } + } + + pub fn get_priority_fee_estimates( + &self, + priority_fees: &PriorityFeesBySlot, + ) -> anyhow::Result { + let start = Instant::now(); + + let result = match self { + Calculation1 { + accounts, + include_vote, + include_empty_slots, + lookback_period, + } => v1::get_priority_fee_estimates( + accounts, + include_vote, + include_empty_slots, + lookback_period, + priority_fees, + ), + Calculation2 { + accounts, + include_vote, + include_empty_slots, + lookback_period, + } => v2::get_priority_fee_estimates( + accounts, + include_vote, + include_empty_slots, + lookback_period, + priority_fees, + ), + }; + let version = match self { + Calculation1 { .. } => "v1", + Calculation2 { .. } => "v2", + }; + statsd_gauge!( + "get_priority_fee_estimates_time", + start.elapsed().as_nanos() as u64, + "version" => &version + ); + statsd_count!( + "get_priority_fee_calculation_version", + 1, + "version" => &version + ); + + result + } +} + +mod v1 { + use crate::model::{MicroLamportPriorityFeeEstimates, PriorityFeesBySlot}; + use crate::priority_fee_calculation::max_percentile; + use solana_sdk::pubkey::Pubkey; + + /// + /// Algo1: given the list of accounts the algorithm will: + /// 1. collect all the transactions fees over n slots + /// 2. collect all the transaction fees for all the accounts specified over n slots + /// 3. will calculate the percentile distributions for each of two groups + /// 4. will choose the highest value from each percentile between two groups + /// + pub(crate) fn get_priority_fee_estimates( + accounts: &[Pubkey], + include_vote: &bool, + include_empty_slots: &bool, + lookback_period: &Option, + priority_fees: &PriorityFeesBySlot, + ) -> anyhow::Result { + let mut account_fees = vec![]; + let mut transaction_fees = vec![]; + for (i, slot_priority_fees) in priority_fees.iter().enumerate() { + if let Some(lookback_period) = lookback_period { + if i >= *lookback_period as usize { + break; + } + } + if *include_vote { + transaction_fees.extend_from_slice(&slot_priority_fees.fees.vote_fees); + } + transaction_fees.extend_from_slice(&slot_priority_fees.fees.non_vote_fees); + for account in accounts { + let account_priority_fees = slot_priority_fees.account_fees.get(account); + if let Some(account_priority_fees) = account_priority_fees { + if *include_vote { + account_fees.extend_from_slice(&account_priority_fees.vote_fees); + } + account_fees.extend_from_slice(&account_priority_fees.non_vote_fees); + } + } + } + if *include_empty_slots { + let lookback = lookback_period + .map(|v| v as usize) + .unwrap_or(priority_fees.len()); + let account_max_size = account_fees.len().max(lookback); + let transaction_max_size = transaction_fees.len().max(lookback); + // if there are less data than number of slots - append 0s for up to number of slots so we don't overestimate the values + account_fees.resize(account_max_size, 0f64); + transaction_fees.resize(transaction_max_size, 0f64); + } + + let micro_lamport_priority_fee_estimates = MicroLamportPriorityFeeEstimates { + min: max_percentile(&mut account_fees, &mut transaction_fees, 0), + low: max_percentile(&mut account_fees, &mut transaction_fees, 25), + medium: max_percentile(&mut account_fees, &mut transaction_fees, 50), + high: max_percentile(&mut account_fees, &mut transaction_fees, 75), + very_high: max_percentile(&mut account_fees, &mut transaction_fees, 95), + unsafe_max: max_percentile(&mut account_fees, &mut transaction_fees, 100), + }; + + Ok(micro_lamport_priority_fee_estimates) + } +} + +mod v2 { + use crate::model::{MicroLamportPriorityFeeEstimates, PriorityFeesBySlot}; + use crate::priority_fee_calculation::{calculate_lookback_size, estimate_max_values}; + use solana_sdk::clock::Slot; + use solana_sdk::pubkey::Pubkey; + + /// + /// Algo2: given the list of accounts the algorithm will: + /// 1. collect all the transactions fees over n slots + /// 2. for each specified account collect the fees and calculate the percentiles + /// 4. choose maximum values for each percentile between all transactions and each account + /// + pub(crate) fn get_priority_fee_estimates( + accounts: &[Pubkey], + include_vote: &bool, + include_empty_slots: &bool, + lookback_period: &Option, + priority_fees: &PriorityFeesBySlot, + ) -> anyhow::Result { + let mut slots_vec: Vec = priority_fees.iter().map(|value| value.slot).collect(); + slots_vec.sort(); + slots_vec.reverse(); + + let slots_vec = slots_vec; + + let lookback = calculate_lookback_size(&lookback_period, slots_vec.len()); + + let mut fees = Vec::with_capacity(slots_vec.len()); + let mut micro_lamport_priority_fee_estimates = MicroLamportPriorityFeeEstimates::default(); + + for slot in &slots_vec[..lookback] { + if let Some(slot_priority_fees) = priority_fees.get(slot) { + if *include_vote { + fees.extend_from_slice(&slot_priority_fees.fees.vote_fees); + } + fees.extend_from_slice(&slot_priority_fees.fees.non_vote_fees); + } + } + micro_lamport_priority_fee_estimates = + estimate_max_values(&mut fees, micro_lamport_priority_fee_estimates); + + for account in accounts { + fees.clear(); + let mut zero_slots: usize = 0; + for slot in &slots_vec[..lookback] { + if let Some(slot_priority_fees) = priority_fees.get(slot) { + let account_priority_fees = slot_priority_fees.account_fees.get(account); + if let Some(account_priority_fees) = account_priority_fees { + if *include_vote { + fees.extend_from_slice(&account_priority_fees.vote_fees); + } + fees.extend_from_slice(&account_priority_fees.non_vote_fees); + } else { + zero_slots += 1; + } + } else { + zero_slots += 1; + } + } + if *include_empty_slots { + // for all empty slot we need to add a 0 + fees.resize(fees.len() + zero_slots, 0f64); + } + micro_lamport_priority_fee_estimates = + estimate_max_values(&mut fees, micro_lamport_priority_fee_estimates); + } + Ok(micro_lamport_priority_fee_estimates) + } +} + +fn estimate_max_values( + mut fees: &mut Vec, + mut estimates: MicroLamportPriorityFeeEstimates, +) -> MicroLamportPriorityFeeEstimates { + let vals: HashMap = percentile( + &mut fees, + &[ + PriorityLevel::Min.into(), + PriorityLevel::Low.into(), + PriorityLevel::Medium.into(), + PriorityLevel::High.into(), + PriorityLevel::VeryHigh.into(), + PriorityLevel::UnsafeMax.into(), + ], + ); + estimates.min = vals + .get(&PriorityLevel::Min.into()) + .unwrap_or(&estimates.min) + .max(estimates.min); + estimates.low = vals + .get(&PriorityLevel::Low.into()) + .unwrap_or(&estimates.low) + .max(estimates.low); + estimates.medium = vals + .get(&PriorityLevel::Medium.into()) + .unwrap_or(&estimates.medium) + .max(estimates.medium); + estimates.high = vals + .get(&PriorityLevel::High.into()) + .unwrap_or(&estimates.high) + .max(estimates.high); + estimates.very_high = vals + .get(&PriorityLevel::VeryHigh.into()) + .unwrap_or(&estimates.very_high) + .max(estimates.very_high); + estimates.unsafe_max = vals + .get(&PriorityLevel::UnsafeMax.into()) + .unwrap_or(&estimates.unsafe_max) + .max(estimates.unsafe_max); + estimates +} + +fn max(a: f64, b: f64) -> f64 { + if a > b { + a + } else { + b + } +} +fn calculate_lookback_size(pref_num_slots: &Option, max_available_slots: usize) -> usize { + max_available_slots.min( + pref_num_slots + .map(|v| v as usize) + .unwrap_or(max_available_slots), + ) +} + +fn max_percentile( + account_fees: &mut Vec, + transaction_fees: &mut Vec, + percentile_value: Percentile, +) -> f64 { + let results1 = percentile(account_fees, &[percentile_value]); + let results2 = percentile(transaction_fees, &[percentile_value]); + max( + *results1.get(&percentile_value).unwrap_or(&0f64), + *results2.get(&percentile_value).unwrap_or(&0f64), + ) +} + +// pulled from here - https://www.calculatorsoup.com/calculators/statistics/percentile-calculator.php +// couldn't find any good libraries that worked +fn percentile(values: &mut Vec, percentiles: &[Percentile]) -> HashMap { + if values.is_empty() { + return HashMap::with_capacity(0); + } + + values.sort_by(|a, b| a.partial_cmp(b).unwrap()); + let n = values.len() as f64; + + percentiles.into_iter().fold( + HashMap::with_capacity(percentiles.len()), + |mut data, &percentile| { + let r = (percentile as f64 / 100.0) * (n - 1.0) + 1.0; + + let val = if r.fract() == 0.0 { + values[r as usize - 1] + } else { + let ri = r.trunc() as usize - 1; + let rf = r.fract(); + values[ri] + rf * (values[ri + 1] - values[ri]) + }; + data.insert(percentile, val); + data + }, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::model::{Fees, SlotPriorityFees}; + use cadence::{NopMetricSink, StatsdClient}; + use cadence_macros::set_global_default; + use solana_sdk::clock::Slot; + + fn init_metrics() { + let noop = NopMetricSink {}; + let client = StatsdClient::builder("", noop).build(); + set_global_default(client) + } + + #[tokio::test] + async fn test_specific_fee_estimates() { + init_metrics(); + let tracker = PriorityFeesBySlot::new(); + + let mut fees = vec![]; + let mut i = 0; + while i <= 100 { + fees.push(i as f64); + i += 1; + } + let account_1 = Pubkey::new_unique(); + let account_2 = Pubkey::new_unique(); + let account_3 = Pubkey::new_unique(); + let accounts = vec![account_1, account_2, account_3]; + + // Simulate adding the fixed fees as both account-specific and transaction fees + for fee in fees.clone() { + push_priority_fee_for_txn(1, accounts.clone(), fee as u64, false, &tracker); + } + + // Now test the fee estimates for a known priority level, let's say medium (50th percentile) + let estimates = Calculations::new_calculation1( + &vec![accounts.get(0).unwrap().clone()], + false, + false, + &None, + ) + .get_priority_fee_estimates(&tracker) + .expect("estimates to be valid"); + + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + let expected_min_fee = 0.0; + let expected_low_fee = 25.0; + let expected_medium_fee = 50.0; + let expected_high_fee = 75.0; + let expected_very_high_fee = 95.0; + let expected_max_fee = 100.0; + assert_eq!(estimates.min, expected_min_fee); + assert_eq!(estimates.low, expected_low_fee); + assert_eq!(estimates.medium, expected_medium_fee); + assert_eq!(estimates.high, expected_high_fee); + assert_eq!(estimates.very_high, expected_very_high_fee); + assert_eq!(estimates.unsafe_max, expected_max_fee); + + // Now test the fee estimates for a known priority level, let's say medium (50th percentile) + + let estimates = Calculations::new_calculation1( + &vec![accounts.get(0).unwrap().clone()], + false, + false, + &Some(150), + ) + .get_priority_fee_estimates(&tracker) + .expect("estimates to be valid"); + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + let expected_min_fee = 0.0; + let expected_low_fee = 25.0; + let expected_medium_fee = 50.0; + let expected_high_fee = 75.0; + let expected_very_high_fee = 95.0; + let expected_max_fee = 100.0; + assert_eq!(estimates.min, expected_min_fee); + assert_eq!(estimates.low, expected_low_fee); + assert_eq!(estimates.medium, expected_medium_fee); + assert_eq!(estimates.high, expected_high_fee); + assert_eq!(estimates.very_high, expected_very_high_fee); + assert_eq!(estimates.unsafe_max, expected_max_fee); + + // Now test the fee estimates for a known priority level, let's say medium (50th percentile) + let estimates = Calculations::new_calculation1( + &vec![accounts.get(0).unwrap().clone()], + false, + true, + &None, + ) + .get_priority_fee_estimates(&tracker) + .expect("estimates to be valid"); + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + let expected_min_fee = 0.0; + let expected_low_fee = 25.0; + let expected_medium_fee = 50.0; + let expected_high_fee = 75.0; + let expected_very_high_fee = 95.0; + let expected_max_fee = 100.0; + assert_eq!(estimates.min, expected_min_fee); + assert_eq!(estimates.low, expected_low_fee); + assert_eq!(estimates.medium, expected_medium_fee); + assert_eq!(estimates.high, expected_high_fee); + assert_eq!(estimates.very_high, expected_very_high_fee); + assert_eq!(estimates.unsafe_max, expected_max_fee); + + // Now test the fee estimates for a known priority level, let's say medium (50th percentile) + let estimates = Calculations::new_calculation1( + &vec![accounts.get(0).unwrap().clone()], + false, + true, + &Some(150), + ) + .get_priority_fee_estimates(&tracker) + .expect("estimates to be valid"); + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + let expected_min_fee = 0.0; + let expected_low_fee = 0.0; + let expected_medium_fee = 25.5; + let expected_high_fee = 62.75; + let expected_very_high_fee = 92.54999999999998; + let expected_max_fee = 100.0; + assert_eq!(estimates.min, expected_min_fee); + assert_eq!(estimates.low, expected_low_fee); + assert_eq!(estimates.medium, expected_medium_fee); + assert_eq!(estimates.high, expected_high_fee); + assert_eq!(estimates.very_high, expected_very_high_fee); + assert_eq!(estimates.unsafe_max, expected_max_fee); + } + + #[tokio::test] + async fn test_specific_fee_estimates_v2() { + init_metrics(); + let tracker = PriorityFeesBySlot::new(); + + let mut fees = vec![]; + let mut i = 0; + while i <= 100 { + fees.push(i as f64); + i += 1; + } + let account_1 = Pubkey::new_unique(); + let account_2 = Pubkey::new_unique(); + let account_3 = Pubkey::new_unique(); + let accounts = vec![account_1, account_2, account_3]; + + // Simulate adding the fixed fees as both account-specific and transaction fees + for fee in fees.clone() { + push_priority_fee_for_txn(1, accounts.clone(), fee as u64, false, &tracker); + } + + // Now test the fee estimates for a known priority level, let's say medium (50th percentile) + let estimates = Calculations::new_calculation2( + &vec![accounts.get(0).unwrap().clone()], + false, + false, + &None, + ) + .get_priority_fee_estimates(&tracker) + .expect("estimates to be valid"); + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + let expected_min_fee = 0.0; + let expected_low_fee = 25.0; + let expected_medium_fee = 50.0; + let expected_high_fee = 75.0; + let expected_very_high_fee = 95.0; + let expected_max_fee = 100.0; + assert_eq!(estimates.min, expected_min_fee); + assert_eq!(estimates.low, expected_low_fee); + assert_eq!(estimates.medium, expected_medium_fee); + assert_eq!(estimates.high, expected_high_fee); + assert_eq!(estimates.very_high, expected_very_high_fee); + assert_eq!(estimates.unsafe_max, expected_max_fee); + + // Now test the fee estimates for a known priority level, let's say medium (50th percentile) + let estimates = Calculations::new_calculation2( + &vec![accounts.get(0).unwrap().clone()], + false, + false, + &Some(150), + ) + .get_priority_fee_estimates(&tracker) + .expect("estimates to be valid"); + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + let expected_min_fee = 0.0; + let expected_low_fee = 25.0; + let expected_medium_fee = 50.0; + let expected_high_fee = 75.0; + let expected_very_high_fee = 95.0; + let expected_max_fee = 100.0; + assert_eq!(estimates.min, expected_min_fee); + assert_eq!(estimates.low, expected_low_fee); + assert_eq!(estimates.medium, expected_medium_fee); + assert_eq!(estimates.high, expected_high_fee); + assert_eq!(estimates.very_high, expected_very_high_fee); + assert_eq!(estimates.unsafe_max, expected_max_fee); + + // Now test the fee estimates for a known priority level, let's say medium (50th percentile) + let estimates = Calculations::new_calculation2( + &vec![accounts.get(0).unwrap().clone()], + false, + true, + &None, + ) + .get_priority_fee_estimates(&tracker) + .expect("estimates to be valid"); + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + let expected_min_fee = 0.0; + let expected_low_fee = 25.0; + let expected_medium_fee = 50.0; + let expected_high_fee = 75.0; + let expected_very_high_fee = 95.0; + let expected_max_fee = 100.0; + assert_eq!(estimates.min, expected_min_fee); + assert_eq!(estimates.low, expected_low_fee); + assert_eq!(estimates.medium, expected_medium_fee); + assert_eq!(estimates.high, expected_high_fee); + assert_eq!(estimates.very_high, expected_very_high_fee); + assert_eq!(estimates.unsafe_max, expected_max_fee); + + // Now test the fee estimates for a known priority level, let's say medium (50th percentile) + let estimates = Calculations::new_calculation2( + &vec![accounts.get(0).unwrap().clone()], + false, + true, + &Some(150), + ) + .get_priority_fee_estimates(&tracker) + .expect("estimates to be valid"); + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + let expected_min_fee = 0.0; + let expected_low_fee = 25.0; + let expected_medium_fee = 50.0; + let expected_high_fee = 75.0; + let expected_very_high_fee = 95.0; + let expected_max_fee = 100.0; + assert_eq!(estimates.min, expected_min_fee); + assert_eq!(estimates.low, expected_low_fee); + assert_eq!(estimates.medium, expected_medium_fee); + assert_eq!(estimates.high, expected_high_fee); + assert_eq!(estimates.very_high, expected_very_high_fee); + assert_eq!(estimates.unsafe_max, expected_max_fee); + // NOTE: calculation 2 + } + + pub fn push_priority_fee_for_txn( + slot: Slot, + accounts: Vec, + priority_fee: u64, + is_vote: bool, + priority_fees: &PriorityFeesBySlot, + ) { + // update the slot cache so we can keep track of the slots we have processed in order + // for removal later + if !priority_fees.contains_key(&slot) { + priority_fees.insert( + slot, + SlotPriorityFees::new(slot, accounts, priority_fee, is_vote), + ); + } else { + // update the slot priority fees + priority_fees.entry(slot).and_modify(|priority_fees| { + priority_fees.fees.add_fee(priority_fee as f64, is_vote); + for account in accounts { + priority_fees + .account_fees + .entry(account) + .and_modify(|fees| fees.add_fee(priority_fee as f64, is_vote)) + .or_insert(Fees::new(priority_fee as f64, is_vote)); + } + }); + } + } +} diff --git a/src/rpc_server.rs b/src/rpc_server.rs index f9bc358..5333924 100644 --- a/src/rpc_server.rs +++ b/src/rpc_server.rs @@ -6,27 +6,27 @@ use std::{ time::Instant, }; -use crate::{ - errors::invalid_request, - priority_fee::{MicroLamportPriorityFeeEstimates, PriorityFeeTracker, PriorityLevel}, - solana::solana_rpc::decode_and_deserialize, -}; +use crate::errors::invalid_request; +use crate::model::{MicroLamportPriorityFeeEstimates, PriorityLevel}; +use crate::priority_fee::{construct_writable_accounts, PriorityFeeTracker}; +use crate::priority_fee_calculation::Calculations; +use crate::solana::solana_rpc::decode_and_deserialize; use cadence_macros::{statsd_count, statsd_time}; use jsonrpsee::{ core::{async_trait, RpcResult}, proc_macros::rpc, types::ErrorObjectOwned, }; +use jsonrpsee::types::error::{INTERNAL_ERROR_CODE, INTERNAL_ERROR_MSG}; use serde::{Deserialize, Serialize}; use solana_account_decoder::parse_address_lookup_table::{ parse_address_lookup_table, LookupTableAccountType, }; use solana_client::rpc_client::RpcClient; -use solana_sdk::{pubkey::Pubkey, transaction::VersionedTransaction}; use solana_sdk::message::MessageHeader; +use solana_sdk::{pubkey::Pubkey, transaction::VersionedTransaction}; use solana_transaction_status::UiTransactionEncoding; -use tracing::info; -use crate::priority_fee::construct_writable_accounts; +use tracing::{info, warn}; pub struct AtlasPriorityFeeEstimator { pub priority_fee_tracker: Arc, @@ -44,9 +44,7 @@ impl fmt::Debug for AtlasPriorityFeeEstimator { } #[derive(Serialize, Deserialize, Clone, Debug, Default)] -#[serde( - rename_all(serialize = "camelCase", deserialize = "camelCase"), -)] +#[serde(rename_all(serialize = "camelCase", deserialize = "camelCase"))] // TODO: DKH - delete after all the users were notified pub struct GetPriorityFeeEstimateRequestLight { pub transaction: Option, // estimate fee for a txn @@ -55,9 +53,7 @@ pub struct GetPriorityFeeEstimateRequestLight { } #[derive(Serialize, Deserialize, Clone, Debug, Default)] -#[serde( - rename_all(serialize = "camelCase", deserialize = "camelCase"), -)] +#[serde(rename_all(serialize = "camelCase", deserialize = "camelCase"))] // TODO: DKH - Delete after all the users were notified pub struct GetPriorityFeeEstimateOptionsLight { // controls input txn encoding @@ -77,20 +73,17 @@ impl Into for GetPriorityFeeEstimateRequestLight fn into(self) -> GetPriorityFeeEstimateRequest { let transaction = self.transaction; let account_keys = self.account_keys; - let options = self.options.map(|o| { - GetPriorityFeeEstimateOptions { - transaction_encoding: o.transaction_encoding, - priority_level: o.priority_level, - include_all_priority_fee_levels: o.include_all_priority_fee_levels, - lookback_slots: o.lookback_slots, - include_vote: o.include_vote, - recommended: o.recommended, - evaluate_empty_slot_as_zero: o.evaluate_empty_slot_as_zero, - } + let options = self.options.map(|o| GetPriorityFeeEstimateOptions { + transaction_encoding: o.transaction_encoding, + priority_level: o.priority_level, + include_all_priority_fee_levels: o.include_all_priority_fee_levels, + lookback_slots: o.lookback_slots, + include_vote: o.include_vote, + recommended: o.recommended, + evaluate_empty_slot_as_zero: o.evaluate_empty_slot_as_zero, }); - GetPriorityFeeEstimateRequest - { + GetPriorityFeeEstimateRequest { transaction, account_keys, options, @@ -141,20 +134,17 @@ impl Into for GetPriorityFeeEstimateRequest fn into(self) -> GetPriorityFeeEstimateRequestLight { let transaction = self.transaction; let account_keys = self.account_keys; - let options = self.options.map(|o| { - GetPriorityFeeEstimateOptionsLight { - transaction_encoding: o.transaction_encoding, - priority_level: o.priority_level, - include_all_priority_fee_levels: o.include_all_priority_fee_levels, - lookback_slots: o.lookback_slots, - include_vote: o.include_vote, - recommended: o.recommended, - evaluate_empty_slot_as_zero: o.evaluate_empty_slot_as_zero, - } + let options = self.options.map(|o| GetPriorityFeeEstimateOptionsLight { + transaction_encoding: o.transaction_encoding, + priority_level: o.priority_level, + include_all_priority_fee_levels: o.include_all_priority_fee_levels, + lookback_slots: o.lookback_slots, + include_vote: o.include_vote, + recommended: o.recommended, + evaluate_empty_slot_as_zero: o.evaluate_empty_slot_as_zero, }); - GetPriorityFeeEstimateRequestLight - { + GetPriorityFeeEstimateRequestLight { transaction, account_keys, options, @@ -361,7 +351,7 @@ fn get_accounts( get_from_account_keys(&transaction), get_from_address_lookup_tables(rpc_client, &transaction), ] - .concat(); + .concat(); return Ok(account_keys); } } @@ -376,43 +366,19 @@ impl AtlasPriorityFeeEstimatorRpcServer for AtlasPriorityFeeEstimator { fn get_priority_fee_estimate_v1( &self, - get_priority_fee_estimate_request: GetPriorityFeeEstimateRequestLight + get_priority_fee_estimate_request: GetPriorityFeeEstimateRequestLight, ) -> RpcResult { - let algo_run_fn = |accounts: Vec, - include_vote: bool, - evaluate_empty_slot_as_zero: bool, - lookback_period: Option| - -> MicroLamportPriorityFeeEstimates { - self.priority_fee_tracker.get_priority_fee_estimates( - accounts, - include_vote, - evaluate_empty_slot_as_zero, - lookback_period, - true, - ) - }; - self.execute_priority_fee_estimate_coordinator(get_priority_fee_estimate_request.into(), algo_run_fn) - + self.execute_priority_fee_estimate_coordinator( + get_priority_fee_estimate_request.into(), + true, + ) } fn get_priority_fee_estimate_v2( &self, get_priority_fee_estimate_request: GetPriorityFeeEstimateRequest, ) -> RpcResult { - let algo_run_fn = |accounts: Vec, - include_vote: bool, - evaluate_empty_slot_as_zero: bool, - lookback_period: Option| - -> MicroLamportPriorityFeeEstimates { - self.priority_fee_tracker.get_priority_fee_estimates( - accounts, - include_vote, - evaluate_empty_slot_as_zero, - lookback_period, - false, - ) - }; - self.execute_priority_fee_estimate_coordinator(get_priority_fee_estimate_request, algo_run_fn) + self.execute_priority_fee_estimate_coordinator(get_priority_fee_estimate_request, false) } } @@ -433,9 +399,8 @@ impl AtlasPriorityFeeEstimator { fn execute_priority_fee_estimate_coordinator( &self, get_priority_fee_estimate_request: GetPriorityFeeEstimateRequest, - priority_fee_calc_fn: impl FnOnce(Vec, bool, bool, Option) -> MicroLamportPriorityFeeEstimates, - ) -> RpcResult - { + is_v1: bool, + ) -> RpcResult { let options = get_priority_fee_estimate_request.options.clone(); let reason = validate_get_priority_fee_estimate_request(&get_priority_fee_estimate_request); if let Some(reason) = reason { @@ -445,23 +410,30 @@ impl AtlasPriorityFeeEstimator { if let Err(e) = accounts { return Err(e); } - let accounts = accounts + let accounts: Vec = accounts .unwrap() .iter() .filter_map(|a| Pubkey::from_str(a).ok()) .collect(); let lookback_slots = options.clone().map(|o| o.lookback_slots).flatten(); - if let Some(lookback_slots) = lookback_slots { - if lookback_slots < 1 || lookback_slots as usize > self.max_lookback_slots { + if let Some(lookback_slots) = &lookback_slots { + if *lookback_slots < 1 || *lookback_slots as usize > self.max_lookback_slots { return Err(invalid_request("lookback_slots must be between 1 and 150")); } } let include_vote = should_include_vote(&options); let include_empty_slots = should_include_empty_slots(&options); - let priority_fee_levels = priority_fee_calc_fn(accounts, - include_vote, - include_empty_slots, - lookback_slots); + let calc = if is_v1 { + Calculations::new_calculation1(&accounts, include_vote, include_empty_slots, &lookback_slots) + } else { + Calculations::new_calculation2(&accounts, include_vote, include_empty_slots, &lookback_slots) + }; + let priority_fee_levels = self.priority_fee_tracker.calculate_priority_fee_details(&calc); + if let Err(e) = priority_fee_levels { + warn!("failed to calculate priority_fee_levels: {:#?}", e); + return Err(ErrorObjectOwned::owned(INTERNAL_ERROR_CODE, INTERNAL_ERROR_MSG, None::)); + } + let priority_fee_levels = priority_fee_levels.unwrap(); if let Some(options) = options.clone() { if options.include_all_priority_fee_levels == Some(true) { return Ok(GetPriorityFeeEstimateResponse { @@ -661,9 +633,9 @@ mod tests { let res = res.unwrap(); assert_request(&res, Id::Str(Cow::const_str("1")), "getPriorityFeeEstimate"); - if let Some(val) = res.params - { - let params: Result, _> = serde_json::from_str(val.get()); + if let Some(val) = res.params { + let params: Result, _> = + serde_json::from_str(val.get()); assert!(params.is_err()); assert_eq!(params.err().unwrap().to_string(), error, "testing {param}"); } @@ -685,16 +657,16 @@ mod tests { fn bad_params<'a>() -> Vec<(&'a str, &'a str)> { vec![ - (r#"{"transactions": null}"#,"unknown field `transactions`, expected one of `transaction`, `accountKeys`, `options` at line 1 column 16"), - (r#"{"account_keys": null}"#,"unknown field `account_keys`, expected one of `transaction`, `accountKeys`, `options` at line 1 column 16"), - (r#"{"accountkeys": null}"#,"unknown field `accountkeys`, expected one of `transaction`, `accountKeys`, `options` at line 1 column 15"), + (r#"{"transactions": null}"#, "unknown field `transactions`, expected one of `transaction`, `accountKeys`, `options` at line 1 column 16"), + (r#"{"account_keys": null}"#, "unknown field `account_keys`, expected one of `transaction`, `accountKeys`, `options` at line 1 column 16"), + (r#"{"accountkeys": null}"#, "unknown field `accountkeys`, expected one of `transaction`, `accountKeys`, `options` at line 1 column 15"), (r#"{"accountKeys": [1, 2]}"#, "invalid type: integer `1`, expected a string at line 1 column 19"), (r#"{"option": null}"#, "unknown field `option`, expected one of `transaction`, `accountKeys`, `options` at line 1 column 10"), (r#"{"options": {"transaction_encoding":null}}"#, "unknown field `transaction_encoding`, expected one of `transactionEncoding`, `priorityLevel`, `includeAllPriorityFeeLevels`, `lookbackSlots`, `includeVote`, `recommended`, `evaluateEmptySlotAsZero` at line 1 column 36"), (r#"{"options": {"priorityLevel":"HIGH"}}"#, "unknown variant `HIGH`, expected one of `Min`, `Low`, `Medium`, `High`, `VeryHigh`, `UnsafeMax`, `Default` at line 1 column 36"), (r#"{"options": {"includeAllPriorityFeeLevels":"no"}}"#, "invalid type: string \"no\", expected a boolean at line 1 column 48"), - (r#"{"options": {"lookbackSlots":"no"}}"#, "invalid type: string \"no\", expected u32 at line 1 column 34"), - (r#"{"options": {"lookbackSlots":"-1"}}"#, "invalid type: string \"-1\", expected u32 at line 1 column 34"), + (r#"{"options": {"lookbackSlots":"no"}}"#, "invalid type: string \"no\", expected u32 at line 1 column 34"), + (r#"{"options": {"lookbackSlots":"-1"}}"#, "invalid type: string \"-1\", expected u32 at line 1 column 34"), ] } } From 7f3d3e248d5fd5dcd599e15913ba769b6b1542d8 Mon Sep 17 00:00:00 2001 From: Dmitriy Khomitskiy Date: Mon, 18 Nov 2024 09:31:43 -0600 Subject: [PATCH 2/5] RPC-468 exposing more information about each calculation --- Cargo.lock | 139 ++++- Cargo.toml | 3 +- src/priority_fee.rs | 381 ++++++++++--- src/priority_fee_calculation.rs | 919 ++++++++++++++++++++------------ src/rpc_server.rs | 6 +- 5 files changed, 1008 insertions(+), 440 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 46aad51..791622d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -190,6 +190,15 @@ version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10f00e1f6e58a40e807377c75c6a7f97bf9044fab57816f2414e6f5f4499d7b8" +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "ark-bn254" version = "0.4.0" @@ -461,6 +470,7 @@ dependencies = [ "solana-program-runtime", "solana-sdk", "solana-transaction-status", + "statrs", "thiserror", "tokio", "tower", @@ -2261,6 +2271,12 @@ version = "0.2.154" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + [[package]] name = "libsecp256k1" version = "0.6.0" @@ -2364,6 +2380,16 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "matrixmultiply" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9380b911e3e96d10c1f415da0876389aaf1b56759054eeb0de7df940c456ba1a" +dependencies = [ + "autocfg", + "rawpointer", +] + [[package]] name = "memchr" version = "2.7.2" @@ -2447,6 +2473,35 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" +[[package]] +name = "nalgebra" +version = "0.32.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5c17de023a86f59ed79891b2e5d5a94c705dbe904a5b5c9c952ea6221b03e4" +dependencies = [ + "approx", + "matrixmultiply", + "nalgebra-macros", + "num-complex 0.4.6", + "num-rational 0.4.2", + "num-traits", + "rand 0.8.5", + "rand_distr", + "simba", + "typenum", +] + +[[package]] +name = "nalgebra-macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "254a5372af8fc138e36684761d3c0cdb758a4410e938babcff1c860ce14ddbfc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.63", +] + [[package]] name = "nix" version = "0.26.4" @@ -2487,10 +2542,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" dependencies = [ "num-bigint 0.2.6", - "num-complex", + "num-complex 0.2.4", "num-integer", "num-iter", - "num-rational", + "num-rational 0.2.4", "num-traits", ] @@ -2525,6 +2580,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -2585,6 +2649,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2592,6 +2666,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -3186,6 +3261,16 @@ dependencies = [ "getrandom 0.2.15", ] +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -3204,6 +3289,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + [[package]] name = "rayon" version = "1.10.0" @@ -3487,6 +3578,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "safe_arch" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3460605018fdc9612bce72735cba0d27efbcd9904780d44c7e3a9948f96148a" +dependencies = [ + "bytemuck", +] + [[package]] name = "schannel" version = "0.1.23" @@ -3747,6 +3847,19 @@ version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +[[package]] +name = "simba" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061507c94fc6ab4ba1c9a0305018408e312e17c041eb63bef8aa726fa33aceae" +dependencies = [ + "approx", + "num-complex 0.4.6", + "num-traits", + "paste", + "wide", +] + [[package]] name = "sized-chunks" version = "0.6.5" @@ -4826,6 +4939,18 @@ dependencies = [ "spl-program-error", ] +[[package]] +name = "statrs" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f697a07e4606a0a25c044de247e583a330dbb1731d11bc7350b81f48ad567255" +dependencies = [ + "approx", + "nalgebra", + "num-traits", + "rand 0.8.5", +] + [[package]] name = "strsim" version = "0.8.0" @@ -5654,6 +5779,16 @@ version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +[[package]] +name = "wide" +version = "0.7.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b828f995bf1e9622031f8009f8481a85406ce1f4d4588ff746d872043e855690" +dependencies = [ + "bytemuck", + "safe_arch", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 0152cfa..7b9894c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,4 +40,5 @@ futures = "0.3.24" figment = { version = "0.10.6", features = ["env", "test"] } tower = { version = "0.4.13", features = ["full"] } thiserror = "1.0.63" -anyhow = "1.0.86" \ No newline at end of file +anyhow = "1.0.86" +statrs = "0.17.1" \ No newline at end of file diff --git a/src/priority_fee.rs b/src/priority_fee.rs index 30b29a6..eae48d1 100644 --- a/src/priority_fee.rs +++ b/src/priority_fee.rs @@ -1,6 +1,8 @@ use crate::errors::TransactionValidationError; use crate::grpc_consumer::GrpcConsumer; -use crate::model::{Fees, MicroLamportPriorityFeeEstimates, PriorityFeesBySlot, SlotPriorityFees}; +use crate::model::{ + Fees, MicroLamportPriorityFeeEstimates, PriorityFeesBySlot, PriorityLevel, SlotPriorityFees, +}; use crate::priority_fee_calculation::Calculations; use crate::priority_fee_calculation::Calculations::Calculation1; use crate::rpc_server::get_recommended_fee; @@ -14,6 +16,7 @@ use solana_program_runtime::prioritization_fee::PrioritizationFeeDetails; use solana_sdk::instruction::CompiledInstruction; use solana_sdk::transaction::TransactionError; use solana_sdk::{pubkey::Pubkey, slot_history::Slot}; +use statrs::statistics::{Data, OrderStatistics}; use std::sync::Arc; use std::time::Duration; use tracing::error; @@ -304,10 +307,46 @@ impl PriorityFeeTracker { &self, calculation: &Calculations, ) -> anyhow::Result { - calculation.get_priority_fee_estimates(&self.priority_fees) + let res = calculation.get_priority_fee_estimates(&self.priority_fees)?; + let res = res.into_iter().fold( + MicroLamportPriorityFeeEstimates::default(), + |estimate, mut data| estimate_max_values(&mut data.1, estimate), + ); + Ok(res) } } +fn estimate_max_values( + fees: &mut Data>, + mut estimates: MicroLamportPriorityFeeEstimates, +) -> MicroLamportPriorityFeeEstimates { + estimates.min = fees + .percentile(PriorityLevel::Min.into()) + .round() + .max(estimates.min); + estimates.low = fees + .percentile(PriorityLevel::Low.into()) + .round() + .max(estimates.low); + estimates.medium = fees + .percentile(PriorityLevel::Medium.into()) + .round() + .max(estimates.medium); + estimates.high = fees + .percentile(PriorityLevel::High.into()) + .round() + .max(estimates.high); + estimates.very_high = fees + .percentile(PriorityLevel::VeryHigh.into()) + .round() + .max(estimates.very_high); + estimates.unsafe_max = fees + .percentile(PriorityLevel::UnsafeMax.into()) + .round() + .max(estimates.unsafe_max); + estimates +} + #[cfg(test)] mod tests { use super::*; @@ -360,6 +399,180 @@ mod tests { .calculate_priority_fee_details(&calc) .expect(format!("estimates for calc2 to be valid with {:?}", calc).as_str()) } + #[tokio::test] + async fn test_specific_fee_estimates_with_no_account() { + init_metrics(); + let tracker = PriorityFeeTracker::new(10); + + let mut fees = vec![]; + let mut i = 0; + while i <= 100 { + fees.push(i as f64); + i += 1; + } + let account_1 = Pubkey::new_unique(); + let account_2 = Pubkey::new_unique(); + let account_3 = Pubkey::new_unique(); + let accounts = vec![account_1, account_2, account_3]; + + // Simulate adding the fixed fees as both account-specific and transaction fees + for fee in fees.clone() { + tracker.push_priority_fee_for_txn(1, accounts.clone(), fee as u64, false); + } + + // Now test the fee estimates for a known priority level, let's say medium (50th percentile) + let acct = vec![]; + let estimates = calculation1(&acct, false, false, &None, &tracker); + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + let expected_min_fee = 0.0; + let expected_low_fee = 25.0; + let expected_medium_fee = 50.0; + let expected_high_fee = 75.0; + let expected_very_high_fee = 96.0; + let expected_max_fee = 100.0; + assert_eq!(estimates.min, expected_min_fee); + assert_eq!(estimates.low, expected_low_fee); + assert_eq!(estimates.medium, expected_medium_fee); + assert_eq!(estimates.high, expected_high_fee); + assert_eq!(estimates.very_high, expected_very_high_fee); + assert_eq!(estimates.unsafe_max, expected_max_fee); + + // Now test the fee estimates for a known priority level, let's say medium (50th percentile) + let estimates = calculation1(&acct, false, false, &Some(150), &tracker); + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + let expected_min_fee = 0.0; + let expected_low_fee = 25.0; + let expected_medium_fee = 50.0; + let expected_high_fee = 75.0; + let expected_very_high_fee = 96.0; + let expected_max_fee = 100.0; + assert_eq!(estimates.min, expected_min_fee); + assert_eq!(estimates.low, expected_low_fee); + assert_eq!(estimates.medium, expected_medium_fee); + assert_eq!(estimates.high, expected_high_fee); + assert_eq!(estimates.very_high, expected_very_high_fee); + assert_eq!(estimates.unsafe_max, expected_max_fee); + + // Now test the fee estimates for a known priority level, let's say medium (50th percentile) + let estimates = calculation1(&acct, false, true, &None, &tracker); + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + let expected_min_fee = 0.0; + let expected_low_fee = 25.0; + let expected_medium_fee = 50.0; + let expected_high_fee = 75.0; + let expected_very_high_fee = 96.0; + let expected_max_fee = 100.0; + assert_eq!(estimates.min, expected_min_fee); + assert_eq!(estimates.low, expected_low_fee); + assert_eq!(estimates.medium, expected_medium_fee); + assert_eq!(estimates.high, expected_high_fee); + assert_eq!(estimates.very_high, expected_very_high_fee); + assert_eq!(estimates.unsafe_max, expected_max_fee); + + // Now test the fee estimates for a known priority level, let's say medium (50th percentile) + let estimates = calculation1(&acct, false, true, &Some(150), &tracker); + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + let expected_min_fee = 0.0; + let expected_low_fee = 25.0; + let expected_medium_fee = 50.0; + let expected_high_fee = 75.0; + let expected_very_high_fee = 96.0; + let expected_max_fee = 100.0; + assert_eq!(estimates.min, expected_min_fee); + assert_eq!(estimates.low, expected_low_fee); + assert_eq!(estimates.medium, expected_medium_fee); + assert_eq!(estimates.high, expected_high_fee); + assert_eq!(estimates.very_high, expected_very_high_fee); + assert_eq!(estimates.unsafe_max, expected_max_fee); + } + + #[tokio::test] + async fn test_specific_fee_estimates_v2() { + init_metrics(); + let tracker = PriorityFeeTracker::new(10); + + let mut fees = vec![]; + let mut i = 0; + while i <= 100 { + fees.push(i as f64); + i += 1; + } + let account_1 = Pubkey::new_unique(); + let account_2 = Pubkey::new_unique(); + let account_3 = Pubkey::new_unique(); + let accounts = vec![account_1, account_2, account_3]; + + // Simulate adding the fixed fees as both account-specific and transaction fees + for fee in fees.clone() { + tracker.push_priority_fee_for_txn(1, accounts.clone(), fee as u64, false); + } + + let acct = vec![]; + // Now test the fee estimates for a known priority level, let's say medium (50th percentile) + let estimates = calculation2(&acct, false, false, &None, &tracker); + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + let expected_min_fee = 0.0; + let expected_low_fee = 25.0; + let expected_medium_fee = 50.0; + let expected_high_fee = 75.0; + let expected_very_high_fee = 96.0; + let expected_max_fee = 100.0; + assert_eq!(estimates.min, expected_min_fee); + assert_eq!(estimates.low, expected_low_fee); + assert_eq!(estimates.medium, expected_medium_fee); + assert_eq!(estimates.high, expected_high_fee); + assert_eq!(estimates.very_high, expected_very_high_fee); + assert_eq!(estimates.unsafe_max, expected_max_fee); + + // Now test the fee estimates for a known priority level, let's say medium (50th percentile) + let estimates = calculation2(&acct, false, false, &Some(150), &tracker); + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + let expected_min_fee = 0.0; + let expected_low_fee = 25.0; + let expected_medium_fee = 50.0; + let expected_high_fee = 75.0; + let expected_very_high_fee = 96.0; + let expected_max_fee = 100.0; + assert_eq!(estimates.min, expected_min_fee); + assert_eq!(estimates.low, expected_low_fee); + assert_eq!(estimates.medium, expected_medium_fee); + assert_eq!(estimates.high, expected_high_fee); + assert_eq!(estimates.very_high, expected_very_high_fee); + assert_eq!(estimates.unsafe_max, expected_max_fee); + + // Now test the fee estimates for a known priority level, let's say medium (50th percentile) + let estimates = calculation2(&acct, false, true, &None, &tracker); + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + let expected_min_fee = 0.0; + let expected_low_fee = 25.0; + let expected_medium_fee = 50.0; + let expected_high_fee = 75.0; + let expected_very_high_fee = 96.0; + let expected_max_fee = 100.0; + assert_eq!(estimates.min, expected_min_fee); + assert_eq!(estimates.low, expected_low_fee); + assert_eq!(estimates.medium, expected_medium_fee); + assert_eq!(estimates.high, expected_high_fee); + assert_eq!(estimates.very_high, expected_very_high_fee); + assert_eq!(estimates.unsafe_max, expected_max_fee); + + // Now test the fee estimates for a known priority level, let's say medium (50th percentile) + let estimates = calculation2(&acct, false, true, &Some(150), &tracker); + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + let expected_min_fee = 0.0; + let expected_low_fee = 25.0; + let expected_medium_fee = 50.0; + let expected_high_fee = 75.0; + let expected_very_high_fee = 96.0; + let expected_max_fee = 100.0; + assert_eq!(estimates.min, expected_min_fee); + assert_eq!(estimates.low, expected_low_fee); + assert_eq!(estimates.medium, expected_medium_fee); + assert_eq!(estimates.high, expected_high_fee); + assert_eq!(estimates.very_high, expected_very_high_fee); + assert_eq!(estimates.unsafe_max, expected_max_fee); + // NOTE: calculation 2 + } #[tokio::test] async fn test_specific_fee_estimates() { @@ -395,7 +608,7 @@ mod tests { let expected_low_fee = 25.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; + let expected_very_high_fee = 96.0; let expected_max_fee = 100.0; assert_eq!(estimates.min, expected_min_fee); assert_eq!(estimates.low, expected_low_fee); @@ -417,7 +630,7 @@ mod tests { let expected_low_fee = 25.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; + let expected_very_high_fee = 96.0; let expected_max_fee = 100.0; assert_eq!(estimates.min, expected_min_fee); assert_eq!(estimates.low, expected_low_fee); @@ -439,7 +652,7 @@ mod tests { let expected_low_fee = 25.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; + let expected_very_high_fee = 96.0; let expected_max_fee = 100.0; assert_eq!(estimates.min, expected_min_fee); assert_eq!(estimates.low, expected_low_fee); @@ -458,10 +671,10 @@ mod tests { ); // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value let expected_min_fee = 0.0; - let expected_low_fee = 0.0; - let expected_medium_fee = 25.5; - let expected_high_fee = 62.75; - let expected_very_high_fee = 92.54999999999998; + let expected_low_fee = 25.0; + let expected_medium_fee = 50.0; + let expected_high_fee = 75.0; + let expected_very_high_fee = 96.0; let expected_max_fee = 100.0; assert_eq!(estimates.min, expected_min_fee); assert_eq!(estimates.low, expected_low_fee); @@ -472,7 +685,7 @@ mod tests { } #[tokio::test] - async fn test_specific_fee_estimates_v2() { + async fn test_specific_fee_estimates_with_no_account_v2() { init_metrics(); let tracker = PriorityFeeTracker::new(10); @@ -505,7 +718,7 @@ mod tests { let expected_low_fee = 25.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; + let expected_very_high_fee = 96.0; let expected_max_fee = 100.0; assert_eq!(estimates.min, expected_min_fee); assert_eq!(estimates.low, expected_low_fee); @@ -527,7 +740,7 @@ mod tests { let expected_low_fee = 25.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; + let expected_very_high_fee = 96.0; let expected_max_fee = 100.0; assert_eq!(estimates.min, expected_min_fee); assert_eq!(estimates.low, expected_low_fee); @@ -549,7 +762,7 @@ mod tests { let expected_low_fee = 25.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; + let expected_very_high_fee = 96.0; let expected_max_fee = 100.0; assert_eq!(estimates.min, expected_min_fee); assert_eq!(estimates.low, expected_low_fee); @@ -571,7 +784,7 @@ mod tests { let expected_low_fee = 25.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; + let expected_very_high_fee = 96.0; let expected_max_fee = 100.0; assert_eq!(estimates.min, expected_min_fee); assert_eq!(estimates.low, expected_low_fee); @@ -616,7 +829,7 @@ mod tests { let expected_low_fee = 25.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; + let expected_very_high_fee = 96.0; let expected_max_fee = 100.0; assert_eq!(estimates.min, expected_min_fee); assert_eq!(estimates.low, expected_low_fee); @@ -637,7 +850,7 @@ mod tests { let expected_low_fee = 25.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; + let expected_very_high_fee = 96.0; let expected_max_fee = 100.0; assert_eq!(estimates.min, expected_min_fee); assert_eq!(estimates.low, expected_low_fee); @@ -658,7 +871,7 @@ mod tests { let expected_low_fee = 25.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; + let expected_very_high_fee = 96.0; let expected_max_fee = 100.0; assert_eq!(estimates.min, expected_min_fee); assert_eq!(estimates.low, expected_low_fee); @@ -676,10 +889,10 @@ mod tests { &tracker, ); let expected_min_fee = 0.0; - let expected_low_fee = 0.0; - let expected_medium_fee = 25.5; - let expected_high_fee = 62.75; - let expected_very_high_fee = 92.54999999999998; + let expected_low_fee = 25.0; + let expected_medium_fee = 50.00; + let expected_high_fee = 75.00; + let expected_very_high_fee = 96.00; let expected_max_fee = 100.0; assert_eq!(estimates.min, expected_min_fee); assert_eq!(estimates.low, expected_low_fee); @@ -723,7 +936,7 @@ mod tests { let expected_low_fee = 25.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; + let expected_very_high_fee = 96.0; let expected_max_fee = 100.0; assert_eq!(estimates.min, expected_min_fee); assert_eq!(estimates.low, expected_low_fee); @@ -744,7 +957,7 @@ mod tests { let expected_low_fee = 25.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; + let expected_very_high_fee = 96.0; let expected_max_fee = 100.0; assert_eq!(estimates.min, expected_min_fee); assert_eq!(estimates.low, expected_low_fee); @@ -765,7 +978,7 @@ mod tests { let expected_low_fee = 25.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; + let expected_very_high_fee = 96.0; let expected_max_fee = 100.0; assert_eq!(estimates.min, expected_min_fee); assert_eq!(estimates.low, expected_low_fee); @@ -786,7 +999,7 @@ mod tests { let expected_low_fee = 25.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; + let expected_very_high_fee = 96.0; let expected_max_fee = 100.0; assert_eq!(estimates.min, expected_min_fee); assert_eq!(estimates.low, expected_low_fee); @@ -838,14 +1051,14 @@ mod tests { &None, &tracker, ); - let expected_low_fee = 25.0; + let expected_low_fee = 24.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; - assert_ne!(estimates.low, expected_low_fee); - assert_ne!(estimates.medium, expected_medium_fee); - assert_ne!(estimates.high, expected_high_fee); - assert_ne!(estimates.very_high, expected_very_high_fee); + let expected_very_high_fee = 96.0; + assert_eq!(estimates.low, expected_low_fee); + assert_eq!(estimates.medium, expected_medium_fee); + assert_eq!(estimates.high, expected_high_fee); + assert_eq!(estimates.very_high, expected_very_high_fee); // Now test the fee estimates for a known priority level, let's say medium (50th percentile) let estimates = calculation1( @@ -855,14 +1068,14 @@ mod tests { &Some(150), &tracker, ); - let expected_low_fee = 25.0; + let expected_low_fee = 24.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; - assert_ne!(estimates.low, expected_low_fee); - assert_ne!(estimates.medium, expected_medium_fee); - assert_ne!(estimates.high, expected_high_fee); - assert_ne!(estimates.very_high, expected_very_high_fee); + let expected_very_high_fee = 96.0; + assert_eq!(estimates.low, expected_low_fee); + assert_eq!(estimates.medium, expected_medium_fee); + assert_eq!(estimates.high, expected_high_fee); + assert_eq!(estimates.very_high, expected_very_high_fee); // Now test the fee estimates for a known priority level, let's say medium (50th percentile) let estimates = calculation1( @@ -872,14 +1085,14 @@ mod tests { &None, &tracker, ); - let expected_low_fee = 25.0; + let expected_low_fee = 24.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; - assert_ne!(estimates.low, expected_low_fee); - assert_ne!(estimates.medium, expected_medium_fee); - assert_ne!(estimates.high, expected_high_fee); - assert_ne!(estimates.very_high, expected_very_high_fee); + let expected_very_high_fee = 96.0; + assert_eq!(estimates.low, expected_low_fee); + assert_eq!(estimates.medium, expected_medium_fee); + assert_eq!(estimates.high, expected_high_fee); + assert_eq!(estimates.very_high, expected_very_high_fee); // Now test the fee estimates for a known priority level, let's say medium (50th percentile) let estimates = calculation1( @@ -889,14 +1102,14 @@ mod tests { &Some(150), &tracker, ); - let expected_low_fee = 25.0; + let expected_low_fee = 24.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; - assert_ne!(estimates.low, expected_low_fee); - assert_ne!(estimates.medium, expected_medium_fee); - assert_ne!(estimates.high, expected_high_fee); - assert_ne!(estimates.very_high, expected_very_high_fee); + let expected_very_high_fee = 96.0; + assert_eq!(estimates.low, expected_low_fee); + assert_eq!(estimates.medium, expected_medium_fee); + assert_eq!(estimates.high, expected_high_fee); + assert_eq!(estimates.very_high, expected_very_high_fee); } #[tokio::test] @@ -943,14 +1156,14 @@ mod tests { &None, &tracker, ); - let expected_low_fee = 25.0; + let expected_low_fee = 24.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; - assert_ne!(estimates.low, expected_low_fee); - assert_ne!(estimates.medium, expected_medium_fee); - assert_ne!(estimates.high, expected_high_fee); - assert_ne!(estimates.very_high, expected_very_high_fee); + let expected_very_high_fee = 96.0; + assert_eq!(estimates.low, expected_low_fee); + assert_eq!(estimates.medium, expected_medium_fee); + assert_eq!(estimates.high, expected_high_fee); + assert_eq!(estimates.very_high, expected_very_high_fee); // Now test the fee estimates for a known priority level, let's say medium (50th percentile) let estimates = calculation2( @@ -960,14 +1173,14 @@ mod tests { &Some(150), &tracker, ); - let expected_low_fee = 25.0; + let expected_low_fee = 24.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; - assert_ne!(estimates.low, expected_low_fee); - assert_ne!(estimates.medium, expected_medium_fee); - assert_ne!(estimates.high, expected_high_fee); - assert_ne!(estimates.very_high, expected_very_high_fee); + let expected_very_high_fee = 96.0; + assert_eq!(estimates.low, expected_low_fee); + assert_eq!(estimates.medium, expected_medium_fee); + assert_eq!(estimates.high, expected_high_fee); + assert_eq!(estimates.very_high, expected_very_high_fee); // Now test the fee estimates for a known priority level, let's say medium (50th percentile) let estimates = calculation2( @@ -977,14 +1190,14 @@ mod tests { &None, &tracker, ); - let expected_low_fee = 25.0; + let expected_low_fee = 24.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; - assert_ne!(estimates.low, expected_low_fee); - assert_ne!(estimates.medium, expected_medium_fee); - assert_ne!(estimates.high, expected_high_fee); - assert_ne!(estimates.very_high, expected_very_high_fee); + let expected_very_high_fee = 96.0; + assert_eq!(estimates.low, expected_low_fee); + assert_eq!(estimates.medium, expected_medium_fee); + assert_eq!(estimates.high, expected_high_fee); + assert_eq!(estimates.very_high, expected_very_high_fee); // Now test the fee estimates for a known priority level, let's say medium (50th percentile) let estimates = calculation2( @@ -994,14 +1207,14 @@ mod tests { &Some(150), &tracker, ); - let expected_low_fee = 25.0; + let expected_low_fee = 24.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; - assert_ne!(estimates.low, expected_low_fee); - assert_ne!(estimates.medium, expected_medium_fee); - assert_ne!(estimates.high, expected_high_fee); - assert_ne!(estimates.very_high, expected_very_high_fee); + let expected_very_high_fee = 96.0; + assert_eq!(estimates.low, expected_low_fee); + assert_eq!(estimates.medium, expected_medium_fee); + assert_eq!(estimates.high, expected_high_fee); + assert_eq!(estimates.very_high, expected_very_high_fee); } #[tokio::test] @@ -1035,7 +1248,7 @@ mod tests { let expected_low_fee = 25.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; + let expected_very_high_fee = 96.0; let expected_max_fee = 100.0; assert_eq!(estimates.min, expected_min_fee); assert_eq!(estimates.low, expected_low_fee); @@ -1050,7 +1263,7 @@ mod tests { let expected_low_fee = 25.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; + let expected_very_high_fee = 96.0; let expected_max_fee = 100.0; assert_eq!(estimates.min, expected_min_fee); assert_eq!(estimates.low, expected_low_fee); @@ -1065,7 +1278,7 @@ mod tests { let expected_low_fee = 25.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; + let expected_very_high_fee = 96.0; let expected_max_fee = 100.0; assert_eq!(estimates.min, expected_min_fee); assert_eq!(estimates.low, expected_low_fee); @@ -1077,10 +1290,10 @@ mod tests { let estimates = calculation1(&v, false, true, &Some(150), &tracker); // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value let expected_min_fee = 0.0; - let expected_low_fee = 0.0; - let expected_medium_fee = 25.5; - let expected_high_fee = 62.75; - let expected_very_high_fee = 92.54999999999998; + let expected_low_fee = 25.0; + let expected_medium_fee = 50.0; + let expected_high_fee = 75.0; + let expected_very_high_fee = 96.0; let expected_max_fee = 100.0; assert_eq!(estimates.min, expected_min_fee); assert_eq!(estimates.low, expected_low_fee); @@ -1121,7 +1334,7 @@ mod tests { let expected_low_fee = 25.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; + let expected_very_high_fee = 96.0; let expected_max_fee = 100.0; assert_eq!(estimates.min, expected_min_fee); assert_eq!(estimates.low, expected_low_fee); @@ -1136,7 +1349,7 @@ mod tests { let expected_low_fee = 25.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; + let expected_very_high_fee = 96.0; let expected_max_fee = 100.0; assert_eq!(estimates.min, expected_min_fee); assert_eq!(estimates.low, expected_low_fee); @@ -1151,7 +1364,7 @@ mod tests { let expected_low_fee = 25.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; + let expected_very_high_fee = 96.0; let expected_max_fee = 100.0; assert_eq!(estimates.min, expected_min_fee); assert_eq!(estimates.low, expected_low_fee); @@ -1166,7 +1379,7 @@ mod tests { let expected_low_fee = 25.0; let expected_medium_fee = 50.0; let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; + let expected_very_high_fee = 96.0; let expected_max_fee = 100.0; assert_eq!(estimates.min, expected_min_fee); assert_eq!(estimates.low, expected_low_fee); diff --git a/src/priority_fee_calculation.rs b/src/priority_fee_calculation.rs index 6c52e0b..a8c2242 100644 --- a/src/priority_fee_calculation.rs +++ b/src/priority_fee_calculation.rs @@ -1,23 +1,33 @@ -use crate::model::{ - MicroLamportPriorityFeeEstimates, Percentile, PriorityFeesBySlot, PriorityLevel, -}; +use crate::model::PriorityFeesBySlot; use crate::priority_fee_calculation::Calculations::Calculation1; use cadence_macros::{statsd_count, statsd_gauge}; use solana_sdk::pubkey::Pubkey; +use statrs::statistics::Data; use std::collections::HashMap; use std::time::Instant; use Calculations::Calculation2; +#[derive(Debug, Clone, PartialOrd, PartialEq, Eq, Ord, Hash)] +pub enum DataType<'a> { + Global, + AllAccounts, + Account(&'a Pubkey), +} +/// +/// The result with type of +/// +pub type DataStats<'a> = HashMap, Data>>; + #[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] pub enum Calculations<'a> { Calculation1 { - accounts: &'a Vec, + accounts: &'a [Pubkey], include_vote: bool, include_empty_slots: bool, lookback_period: &'a Option, }, Calculation2 { - accounts: &'a Vec, + accounts: &'a [Pubkey], include_vote: bool, include_empty_slots: bool, lookback_period: &'a Option, @@ -26,7 +36,7 @@ pub enum Calculations<'a> { impl<'a> Calculations<'a> { pub fn new_calculation1( - accounts: &'a Vec, + accounts: &'a [Pubkey], include_vote: bool, include_empty_slots: bool, lookback_period: &'a Option, @@ -40,7 +50,7 @@ impl<'a> Calculations<'a> { } pub fn new_calculation2( - accounts: &'a Vec, + accounts: &'a [Pubkey], include_vote: bool, include_empty_slots: bool, lookback_period: &'a Option, @@ -56,7 +66,7 @@ impl<'a> Calculations<'a> { pub fn get_priority_fee_estimates( &self, priority_fees: &PriorityFeesBySlot, - ) -> anyhow::Result { + ) -> anyhow::Result { let start = Instant::now(); let result = match self { @@ -105,9 +115,11 @@ impl<'a> Calculations<'a> { } mod v1 { - use crate::model::{MicroLamportPriorityFeeEstimates, PriorityFeesBySlot}; - use crate::priority_fee_calculation::max_percentile; + use crate::model::PriorityFeesBySlot; + use crate::priority_fee_calculation::{calculate_lookback_size, DataStats, DataType}; + use solana_sdk::clock::Slot; use solana_sdk::pubkey::Pubkey; + use statrs::statistics::Data; /// /// Algo1: given the list of accounts the algorithm will: @@ -116,64 +128,66 @@ mod v1 { /// 3. will calculate the percentile distributions for each of two groups /// 4. will choose the highest value from each percentile between two groups /// - pub(crate) fn get_priority_fee_estimates( - accounts: &[Pubkey], + pub(crate) fn get_priority_fee_estimates<'a>( + accounts: &'a [Pubkey], include_vote: &bool, include_empty_slots: &bool, lookback_period: &Option, priority_fees: &PriorityFeesBySlot, - ) -> anyhow::Result { - let mut account_fees = vec![]; - let mut transaction_fees = vec![]; - for (i, slot_priority_fees) in priority_fees.iter().enumerate() { - if let Some(lookback_period) = lookback_period { - if i >= *lookback_period as usize { - break; + ) -> anyhow::Result> { + let mut slots_vec: Vec = priority_fees.iter().map(|entry| entry.slot).collect(); + slots_vec.sort(); + slots_vec.reverse(); + + let slots_vec = slots_vec; + + let lookback = calculate_lookback_size(&lookback_period, slots_vec.len()); + + let mut global_fees: Vec = Vec::new(); + let mut account_fees: Vec = Vec::new(); + for slot in &slots_vec[..lookback] { + if let Some(slot_priority_fees) = priority_fees.get(slot) { + if *include_vote { + global_fees.extend_from_slice(&slot_priority_fees.fees.vote_fees); } - } - if *include_vote { - transaction_fees.extend_from_slice(&slot_priority_fees.fees.vote_fees); - } - transaction_fees.extend_from_slice(&slot_priority_fees.fees.non_vote_fees); - for account in accounts { - let account_priority_fees = slot_priority_fees.account_fees.get(account); - if let Some(account_priority_fees) = account_priority_fees { - if *include_vote { - account_fees.extend_from_slice(&account_priority_fees.vote_fees); + global_fees.extend_from_slice(&slot_priority_fees.fees.non_vote_fees); + + if 0 < accounts.len() { + let mut has_data = false; + accounts.iter().for_each(|account| { + if let Some(account_priority_fees) = + slot_priority_fees.account_fees.get(account) + { + if *include_vote { + account_fees.extend_from_slice(&account_priority_fees.vote_fees); + } + account_fees.extend_from_slice(&account_priority_fees.non_vote_fees); + has_data = true; + } + }); + if !has_data { + if *include_empty_slots { + account_fees.push(0f64); + } } - account_fees.extend_from_slice(&account_priority_fees.non_vote_fees); } } } - if *include_empty_slots { - let lookback = lookback_period - .map(|v| v as usize) - .unwrap_or(priority_fees.len()); - let account_max_size = account_fees.len().max(lookback); - let transaction_max_size = transaction_fees.len().max(lookback); - // if there are less data than number of slots - append 0s for up to number of slots so we don't overestimate the values - account_fees.resize(account_max_size, 0f64); - transaction_fees.resize(transaction_max_size, 0f64); - } - - let micro_lamport_priority_fee_estimates = MicroLamportPriorityFeeEstimates { - min: max_percentile(&mut account_fees, &mut transaction_fees, 0), - low: max_percentile(&mut account_fees, &mut transaction_fees, 25), - medium: max_percentile(&mut account_fees, &mut transaction_fees, 50), - high: max_percentile(&mut account_fees, &mut transaction_fees, 75), - very_high: max_percentile(&mut account_fees, &mut transaction_fees, 95), - unsafe_max: max_percentile(&mut account_fees, &mut transaction_fees, 100), - }; - Ok(micro_lamport_priority_fee_estimates) + let mut data = DataStats::new(); + data.insert(DataType::Global, Data::new(global_fees)); + data.insert(DataType::AllAccounts, Data::new(account_fees)); + Ok(data) } } mod v2 { - use crate::model::{MicroLamportPriorityFeeEstimates, PriorityFeesBySlot}; - use crate::priority_fee_calculation::{calculate_lookback_size, estimate_max_values}; + use crate::model::PriorityFeesBySlot; + use crate::priority_fee_calculation::{calculate_lookback_size, DataStats, DataType}; use solana_sdk::clock::Slot; use solana_sdk::pubkey::Pubkey; + use statrs::statistics::Data; + use std::collections::HashMap; /// /// Algo2: given the list of accounts the algorithm will: @@ -181,14 +195,14 @@ mod v2 { /// 2. for each specified account collect the fees and calculate the percentiles /// 4. choose maximum values for each percentile between all transactions and each account /// - pub(crate) fn get_priority_fee_estimates( - accounts: &[Pubkey], + pub(crate) fn get_priority_fee_estimates<'a>( + accounts: &'a [Pubkey], include_vote: &bool, include_empty_slots: &bool, lookback_period: &Option, priority_fees: &PriorityFeesBySlot, - ) -> anyhow::Result { - let mut slots_vec: Vec = priority_fees.iter().map(|value| value.slot).collect(); + ) -> anyhow::Result> { + let mut slots_vec: Vec = priority_fees.iter().map(|entry| entry.slot).collect(); slots_vec.sort(); slots_vec.reverse(); @@ -196,98 +210,46 @@ mod v2 { let lookback = calculate_lookback_size(&lookback_period, slots_vec.len()); - let mut fees = Vec::with_capacity(slots_vec.len()); - let mut micro_lamport_priority_fee_estimates = MicroLamportPriorityFeeEstimates::default(); - + let mut data: HashMap, Vec> = HashMap::new(); for slot in &slots_vec[..lookback] { if let Some(slot_priority_fees) = priority_fees.get(slot) { + let fees: &mut Vec = data.entry(DataType::Global).or_insert(Vec::new()); + if *include_vote { fees.extend_from_slice(&slot_priority_fees.fees.vote_fees); } fees.extend_from_slice(&slot_priority_fees.fees.non_vote_fees); - } - } - micro_lamport_priority_fee_estimates = - estimate_max_values(&mut fees, micro_lamport_priority_fee_estimates); - for account in accounts { - fees.clear(); - let mut zero_slots: usize = 0; - for slot in &slots_vec[..lookback] { - if let Some(slot_priority_fees) = priority_fees.get(slot) { - let account_priority_fees = slot_priority_fees.account_fees.get(account); - if let Some(account_priority_fees) = account_priority_fees { + accounts.iter().for_each(|account| { + /* + Always insert the fees list for every account + If account has no fees than we can fill the buckets with 0 for every slot missed + */ + let fees: &mut Vec = + data.entry(DataType::Account(account)).or_insert(Vec::new()); + if let Some(account_priority_fees) = + slot_priority_fees.account_fees.get(account) + { if *include_vote { fees.extend_from_slice(&account_priority_fees.vote_fees); } fees.extend_from_slice(&account_priority_fees.non_vote_fees); - } else { - zero_slots += 1; + } else if *include_empty_slots { + // for all empty slot we need to add a 0 + fees.push(0f64); } - } else { - zero_slots += 1; - } - } - if *include_empty_slots { - // for all empty slot we need to add a 0 - fees.resize(fees.len() + zero_slots, 0f64); + }); } - micro_lamport_priority_fee_estimates = - estimate_max_values(&mut fees, micro_lamport_priority_fee_estimates); } - Ok(micro_lamport_priority_fee_estimates) - } -} - -fn estimate_max_values( - mut fees: &mut Vec, - mut estimates: MicroLamportPriorityFeeEstimates, -) -> MicroLamportPriorityFeeEstimates { - let vals: HashMap = percentile( - &mut fees, - &[ - PriorityLevel::Min.into(), - PriorityLevel::Low.into(), - PriorityLevel::Medium.into(), - PriorityLevel::High.into(), - PriorityLevel::VeryHigh.into(), - PriorityLevel::UnsafeMax.into(), - ], - ); - estimates.min = vals - .get(&PriorityLevel::Min.into()) - .unwrap_or(&estimates.min) - .max(estimates.min); - estimates.low = vals - .get(&PriorityLevel::Low.into()) - .unwrap_or(&estimates.low) - .max(estimates.low); - estimates.medium = vals - .get(&PriorityLevel::Medium.into()) - .unwrap_or(&estimates.medium) - .max(estimates.medium); - estimates.high = vals - .get(&PriorityLevel::High.into()) - .unwrap_or(&estimates.high) - .max(estimates.high); - estimates.very_high = vals - .get(&PriorityLevel::VeryHigh.into()) - .unwrap_or(&estimates.very_high) - .max(estimates.very_high); - estimates.unsafe_max = vals - .get(&PriorityLevel::UnsafeMax.into()) - .unwrap_or(&estimates.unsafe_max) - .max(estimates.unsafe_max); - estimates -} -fn max(a: f64, b: f64) -> f64 { - if a > b { - a - } else { - b + let data = data + .into_iter() + .map(|(data_type, fees)| (data_type, Data::new(fees))) + .collect::(); + Ok(data) } } + fn calculate_lookback_size(pref_num_slots: &Option, max_available_slots: usize) -> usize { max_available_slots.min( pref_num_slots @@ -296,54 +258,15 @@ fn calculate_lookback_size(pref_num_slots: &Option, max_available_slots: us ) } -fn max_percentile( - account_fees: &mut Vec, - transaction_fees: &mut Vec, - percentile_value: Percentile, -) -> f64 { - let results1 = percentile(account_fees, &[percentile_value]); - let results2 = percentile(transaction_fees, &[percentile_value]); - max( - *results1.get(&percentile_value).unwrap_or(&0f64), - *results2.get(&percentile_value).unwrap_or(&0f64), - ) -} - -// pulled from here - https://www.calculatorsoup.com/calculators/statistics/percentile-calculator.php -// couldn't find any good libraries that worked -fn percentile(values: &mut Vec, percentiles: &[Percentile]) -> HashMap { - if values.is_empty() { - return HashMap::with_capacity(0); - } - - values.sort_by(|a, b| a.partial_cmp(b).unwrap()); - let n = values.len() as f64; - - percentiles.into_iter().fold( - HashMap::with_capacity(percentiles.len()), - |mut data, &percentile| { - let r = (percentile as f64 / 100.0) * (n - 1.0) + 1.0; - - let val = if r.fract() == 0.0 { - values[r as usize - 1] - } else { - let ri = r.trunc() as usize - 1; - let rf = r.fract(); - values[ri] + rf * (values[ri + 1] - values[ri]) - }; - data.insert(percentile, val); - data - }, - ) -} - #[cfg(test)] mod tests { use super::*; use crate::model::{Fees, SlotPriorityFees}; + use crate::priority_fee_calculation::DataType::{Account, AllAccounts, Global}; use cadence::{NopMetricSink, StatsdClient}; use cadence_macros::set_global_default; use solana_sdk::clock::Slot; + use statrs::statistics::OrderStatistics; fn init_metrics() { let noop = NopMetricSink {}; @@ -351,8 +274,9 @@ mod tests { set_global_default(client) } + #[tokio::test] - async fn test_specific_fee_estimates() { + async fn test_specific_fee_estimates_for_global_accounts_only() { init_metrics(); let tracker = PriorityFeesBySlot::new(); @@ -372,102 +296,370 @@ mod tests { push_priority_fee_for_txn(1, accounts.clone(), fee as u64, false, &tracker); } - // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = Calculations::new_calculation1( - &vec![accounts.get(0).unwrap().clone()], - false, - false, - &None, - ) - .get_priority_fee_estimates(&tracker) - .expect("estimates to be valid"); + let accounts = vec![]; + + // Scenario 1: no vote transactions and no empty slots and default lookback period + let calc = Calculations::new_calculation1(&accounts, false, false, &None); + let mut estimates: DataStats = calc + .get_priority_fee_estimates(&tracker) + .expect("estimates to be valid"); + + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + assert_eq!(estimates.len(), 2); + { + let stats: &mut Data> = estimates.get_mut(&Global).unwrap(); + assert_eq!(stats.percentile(0).round(), 0.0); + assert_eq!(stats.percentile(25).round(), 25.0); + assert_eq!(stats.percentile(50).round(), 50.0); + assert_eq!(stats.percentile(75).round(), 75.0); + assert_eq!(stats.percentile(95).round(), 96.0); + assert_eq!(stats.percentile(99).round(), 100.0); + } + + { + let stats: &mut Data> = estimates.get_mut(&AllAccounts).unwrap(); + assert!(stats.percentile(0).is_nan()); + assert!(stats.percentile(25).is_nan()); + assert!(stats.percentile(50).is_nan()); + assert!(stats.percentile(75).is_nan()); + assert!(stats.percentile(95).is_nan()); + assert!(stats.percentile(99).is_nan()); + } + + + // Scenario 2: with vote transactions and no empty slots and default lookback period + let calc = Calculations::new_calculation1(&accounts, true, false, &None); + let mut estimates: DataStats = calc + .get_priority_fee_estimates(&tracker) + .expect("estimates to be valid"); + + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + assert_eq!(estimates.len(), 2); + { + let stats: &mut Data> = estimates.get_mut(&Global).unwrap(); + assert_eq!(stats.percentile(0).round(), 0.0); + assert_eq!(stats.percentile(25).round(), 25.0); + assert_eq!(stats.percentile(50).round(), 50.0); + assert_eq!(stats.percentile(75).round(), 75.0); + assert_eq!(stats.percentile(95).round(), 96.0); + assert_eq!(stats.percentile(99).round(), 100.0); + } + + { + let stats: &mut Data> = estimates.get_mut(&AllAccounts).unwrap(); + assert!(stats.percentile(0).is_nan()); + assert!(stats.percentile(25).is_nan()); + assert!(stats.percentile(50).is_nan()); + assert!(stats.percentile(75).is_nan()); + assert!(stats.percentile(95).is_nan()); + assert!(stats.percentile(99).is_nan()); + } + + + + + // Scenario 3: with vote transactions and empty slots and default lookback period + let calc = Calculations::new_calculation1(&accounts, true, true, &None); + let mut estimates: DataStats = calc + .get_priority_fee_estimates(&tracker) + .expect("estimates to be valid"); + + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + assert_eq!(estimates.len(), 2); + { + let stats: &mut Data> = estimates.get_mut(&Global).unwrap(); + assert_eq!(stats.percentile(0).round(), 0.0); + assert_eq!(stats.percentile(25).round(), 25.0); + assert_eq!(stats.percentile(50).round(), 50.0); + assert_eq!(stats.percentile(75).round(), 75.0); + assert_eq!(stats.percentile(95).round(), 96.0); + assert_eq!(stats.percentile(99).round(), 100.0); + } + + { + let stats: &mut Data> = estimates.get_mut(&AllAccounts).unwrap(); + assert!(stats.percentile(0).is_nan()); + assert!(stats.percentile(25).is_nan()); + assert!(stats.percentile(50).is_nan()); + assert!(stats.percentile(75).is_nan()); + assert!(stats.percentile(95).is_nan()); + assert!(stats.percentile(99).is_nan()); + } + + + + // Scenario 4: with vote transactions and empty slots and different lookback period + let calc = Calculations::new_calculation1(&accounts, true, true, &Some(1)); + let mut estimates: DataStats = calc + .get_priority_fee_estimates(&tracker) + .expect("estimates to be valid"); + + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + assert_eq!(estimates.len(), 2); + { + let stats: &mut Data> = estimates.get_mut(&Global).unwrap(); + assert_eq!(stats.percentile(0).round(), 0.0); + assert_eq!(stats.percentile(25).round(), 25.0); + assert_eq!(stats.percentile(50).round(), 50.0); + assert_eq!(stats.percentile(75).round(), 75.0); + assert_eq!(stats.percentile(95).round(), 96.0); + assert_eq!(stats.percentile(99).round(), 100.0); + } + + { + let stats: &mut Data> = estimates.get_mut(&AllAccounts).unwrap(); + assert!(stats.percentile(0).is_nan()); + assert!(stats.percentile(25).is_nan()); + assert!(stats.percentile(50).is_nan()); + assert!(stats.percentile(75).is_nan()); + assert!(stats.percentile(95).is_nan()); + assert!(stats.percentile(99).is_nan()); + } + } + + + #[tokio::test] + async fn test_specific_fee_estimates_for_global_accounts_only_v2() { + init_metrics(); + let tracker = PriorityFeesBySlot::new(); + + let mut fees = vec![]; + let mut i = 0; + while i <= 100 { + fees.push(i as f64); + i += 1; + } + let account_1 = Pubkey::new_unique(); + let account_2 = Pubkey::new_unique(); + let account_3 = Pubkey::new_unique(); + let accounts = vec![account_1, account_2, account_3]; + + // Simulate adding the fixed fees as both account-specific and transaction fees + for fee in fees.clone() { + push_priority_fee_for_txn(1, accounts.clone(), fee as u64, false, &tracker); + } + + let accounts = vec![]; + + // Scenario 1: no vote transactions and no empty slots and default lookback period + let calc = Calculations::new_calculation2(&accounts, false, false, &None); + let mut estimates: DataStats = calc + .get_priority_fee_estimates(&tracker) + .expect("estimates to be valid"); // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value - let expected_min_fee = 0.0; - let expected_low_fee = 25.0; - let expected_medium_fee = 50.0; - let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; - let expected_max_fee = 100.0; - assert_eq!(estimates.min, expected_min_fee); - assert_eq!(estimates.low, expected_low_fee); - assert_eq!(estimates.medium, expected_medium_fee); - assert_eq!(estimates.high, expected_high_fee); - assert_eq!(estimates.very_high, expected_very_high_fee); - assert_eq!(estimates.unsafe_max, expected_max_fee); - - // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - - let estimates = Calculations::new_calculation1( - &vec![accounts.get(0).unwrap().clone()], - false, - false, - &Some(150), - ) - .get_priority_fee_estimates(&tracker) - .expect("estimates to be valid"); + assert_eq!(estimates.len(), 1); + { + let stats: &mut Data> = estimates.get_mut(&Global).unwrap(); + assert_eq!(stats.percentile(0).round(), 0.0); + assert_eq!(stats.percentile(25).round(), 25.0); + assert_eq!(stats.percentile(50).round(), 50.0); + assert_eq!(stats.percentile(75).round(), 75.0); + assert_eq!(stats.percentile(95).round(), 96.0); + assert_eq!(stats.percentile(99).round(), 100.0); + } + + // Scenario 2: with vote transactions and no empty slots and default lookback period + let calc = Calculations::new_calculation2(&accounts, true, false, &None); + let mut estimates: DataStats = calc + .get_priority_fee_estimates(&tracker) + .expect("estimates to be valid"); + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value - let expected_min_fee = 0.0; - let expected_low_fee = 25.0; - let expected_medium_fee = 50.0; - let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; - let expected_max_fee = 100.0; - assert_eq!(estimates.min, expected_min_fee); - assert_eq!(estimates.low, expected_low_fee); - assert_eq!(estimates.medium, expected_medium_fee); - assert_eq!(estimates.high, expected_high_fee); - assert_eq!(estimates.very_high, expected_very_high_fee); - assert_eq!(estimates.unsafe_max, expected_max_fee); - - // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = Calculations::new_calculation1( - &vec![accounts.get(0).unwrap().clone()], - false, - true, - &None, - ) - .get_priority_fee_estimates(&tracker) - .expect("estimates to be valid"); + assert_eq!(estimates.len(), 1); + { + let stats: &mut Data> = estimates.get_mut(&Global).unwrap(); + assert_eq!(stats.percentile(0).round(), 0.0); + assert_eq!(stats.percentile(25).round(), 25.0); + assert_eq!(stats.percentile(50).round(), 50.0); + assert_eq!(stats.percentile(75).round(), 75.0); + assert_eq!(stats.percentile(95).round(), 96.0); + assert_eq!(stats.percentile(99).round(), 100.0); + } + + + // Scenario 3: with vote transactions and empty slots and default lookback period + let calc = Calculations::new_calculation2(&accounts, true, true, &None); + let mut estimates: DataStats = calc + .get_priority_fee_estimates(&tracker) + .expect("estimates to be valid"); + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value - let expected_min_fee = 0.0; - let expected_low_fee = 25.0; - let expected_medium_fee = 50.0; - let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; - let expected_max_fee = 100.0; - assert_eq!(estimates.min, expected_min_fee); - assert_eq!(estimates.low, expected_low_fee); - assert_eq!(estimates.medium, expected_medium_fee); - assert_eq!(estimates.high, expected_high_fee); - assert_eq!(estimates.very_high, expected_very_high_fee); - assert_eq!(estimates.unsafe_max, expected_max_fee); - - // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = Calculations::new_calculation1( - &vec![accounts.get(0).unwrap().clone()], - false, - true, - &Some(150), - ) - .get_priority_fee_estimates(&tracker) - .expect("estimates to be valid"); + assert_eq!(estimates.len(), 1); + { + let stats: &mut Data> = estimates.get_mut(&Global).unwrap(); + assert_eq!(stats.percentile(0).round(), 0.0); + assert_eq!(stats.percentile(25).round(), 25.0); + assert_eq!(stats.percentile(50).round(), 50.0); + assert_eq!(stats.percentile(75).round(), 75.0); + assert_eq!(stats.percentile(95).round(), 96.0); + assert_eq!(stats.percentile(99).round(), 100.0); + } + + + // Scenario 4: with vote transactions and empty slots and different lookback period + let calc = Calculations::new_calculation2(&accounts, true, true, &Some(1)); + let mut estimates: DataStats = calc + .get_priority_fee_estimates(&tracker) + .expect("estimates to be valid"); + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value - let expected_min_fee = 0.0; - let expected_low_fee = 0.0; - let expected_medium_fee = 25.5; - let expected_high_fee = 62.75; - let expected_very_high_fee = 92.54999999999998; - let expected_max_fee = 100.0; - assert_eq!(estimates.min, expected_min_fee); - assert_eq!(estimates.low, expected_low_fee); - assert_eq!(estimates.medium, expected_medium_fee); - assert_eq!(estimates.high, expected_high_fee); - assert_eq!(estimates.very_high, expected_very_high_fee); - assert_eq!(estimates.unsafe_max, expected_max_fee); + assert_eq!(estimates.len(), 1); + { + let stats: &mut Data> = estimates.get_mut(&Global).unwrap(); + assert_eq!(stats.percentile(0).round(), 0.0); + assert_eq!(stats.percentile(25).round(), 25.0); + assert_eq!(stats.percentile(50).round(), 50.0); + assert_eq!(stats.percentile(75).round(), 75.0); + assert_eq!(stats.percentile(95).round(), 96.0); + assert_eq!(stats.percentile(99).round(), 100.0); + } + } #[tokio::test] + async fn test_specific_fee_estimates() { + init_metrics(); + let tracker = PriorityFeesBySlot::new(); + + let mut fees = vec![]; + let mut i = 0; + while i <= 100 { + fees.push(i as f64); + i += 1; + } + let account_1 = Pubkey::new_unique(); + let account_2 = Pubkey::new_unique(); + let account_3 = Pubkey::new_unique(); + let accounts = vec![account_1, account_2, account_3]; + + // Simulate adding the fixed fees as both account-specific and transaction fees + for fee in fees.clone() { + push_priority_fee_for_txn(1, accounts.clone(), fee as u64, false, &tracker); + } + + // Scenario 1: no vote transactions and no empty slots and default lookback period + let calc = Calculations::new_calculation1(&accounts[..=0], false, false, &None); + let mut estimates: DataStats = calc + .get_priority_fee_estimates(&tracker) + .expect("estimates to be valid"); + + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + assert_eq!(estimates.len(), 2); + { + let stats: &mut Data> = estimates.get_mut(&Global).unwrap(); + assert_eq!(stats.percentile(0).round(), 0.0); + assert_eq!(stats.percentile(25).round(), 25.0); + assert_eq!(stats.percentile(50).round(), 50.0); + assert_eq!(stats.percentile(75).round(), 75.0); + assert_eq!(stats.percentile(95).round(), 96.0); + assert_eq!(stats.percentile(99).round(), 100.0); + } + + { + let stats: &mut Data> = estimates.get_mut(&AllAccounts).unwrap(); + assert_eq!(stats.percentile(0).round(), 0.0); + assert_eq!(stats.percentile(25).round(), 25.0); + assert_eq!(stats.percentile(50).round(), 50.0); + assert_eq!(stats.percentile(75).round(), 75.0); + assert_eq!(stats.percentile(95).round(), 96.0); + assert_eq!(stats.percentile(99).round(), 100.0); + } + + + + // Scenario 2: no vote transactions and no empty slots and default lookback period + let calc = Calculations::new_calculation1(&accounts[..=0], true, false, &None); + let mut estimates: DataStats = calc + .get_priority_fee_estimates(&tracker) + .expect("estimates to be valid"); + + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + assert_eq!(estimates.len(), 2); + { + let stats: &mut Data> = estimates.get_mut(&Global).unwrap(); + assert_eq!(stats.percentile(0).round(), 0.0); + assert_eq!(stats.percentile(25).round(), 25.0); + assert_eq!(stats.percentile(50).round(), 50.0); + assert_eq!(stats.percentile(75).round(), 75.0); + assert_eq!(stats.percentile(95).round(), 96.0); + assert_eq!(stats.percentile(99).round(), 100.0); + } + + { + let stats: &mut Data> = estimates.get_mut(&AllAccounts).unwrap(); + assert_eq!(stats.percentile(0).round(), 0.0); + assert_eq!(stats.percentile(25).round(), 25.0); + assert_eq!(stats.percentile(50).round(), 50.0); + assert_eq!(stats.percentile(75).round(), 75.0); + assert_eq!(stats.percentile(95).round(), 96.0); + assert_eq!(stats.percentile(99).round(), 100.0); + + } + + + // Scenario 1: no vote transactions and no empty slots and default lookback period + let calc = Calculations::new_calculation1(&accounts[..=0], true, true, &None); + let mut estimates: DataStats = calc + .get_priority_fee_estimates(&tracker) + .expect("estimates to be valid"); + + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + assert_eq!(estimates.len(), 2); + { + let stats: &mut Data> = estimates.get_mut(&Global).unwrap(); + assert_eq!(stats.percentile(0).round(), 0.0); + assert_eq!(stats.percentile(25).round(), 25.0); + assert_eq!(stats.percentile(50).round(), 50.0); + assert_eq!(stats.percentile(75).round(), 75.0); + assert_eq!(stats.percentile(95).round(), 96.0); + assert_eq!(stats.percentile(99).round(), 100.0); + } + + { + let stats: &mut Data> = estimates.get_mut(&AllAccounts).unwrap(); + assert_eq!(stats.percentile(0).round(), 0.0); + assert_eq!(stats.percentile(25).round(), 25.0); + assert_eq!(stats.percentile(50).round(), 50.0); + assert_eq!(stats.percentile(75).round(), 75.0); + assert_eq!(stats.percentile(95).round(), 96.0); + assert_eq!(stats.percentile(99).round(), 100.0); + + } + + + + // Scenario 1: no vote transactions and no empty slots and default lookback period + let calc = Calculations::new_calculation1(&accounts[..=0], true, true, &Some(1)); + let mut estimates: DataStats = calc + .get_priority_fee_estimates(&tracker) + .expect("estimates to be valid"); + + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + assert_eq!(estimates.len(), 2); + { + let stats: &mut Data> = estimates.get_mut(&Global).unwrap(); + assert_eq!(stats.percentile(0).round(), 0.0); + assert_eq!(stats.percentile(25).round(), 25.0); + assert_eq!(stats.percentile(50).round(), 50.0); + assert_eq!(stats.percentile(75).round(), 75.0); + assert_eq!(stats.percentile(95).round(), 96.0); + assert_eq!(stats.percentile(99).round(), 100.0); + } + + { + let stats: &mut Data> = estimates.get_mut(&AllAccounts).unwrap(); + assert_eq!(stats.percentile(0).round(), 0.0); + assert_eq!(stats.percentile(25).round(), 25.0); + assert_eq!(stats.percentile(50).round(), 50.0); + assert_eq!(stats.percentile(75).round(), 75.0); + assert_eq!(stats.percentile(95).round(), 96.0); + assert_eq!(stats.percentile(99).round(), 100.0); + + } + } + + #[tokio::test] async fn test_specific_fee_estimates_v2() { init_metrics(); let tracker = PriorityFeesBySlot::new(); @@ -488,101 +680,128 @@ mod tests { push_priority_fee_for_txn(1, accounts.clone(), fee as u64, false, &tracker); } - // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = Calculations::new_calculation2( - &vec![accounts.get(0).unwrap().clone()], - false, - false, - &None, - ) - .get_priority_fee_estimates(&tracker) - .expect("estimates to be valid"); + // Scenario 1: no vote transactions and no empty slots and default lookback period + let calc = Calculations::new_calculation2(&accounts[..=0], false, false, &None); + let mut estimates: DataStats = calc + .get_priority_fee_estimates(&tracker) + .expect("estimates to be valid"); + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value - let expected_min_fee = 0.0; - let expected_low_fee = 25.0; - let expected_medium_fee = 50.0; - let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; - let expected_max_fee = 100.0; - assert_eq!(estimates.min, expected_min_fee); - assert_eq!(estimates.low, expected_low_fee); - assert_eq!(estimates.medium, expected_medium_fee); - assert_eq!(estimates.high, expected_high_fee); - assert_eq!(estimates.very_high, expected_very_high_fee); - assert_eq!(estimates.unsafe_max, expected_max_fee); - - // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = Calculations::new_calculation2( - &vec![accounts.get(0).unwrap().clone()], - false, - false, - &Some(150), - ) - .get_priority_fee_estimates(&tracker) - .expect("estimates to be valid"); + assert_eq!(estimates.len(), 2); + { + let stats: &mut Data> = estimates.get_mut(&Global).unwrap(); + assert_eq!(stats.percentile(0).round(), 0.0); + assert_eq!(stats.percentile(25).round(), 25.0); + assert_eq!(stats.percentile(50).round(), 50.0); + assert_eq!(stats.percentile(75).round(), 75.0); + assert_eq!(stats.percentile(95).round(), 96.0); + assert_eq!(stats.percentile(99).round(), 100.0); + } + + { + let stats: &mut Data> = estimates.get_mut(&Account(&account_1)).unwrap(); + assert_eq!(stats.percentile(0).round(), 0.0); + assert_eq!(stats.percentile(25).round(), 25.0); + assert_eq!(stats.percentile(50).round(), 50.0); + assert_eq!(stats.percentile(75).round(), 75.0); + assert_eq!(stats.percentile(95).round(), 96.0); + assert_eq!(stats.percentile(99).round(), 100.0); + } + + + + // Scenario 2: no vote transactions and no empty slots and default lookback period + let calc = Calculations::new_calculation2(&accounts[..=0], true, false, &None); + let mut estimates: DataStats = calc + .get_priority_fee_estimates(&tracker) + .expect("estimates to be valid"); + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value - let expected_min_fee = 0.0; - let expected_low_fee = 25.0; - let expected_medium_fee = 50.0; - let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; - let expected_max_fee = 100.0; - assert_eq!(estimates.min, expected_min_fee); - assert_eq!(estimates.low, expected_low_fee); - assert_eq!(estimates.medium, expected_medium_fee); - assert_eq!(estimates.high, expected_high_fee); - assert_eq!(estimates.very_high, expected_very_high_fee); - assert_eq!(estimates.unsafe_max, expected_max_fee); - - // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = Calculations::new_calculation2( - &vec![accounts.get(0).unwrap().clone()], - false, - true, - &None, - ) - .get_priority_fee_estimates(&tracker) - .expect("estimates to be valid"); + assert_eq!(estimates.len(), 2); + { + let stats: &mut Data> = estimates.get_mut(&Global).unwrap(); + assert_eq!(stats.percentile(0).round(), 0.0); + assert_eq!(stats.percentile(25).round(), 25.0); + assert_eq!(stats.percentile(50).round(), 50.0); + assert_eq!(stats.percentile(75).round(), 75.0); + assert_eq!(stats.percentile(95).round(), 96.0); + assert_eq!(stats.percentile(99).round(), 100.0); + } + + { + let stats: &mut Data> = estimates.get_mut(&Account(&account_1)).unwrap(); + assert_eq!(stats.percentile(0).round(), 0.0); + assert_eq!(stats.percentile(25).round(), 25.0); + assert_eq!(stats.percentile(50).round(), 50.0); + assert_eq!(stats.percentile(75).round(), 75.0); + assert_eq!(stats.percentile(95).round(), 96.0); + assert_eq!(stats.percentile(99).round(), 100.0); + + } + + + // Scenario 1: no vote transactions and no empty slots and default lookback period + let calc = Calculations::new_calculation2(&accounts[..=0], true, true, &None); + let mut estimates: DataStats = calc + .get_priority_fee_estimates(&tracker) + .expect("estimates to be valid"); + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value - let expected_min_fee = 0.0; - let expected_low_fee = 25.0; - let expected_medium_fee = 50.0; - let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; - let expected_max_fee = 100.0; - assert_eq!(estimates.min, expected_min_fee); - assert_eq!(estimates.low, expected_low_fee); - assert_eq!(estimates.medium, expected_medium_fee); - assert_eq!(estimates.high, expected_high_fee); - assert_eq!(estimates.very_high, expected_very_high_fee); - assert_eq!(estimates.unsafe_max, expected_max_fee); - - // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = Calculations::new_calculation2( - &vec![accounts.get(0).unwrap().clone()], - false, - true, - &Some(150), - ) - .get_priority_fee_estimates(&tracker) - .expect("estimates to be valid"); + assert_eq!(estimates.len(), 2); + { + let stats: &mut Data> = estimates.get_mut(&Global).unwrap(); + assert_eq!(stats.percentile(0).round(), 0.0); + assert_eq!(stats.percentile(25).round(), 25.0); + assert_eq!(stats.percentile(50).round(), 50.0); + assert_eq!(stats.percentile(75).round(), 75.0); + assert_eq!(stats.percentile(95).round(), 96.0); + assert_eq!(stats.percentile(99).round(), 100.0); + } + + { + let stats: &mut Data> = estimates.get_mut(&Account(&account_1)).unwrap(); + assert_eq!(stats.percentile(0).round(), 0.0); + assert_eq!(stats.percentile(25).round(), 25.0); + assert_eq!(stats.percentile(50).round(), 50.0); + assert_eq!(stats.percentile(75).round(), 75.0); + assert_eq!(stats.percentile(95).round(), 96.0); + assert_eq!(stats.percentile(99).round(), 100.0); + + } + + + + // Scenario 1: no vote transactions and no empty slots and default lookback period + let calc = Calculations::new_calculation2(&accounts[..=0], true, true, &Some(1)); + let mut estimates: DataStats = calc + .get_priority_fee_estimates(&tracker) + .expect("estimates to be valid"); + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value - let expected_min_fee = 0.0; - let expected_low_fee = 25.0; - let expected_medium_fee = 50.0; - let expected_high_fee = 75.0; - let expected_very_high_fee = 95.0; - let expected_max_fee = 100.0; - assert_eq!(estimates.min, expected_min_fee); - assert_eq!(estimates.low, expected_low_fee); - assert_eq!(estimates.medium, expected_medium_fee); - assert_eq!(estimates.high, expected_high_fee); - assert_eq!(estimates.very_high, expected_very_high_fee); - assert_eq!(estimates.unsafe_max, expected_max_fee); - // NOTE: calculation 2 + assert_eq!(estimates.len(), 2); + { + let stats: &mut Data> = estimates.get_mut(&Global).unwrap(); + assert_eq!(stats.percentile(0).round(), 0.0); + assert_eq!(stats.percentile(25).round(), 25.0); + assert_eq!(stats.percentile(50).round(), 50.0); + assert_eq!(stats.percentile(75).round(), 75.0); + assert_eq!(stats.percentile(95).round(), 96.0); + assert_eq!(stats.percentile(99).round(), 100.0); + } + + { + let stats: &mut Data> = estimates.get_mut(&Account(&account_1)).unwrap(); + assert_eq!(stats.percentile(0).round(), 0.0); + assert_eq!(stats.percentile(25).round(), 25.0); + assert_eq!(stats.percentile(50).round(), 50.0); + assert_eq!(stats.percentile(75).round(), 75.0); + assert_eq!(stats.percentile(95).round(), 96.0); + assert_eq!(stats.percentile(99).round(), 100.0); + + } } - pub fn push_priority_fee_for_txn( + fn push_priority_fee_for_txn( slot: Slot, accounts: Vec, priority_fee: u64, diff --git a/src/rpc_server.rs b/src/rpc_server.rs index 5333924..ea5d371 100644 --- a/src/rpc_server.rs +++ b/src/rpc_server.rs @@ -589,10 +589,10 @@ mod tests { let resp = result.unwrap(); let levels = resp.priority_fee_levels.unwrap(); assert_eq!(levels.min, 100.0); - assert_eq!(levels.low, 125.0); + assert_eq!(levels.low, 100.0); assert_eq!(levels.medium, 150.0); - assert_eq!(levels.high, 175.0); - assert_eq!(levels.very_high, 195.0); + assert_eq!(levels.high, 200.0); + assert_eq!(levels.very_high, 200.0); assert_eq!(levels.unsafe_max, 200.0); assert!(resp.priority_fee_estimate.is_none()); } From 1f30181a367950f0e543c639cd8667c414940b13 Mon Sep 17 00:00:00 2001 From: Dmitriy Khomitskiy Date: Mon, 18 Nov 2024 11:35:05 -0600 Subject: [PATCH 3/5] RPC-468 adding response for detailed request --- src/model.rs | 40 +++- src/priority_fee.rs | 363 +++++++++++++++++++++++++++----- src/priority_fee_calculation.rs | 11 +- src/rpc_server.rs | 187 +++++++++++++++- 4 files changed, 523 insertions(+), 78 deletions(-) diff --git a/src/model.rs b/src/model.rs index de17286..37a16a0 100644 --- a/src/model.rs +++ b/src/model.rs @@ -2,6 +2,7 @@ use dashmap::DashMap; use serde::{Deserialize, Serialize}; use solana_sdk::clock::Slot; use solana_sdk::pubkey::Pubkey; +use std::fmt::{Debug, Display, Formatter}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub enum PriorityLevel { @@ -44,6 +45,23 @@ impl Into for PriorityLevel { pub type Percentile = usize; +#[derive(Debug, Clone, PartialOrd, PartialEq, Eq, Ord, Hash)] +pub enum DataType<'a> { + Global, + AllAccounts, + Account(&'a Pubkey), +} + +impl Display for DataType<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + DataType::Global => f.write_str("Global"), + DataType::AllAccounts => f.write_str("All Accounts"), + DataType::Account(pubkey) => f.write_str(pubkey.to_string().as_str()), + } + } +} + #[derive(Deserialize, Serialize, Debug, Clone, Default)] #[serde(rename_all(serialize = "camelCase", deserialize = "camelCase"))] pub struct MicroLamportPriorityFeeEstimates { @@ -55,6 +73,16 @@ pub struct MicroLamportPriorityFeeEstimates { pub unsafe_max: f64, } +#[derive(Deserialize, Serialize, Debug, Clone, Default)] +#[serde(rename_all(serialize = "camelCase", deserialize = "camelCase"))] +pub struct MicroLamportPriorityFeeDetails { + pub estimates: MicroLamportPriorityFeeEstimates, + pub mean: f64, + pub stdev: f64, + pub skew: f64, + pub count: usize, +} + #[derive(Debug, Clone)] pub struct Fees { pub(crate) non_vote_fees: Vec, @@ -100,9 +128,17 @@ impl SlotPriorityFees { account_fees.insert(account, fees.clone()); } if is_vote { - Self { slot, fees, account_fees } + Self { + slot, + fees, + account_fees, + } } else { - Self { slot, fees, account_fees } + Self { + slot, + fees, + account_fees, + } } } } diff --git a/src/priority_fee.rs b/src/priority_fee.rs index eae48d1..70ba734 100644 --- a/src/priority_fee.rs +++ b/src/priority_fee.rs @@ -1,7 +1,8 @@ use crate::errors::TransactionValidationError; use crate::grpc_consumer::GrpcConsumer; use crate::model::{ - Fees, MicroLamportPriorityFeeEstimates, PriorityFeesBySlot, PriorityLevel, SlotPriorityFees, + Fees, MicroLamportPriorityFeeDetails, MicroLamportPriorityFeeEstimates, PriorityFeesBySlot, + PriorityLevel, SlotPriorityFees, }; use crate::priority_fee_calculation::Calculations; use crate::priority_fee_calculation::Calculations::Calculation1; @@ -16,7 +17,8 @@ use solana_program_runtime::prioritization_fee::PrioritizationFeeDetails; use solana_sdk::instruction::CompiledInstruction; use solana_sdk::transaction::TransactionError; use solana_sdk::{pubkey::Pubkey, slot_history::Slot}; -use statrs::statistics::{Data, OrderStatistics}; +use statrs::statistics::{Data, Distribution, OrderStatistics}; +use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; use tracing::error; @@ -229,7 +231,7 @@ impl PriorityFeeTracker { } fn record_general_fees(&self) { - let global_fees = self.calculate_priority_fee_details(&Calculation1 { + let global_fees = self.calculate_priority_fee(&Calculation1 { accounts: &vec![], include_vote: false, include_empty_slots: false, @@ -303,7 +305,7 @@ impl PriorityFeeTracker { } } - pub fn calculate_priority_fee_details( + pub fn calculate_priority_fee( &self, calculation: &Calculations, ) -> anyhow::Result { @@ -314,6 +316,34 @@ impl PriorityFeeTracker { ); Ok(res) } + + pub fn calculate_priority_fee_details( + &self, + calculation: &Calculations, + ) -> anyhow::Result<(MicroLamportPriorityFeeEstimates, HashMap)> { + let data = calculation.get_priority_fee_estimates(&self.priority_fees)?; + let final_result: MicroLamportPriorityFeeEstimates = data.clone().into_iter().fold( + MicroLamportPriorityFeeEstimates::default(), + |estimate, mut data| estimate_max_values(&mut data.1, estimate), + ); + let results: HashMap = data + .into_iter() + .map(|mut data| { + let estimate = MicroLamportPriorityFeeDetails { + estimates: estimate_max_values( + &mut data.1, + MicroLamportPriorityFeeEstimates::default(), + ), + mean: data.1.mean().unwrap_or(f64::NAN).round(), + stdev: data.1.std_dev().unwrap_or(f64::NAN).round(), + skew: data.1.skewness().unwrap_or(f64::NAN).round(), + count: data.1.len(), + }; + (data.0.to_string(), estimate) + }) + .collect(); + Ok((final_result, results)) + } } fn estimate_max_values( @@ -371,6 +401,24 @@ mod tests { lookback_period: &Option, tracker: &PriorityFeeTracker, ) -> MicroLamportPriorityFeeEstimates { + let calc = Calculations::new_calculation1( + accounts, + include_vote, + include_empty_slot, + lookback_period, + ); + tracker + .calculate_priority_fee(&calc) + .expect(format!("estimates for calc1 to be valid with {:?}", calc).as_str()) + } + + fn calculation_details_1( + accounts: &Vec, + include_vote: bool, + include_empty_slot: bool, + lookback_period: &Option, + tracker: &PriorityFeeTracker, + ) -> HashMap { let calc = Calculations::new_calculation1( accounts, include_vote, @@ -389,6 +437,24 @@ mod tests { lookback_period: &Option, tracker: &PriorityFeeTracker, ) -> MicroLamportPriorityFeeEstimates { + let calc = Calculations::new_calculation2( + accounts, + include_vote, + include_empty_slot, + lookback_period, + ); + tracker + .calculate_priority_fee(&calc) + .expect(format!("estimates for calc2 to be valid with {:?}", calc).as_str()) + } + + fn calculation_details_2( + accounts: &Vec, + include_vote: bool, + include_empty_slot: bool, + lookback_period: &Option, + tracker: &PriorityFeeTracker, + ) -> HashMap { let calc = Calculations::new_calculation2( accounts, include_vote, @@ -399,6 +465,7 @@ mod tests { .calculate_priority_fee_details(&calc) .expect(format!("estimates for calc2 to be valid with {:?}", calc).as_str()) } + #[tokio::test] async fn test_specific_fee_estimates_with_no_account() { init_metrics(); @@ -438,7 +505,7 @@ mod tests { assert_eq!(estimates.unsafe_max, expected_max_fee); // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = calculation1(&acct, false, false, &Some(150), &tracker); + let estimates = calculation1(&acct, false, true, &None, &tracker); // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value let expected_min_fee = 0.0; let expected_low_fee = 25.0; @@ -454,7 +521,7 @@ mod tests { assert_eq!(estimates.unsafe_max, expected_max_fee); // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = calculation1(&acct, false, true, &None, &tracker); + let estimates = calculation1(&acct, false, true, &Some(150), &tracker); // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value let expected_min_fee = 0.0; let expected_low_fee = 25.0; @@ -470,7 +537,7 @@ mod tests { assert_eq!(estimates.unsafe_max, expected_max_fee); // Now test the fee estimates for a known priority level, let's say medium (50th percentile) - let estimates = calculation1(&acct, false, true, &Some(150), &tracker); + let estimates = calculation1(&acct, true, true, &Some(150), &tracker); // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value let expected_min_fee = 0.0; let expected_low_fee = 25.0; @@ -575,7 +642,133 @@ mod tests { } #[tokio::test] - async fn test_specific_fee_estimates() { + async fn test_specific_fee_estimates_details_with_no_account() { + init_metrics(); + let tracker = PriorityFeeTracker::new(10); + + let mut fees = vec![]; + let mut i = 0; + while i <= 100 { + fees.push(i as f64); + i += 1; + } + let account_1 = Pubkey::new_unique(); + let account_2 = Pubkey::new_unique(); + let account_3 = Pubkey::new_unique(); + let accounts = vec![account_1, account_2, account_3]; + + // Simulate adding the fixed fees as both account-specific and transaction fees + for fee in fees.clone() { + tracker.push_priority_fee_for_txn(1, accounts.clone(), fee as u64, false); + } + + // Now test the fee estimates for a known priority level, let's say medium (50th percentile) + let acct = vec![]; + let estimates = calculation_details_1(&acct, false, false, &None, &tracker); + assert_eq!(estimates.len(), 2); + + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + assert_eq!(estimates.get("Global").unwrap().estimates.min, 0.0); + assert_eq!(estimates.get("Global").unwrap().estimates.low, 25.0); + assert_eq!(estimates.get("Global").unwrap().estimates.medium, 50.0); + assert_eq!(estimates.get("Global").unwrap().estimates.high, 75.0); + assert_eq!(estimates.get("Global").unwrap().estimates.very_high, 96.0); + assert_eq!(estimates.get("Global").unwrap().estimates.unsafe_max, 100.0); + assert_eq!(estimates.get("Global").unwrap().count, 101); + assert_eq!(estimates.get("Global").unwrap().mean, 50.0); + assert_eq!(estimates.get("Global").unwrap().stdev, 29.0); + assert!(estimates.get("Global").unwrap().skew.is_nan()); + + assert_eq!(estimates.get("All Accounts").unwrap().estimates.min, 0.0); + assert_eq!(estimates.get("All Accounts").unwrap().estimates.low, 0.0); + assert_eq!(estimates.get("All Accounts").unwrap().estimates.medium, 0.0); + assert_eq!(estimates.get("All Accounts").unwrap().estimates.high, 0.0); + assert_eq!(estimates.get("All Accounts").unwrap().estimates.very_high, 0.0); + assert_eq!(estimates.get("All Accounts").unwrap().estimates.unsafe_max, 0.0); + assert_eq!(estimates.get("All Accounts").unwrap().count, 0); + assert!(estimates.get("All Accounts").unwrap().mean.is_nan()); + assert!(estimates.get("All Accounts").unwrap().stdev.is_nan()); + assert!(estimates.get("All Accounts").unwrap().skew.is_nan()); + + // Now test the fee estimates for a known priority level, let's say medium (50th percentile) + let estimates = calculation_details_1(&acct, false, true, &None, &tracker); + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + assert_eq!(estimates.get("Global").unwrap().estimates.min, 0.0); + assert_eq!(estimates.get("Global").unwrap().estimates.low, 25.0); + assert_eq!(estimates.get("Global").unwrap().estimates.medium, 50.0); + assert_eq!(estimates.get("Global").unwrap().estimates.high, 75.0); + assert_eq!(estimates.get("Global").unwrap().estimates.very_high, 96.0); + assert_eq!(estimates.get("Global").unwrap().estimates.unsafe_max, 100.0); + assert_eq!(estimates.get("Global").unwrap().count, 101); + assert_eq!(estimates.get("Global").unwrap().mean, 50.0); + assert_eq!(estimates.get("Global").unwrap().stdev, 29.0); + assert!(estimates.get("Global").unwrap().skew.is_nan()); + + assert_eq!(estimates.get("All Accounts").unwrap().estimates.min, 0.0); + assert_eq!(estimates.get("All Accounts").unwrap().estimates.low, 0.0); + assert_eq!(estimates.get("All Accounts").unwrap().estimates.medium, 0.0); + assert_eq!(estimates.get("All Accounts").unwrap().estimates.high, 0.0); + assert_eq!(estimates.get("All Accounts").unwrap().estimates.very_high, 0.0); + assert_eq!(estimates.get("All Accounts").unwrap().estimates.unsafe_max, 0.0); + assert_eq!(estimates.get("All Accounts").unwrap().count, 0); + assert!(estimates.get("All Accounts").unwrap().mean.is_nan()); + assert!(estimates.get("All Accounts").unwrap().stdev.is_nan()); + assert!(estimates.get("All Accounts").unwrap().skew.is_nan()); + + // Now test the fee estimates for a known priority level, let's say medium (50th percentile) + let estimates = calculation_details_1(&acct, false, true, &Some(150), &tracker); + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + assert_eq!(estimates.get("Global").unwrap().estimates.min, 0.0); + assert_eq!(estimates.get("Global").unwrap().estimates.low, 25.0); + assert_eq!(estimates.get("Global").unwrap().estimates.medium, 50.0); + assert_eq!(estimates.get("Global").unwrap().estimates.high, 75.0); + assert_eq!(estimates.get("Global").unwrap().estimates.very_high, 96.0); + assert_eq!(estimates.get("Global").unwrap().estimates.unsafe_max, 100.0); + assert_eq!(estimates.get("Global").unwrap().count, 101); + assert_eq!(estimates.get("Global").unwrap().mean, 50.0); + assert_eq!(estimates.get("Global").unwrap().stdev, 29.0); + assert!(estimates.get("Global").unwrap().skew.is_nan()); + + assert_eq!(estimates.get("All Accounts").unwrap().estimates.min, 0.0); + assert_eq!(estimates.get("All Accounts").unwrap().estimates.low, 0.0); + assert_eq!(estimates.get("All Accounts").unwrap().estimates.medium, 0.0); + assert_eq!(estimates.get("All Accounts").unwrap().estimates.high, 0.0); + assert_eq!(estimates.get("All Accounts").unwrap().estimates.very_high, 0.0); + assert_eq!(estimates.get("All Accounts").unwrap().estimates.unsafe_max, 0.0); + assert_eq!(estimates.get("All Accounts").unwrap().count, 0); + assert!(estimates.get("All Accounts").unwrap().mean.is_nan()); + assert!(estimates.get("All Accounts").unwrap().stdev.is_nan()); + assert!(estimates.get("All Accounts").unwrap().skew.is_nan()); + + + // Now test the fee estimates for a known priority level, let's say medium (50th percentile) + let estimates = calculation_details_1(&acct, true, true, &Some(150), &tracker); + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + assert_eq!(estimates.get("Global").unwrap().estimates.min, 0.0); + assert_eq!(estimates.get("Global").unwrap().estimates.low, 25.0); + assert_eq!(estimates.get("Global").unwrap().estimates.medium, 50.0); + assert_eq!(estimates.get("Global").unwrap().estimates.high, 75.0); + assert_eq!(estimates.get("Global").unwrap().estimates.very_high, 96.0); + assert_eq!(estimates.get("Global").unwrap().estimates.unsafe_max, 100.0); + assert_eq!(estimates.get("Global").unwrap().count, 101); + assert_eq!(estimates.get("Global").unwrap().mean, 50.0); + assert_eq!(estimates.get("Global").unwrap().stdev, 29.0); + assert!(estimates.get("Global").unwrap().skew.is_nan()); + + assert_eq!(estimates.get("All Accounts").unwrap().estimates.min, 0.0); + assert_eq!(estimates.get("All Accounts").unwrap().estimates.low, 0.0); + assert_eq!(estimates.get("All Accounts").unwrap().estimates.medium, 0.0); + assert_eq!(estimates.get("All Accounts").unwrap().estimates.high, 0.0); + assert_eq!(estimates.get("All Accounts").unwrap().estimates.very_high, 0.0); + assert_eq!(estimates.get("All Accounts").unwrap().estimates.unsafe_max, 0.0); + assert_eq!(estimates.get("All Accounts").unwrap().count, 0); + assert!(estimates.get("All Accounts").unwrap().mean.is_nan()); + assert!(estimates.get("All Accounts").unwrap().stdev.is_nan()); + assert!(estimates.get("All Accounts").unwrap().skew.is_nan()); + } + + #[tokio::test] + async fn test_specific_fee_estimates_detail_v2() { init_metrics(); let tracker = PriorityFeeTracker::new(10); @@ -595,7 +788,89 @@ mod tests { tracker.push_priority_fee_for_txn(1, accounts.clone(), fee as u64, false); } + let acct = vec![]; // Now test the fee estimates for a known priority level, let's say medium (50th percentile) + let estimates = calculation_details_2(&acct, false, false, &None, &tracker); + assert_eq!(estimates.len(), 1); + + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + assert_eq!(estimates.get("Global").unwrap().estimates.min, 0.0); + assert_eq!(estimates.get("Global").unwrap().estimates.low, 25.0); + assert_eq!(estimates.get("Global").unwrap().estimates.medium, 50.0); + assert_eq!(estimates.get("Global").unwrap().estimates.high, 75.0); + assert_eq!(estimates.get("Global").unwrap().estimates.very_high, 96.0); + assert_eq!(estimates.get("Global").unwrap().estimates.unsafe_max, 100.0); + assert_eq!(estimates.get("Global").unwrap().count, 101); + assert_eq!(estimates.get("Global").unwrap().mean, 50.0); + assert_eq!(estimates.get("Global").unwrap().stdev, 29.0); + assert!(estimates.get("Global").unwrap().skew.is_nan()); + + + // Now test the fee estimates for a known priority level, let's say medium (50th percentile) + let estimates = calculation_details_2(&acct, false, false, &Some(150), &tracker); + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + assert_eq!(estimates.get("Global").unwrap().estimates.min, 0.0); + assert_eq!(estimates.get("Global").unwrap().estimates.low, 25.0); + assert_eq!(estimates.get("Global").unwrap().estimates.medium, 50.0); + assert_eq!(estimates.get("Global").unwrap().estimates.high, 75.0); + assert_eq!(estimates.get("Global").unwrap().estimates.very_high, 96.0); + assert_eq!(estimates.get("Global").unwrap().estimates.unsafe_max, 100.0); + assert_eq!(estimates.get("Global").unwrap().count, 101); + assert_eq!(estimates.get("Global").unwrap().mean, 50.0); + assert_eq!(estimates.get("Global").unwrap().stdev, 29.0); + assert!(estimates.get("Global").unwrap().skew.is_nan()); + + // Now test the fee estimates for a known priority level, let's say medium (50th percentile) + let estimates = calculation_details_2(&acct, false, true, &None, &tracker); + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + assert_eq!(estimates.get("Global").unwrap().estimates.min, 0.0); + assert_eq!(estimates.get("Global").unwrap().estimates.low, 25.0); + assert_eq!(estimates.get("Global").unwrap().estimates.medium, 50.0); + assert_eq!(estimates.get("Global").unwrap().estimates.high, 75.0); + assert_eq!(estimates.get("Global").unwrap().estimates.very_high, 96.0); + assert_eq!(estimates.get("Global").unwrap().estimates.unsafe_max, 100.0); + assert_eq!(estimates.get("Global").unwrap().count, 101); + assert_eq!(estimates.get("Global").unwrap().mean, 50.0); + assert_eq!(estimates.get("Global").unwrap().stdev, 29.0); + assert!(estimates.get("Global").unwrap().skew.is_nan()); + + // Now test the fee estimates for a known priority level, let's say medium (50th percentile) + let estimates = calculation_details_2(&acct, false, true, &Some(150), &tracker); + // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value + assert_eq!(estimates.get("Global").unwrap().estimates.min, 0.0); + assert_eq!(estimates.get("Global").unwrap().estimates.low, 25.0); + assert_eq!(estimates.get("Global").unwrap().estimates.medium, 50.0); + assert_eq!(estimates.get("Global").unwrap().estimates.high, 75.0); + assert_eq!(estimates.get("Global").unwrap().estimates.very_high, 96.0); + assert_eq!(estimates.get("Global").unwrap().estimates.unsafe_max, 100.0); + assert_eq!(estimates.get("Global").unwrap().count, 101); + assert_eq!(estimates.get("Global").unwrap().mean, 50.0); + assert_eq!(estimates.get("Global").unwrap().stdev, 29.0); + assert!(estimates.get("Global").unwrap().skew.is_nan()); + // NOTE: calculation 2 + } + + #[tokio::test] + async fn test_specific_fee_estimates() { + init_metrics(); + let tracker = PriorityFeeTracker::new(10); + + let mut fees = vec![]; + let mut i = 0; + while i <= 100 { + fees.push(i as f64); + i += 1; + } + let account_1 = Pubkey::new_unique(); + let account_2 = Pubkey::new_unique(); + let account_3 = Pubkey::new_unique(); + let accounts = vec![account_1, account_2, account_3]; + + // Simulate adding the fixed fees as both account-specific and transaction fees + for fee in fees.clone() { + tracker.push_priority_fee_for_txn(1, accounts.clone(), fee as u64, false); + } + let estimates = calculation1( &vec![accounts.get(0).unwrap().clone()], false, @@ -604,18 +879,12 @@ mod tests { &tracker, ); // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value - let expected_min_fee = 0.0; - let expected_low_fee = 25.0; - let expected_medium_fee = 50.0; - let expected_high_fee = 75.0; - let expected_very_high_fee = 96.0; - let expected_max_fee = 100.0; - assert_eq!(estimates.min, expected_min_fee); - assert_eq!(estimates.low, expected_low_fee); - assert_eq!(estimates.medium, expected_medium_fee); - assert_eq!(estimates.high, expected_high_fee); - assert_eq!(estimates.very_high, expected_very_high_fee); - assert_eq!(estimates.unsafe_max, expected_max_fee); + assert_eq!(estimates.min, 0.0); + assert_eq!(estimates.low, 25.0); + assert_eq!(estimates.medium, 50.0); + assert_eq!(estimates.high, 75.0); + assert_eq!(estimates.very_high, 96.0); + assert_eq!(estimates.unsafe_max, 100.0); // Now test the fee estimates for a known priority level, let's say medium (50th percentile) let estimates = calculation1( @@ -626,18 +895,12 @@ mod tests { &tracker, ); // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value - let expected_min_fee = 0.0; - let expected_low_fee = 25.0; - let expected_medium_fee = 50.0; - let expected_high_fee = 75.0; - let expected_very_high_fee = 96.0; - let expected_max_fee = 100.0; - assert_eq!(estimates.min, expected_min_fee); - assert_eq!(estimates.low, expected_low_fee); - assert_eq!(estimates.medium, expected_medium_fee); - assert_eq!(estimates.high, expected_high_fee); - assert_eq!(estimates.very_high, expected_very_high_fee); - assert_eq!(estimates.unsafe_max, expected_max_fee); + assert_eq!(estimates.min, 0.0); + assert_eq!(estimates.low, 25.0); + assert_eq!(estimates.medium, 50.0); + assert_eq!(estimates.high, 75.0); + assert_eq!(estimates.very_high, 96.0); + assert_eq!(estimates.unsafe_max, 100.0); // Now test the fee estimates for a known priority level, let's say medium (50th percentile) let estimates = calculation1( @@ -648,18 +911,12 @@ mod tests { &tracker, ); // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value - let expected_min_fee = 0.0; - let expected_low_fee = 25.0; - let expected_medium_fee = 50.0; - let expected_high_fee = 75.0; - let expected_very_high_fee = 96.0; - let expected_max_fee = 100.0; - assert_eq!(estimates.min, expected_min_fee); - assert_eq!(estimates.low, expected_low_fee); - assert_eq!(estimates.medium, expected_medium_fee); - assert_eq!(estimates.high, expected_high_fee); - assert_eq!(estimates.very_high, expected_very_high_fee); - assert_eq!(estimates.unsafe_max, expected_max_fee); + assert_eq!(estimates.min, 0.0); + assert_eq!(estimates.low, 25.0); + assert_eq!(estimates.medium, 50.0); + assert_eq!(estimates.high, 75.0); + assert_eq!(estimates.very_high, 96.0); + assert_eq!(estimates.unsafe_max, 100.0); // Now test the fee estimates for a known priority level, let's say medium (50th percentile) let estimates = calculation1( @@ -670,18 +927,12 @@ mod tests { &tracker, ); // Since the fixed fees are evenly distributed, the 50th percentile should be the middle value - let expected_min_fee = 0.0; - let expected_low_fee = 25.0; - let expected_medium_fee = 50.0; - let expected_high_fee = 75.0; - let expected_very_high_fee = 96.0; - let expected_max_fee = 100.0; - assert_eq!(estimates.min, expected_min_fee); - assert_eq!(estimates.low, expected_low_fee); - assert_eq!(estimates.medium, expected_medium_fee); - assert_eq!(estimates.high, expected_high_fee); - assert_eq!(estimates.very_high, expected_very_high_fee); - assert_eq!(estimates.unsafe_max, expected_max_fee); + assert_eq!(estimates.min, 0.0); + assert_eq!(estimates.low, 25.0); + assert_eq!(estimates.medium, 50.0); + assert_eq!(estimates.high, 75.0); + assert_eq!(estimates.very_high, 96.0); + assert_eq!(estimates.unsafe_max, 100.0); } #[tokio::test] diff --git a/src/priority_fee_calculation.rs b/src/priority_fee_calculation.rs index a8c2242..7af4ea3 100644 --- a/src/priority_fee_calculation.rs +++ b/src/priority_fee_calculation.rs @@ -1,18 +1,11 @@ -use crate::model::PriorityFeesBySlot; -use crate::priority_fee_calculation::Calculations::Calculation1; +use crate::model::{DataType, PriorityFeesBySlot}; use cadence_macros::{statsd_count, statsd_gauge}; use solana_sdk::pubkey::Pubkey; use statrs::statistics::Data; use std::collections::HashMap; use std::time::Instant; -use Calculations::Calculation2; +use Calculations::{Calculation1, Calculation2}; -#[derive(Debug, Clone, PartialOrd, PartialEq, Eq, Ord, Hash)] -pub enum DataType<'a> { - Global, - AllAccounts, - Account(&'a Pubkey), -} /// /// The result with type of /// diff --git a/src/rpc_server.rs b/src/rpc_server.rs index ea5d371..bc06369 100644 --- a/src/rpc_server.rs +++ b/src/rpc_server.rs @@ -7,17 +7,19 @@ use std::{ }; use crate::errors::invalid_request; -use crate::model::{MicroLamportPriorityFeeEstimates, PriorityLevel}; +use crate::model::{ + MicroLamportPriorityFeeDetails, MicroLamportPriorityFeeEstimates, PriorityLevel, +}; use crate::priority_fee::{construct_writable_accounts, PriorityFeeTracker}; use crate::priority_fee_calculation::Calculations; use crate::solana::solana_rpc::decode_and_deserialize; use cadence_macros::{statsd_count, statsd_time}; +use jsonrpsee::types::error::{INTERNAL_ERROR_CODE, INTERNAL_ERROR_MSG}; use jsonrpsee::{ core::{async_trait, RpcResult}, proc_macros::rpc, types::ErrorObjectOwned, }; -use jsonrpsee::types::error::{INTERNAL_ERROR_CODE, INTERNAL_ERROR_MSG}; use serde::{Deserialize, Serialize}; use solana_account_decoder::parse_address_lookup_table::{ parse_address_lookup_table, LookupTableAccountType, @@ -130,6 +132,15 @@ pub struct GetPriorityFeeEstimateResponse { pub priority_fee_levels: Option, } +#[derive(Serialize, Clone, Debug, Default)] +#[serde(rename_all(serialize = "camelCase", deserialize = "camelCase"))] +pub struct GetPriorityFeeEstimateDetailsResponse { + pub priority_fee_estimate_details: Vec<(String, MicroLamportPriorityFeeDetails)>, + #[serde(skip_serializing_if = "Option::is_none")] + pub priority_fee_estimate: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub priority_fee_levels: Option, +} impl Into for GetPriorityFeeEstimateRequest { fn into(self) -> GetPriorityFeeEstimateRequestLight { let transaction = self.transaction; @@ -165,6 +176,15 @@ pub trait AtlasPriorityFeeEstimatorRpc { self.get_priority_fee_estimate_v1(get_priority_fee_estimate_request) } + // TODO: DKH - delete after all the users were notified about moving to strict parsing + #[method(name = "getPriorityFeeEstimateDetails")] + fn get_priority_fee_estimate_details_light( + &self, + get_priority_fee_estimate_request: GetPriorityFeeEstimateRequestLight, + ) -> RpcResult { + self.get_priority_fee_estimate_details_v1(get_priority_fee_estimate_request) + } + // TODO: DKH - rename annotation method name to "getPriorityFeeEstimateStrict" to "getPriorityFeeEstimate" #[method(name = "getPriorityFeeEstimateStrict")] fn get_priority_fee_estimate( @@ -194,6 +214,18 @@ pub trait AtlasPriorityFeeEstimatorRpc { &self, get_priority_fee_estimate_request: GetPriorityFeeEstimateRequest, ) -> RpcResult; + + #[method(name = "getPriorityFeeEstimateDetailsV1")] + fn get_priority_fee_estimate_details_v1( + &self, + get_priority_fee_estimate_request: GetPriorityFeeEstimateRequestLight, + ) -> RpcResult; + + #[method(name = "getPriorityFeeEstimateDetailsV2")] + fn get_priority_fee_estimate_details_v2( + &self, + get_priority_fee_estimate_request: GetPriorityFeeEstimateRequest, + ) -> RpcResult; } fn validate_get_priority_fee_estimate_request( @@ -351,7 +383,7 @@ fn get_accounts( get_from_account_keys(&transaction), get_from_address_lookup_tables(rpc_client, &transaction), ] - .concat(); + .concat(); return Ok(account_keys); } } @@ -380,6 +412,23 @@ impl AtlasPriorityFeeEstimatorRpcServer for AtlasPriorityFeeEstimator { ) -> RpcResult { self.execute_priority_fee_estimate_coordinator(get_priority_fee_estimate_request, false) } + + fn get_priority_fee_estimate_details_v1( + &self, + get_priority_fee_estimate_request: GetPriorityFeeEstimateRequestLight, + ) -> RpcResult { + self.execute_priority_fee_details_coordinator( + get_priority_fee_estimate_request.into(), + true, + ) + } + + fn get_priority_fee_estimate_details_v2( + &self, + get_priority_fee_estimate_request: GetPriorityFeeEstimateRequest, + ) -> RpcResult { + self.execute_priority_fee_details_coordinator(get_priority_fee_estimate_request, false) + } } impl AtlasPriorityFeeEstimator { @@ -424,16 +473,31 @@ impl AtlasPriorityFeeEstimator { let include_vote = should_include_vote(&options); let include_empty_slots = should_include_empty_slots(&options); let calc = if is_v1 { - Calculations::new_calculation1(&accounts, include_vote, include_empty_slots, &lookback_slots) + Calculations::new_calculation1( + &accounts, + include_vote, + include_empty_slots, + &lookback_slots, + ) } else { - Calculations::new_calculation2(&accounts, include_vote, include_empty_slots, &lookback_slots) + Calculations::new_calculation2( + &accounts, + include_vote, + include_empty_slots, + &lookback_slots, + ) + }; + let priority_fee_levels = match self.priority_fee_tracker.calculate_priority_fee(&calc) { + Ok(level) => level, + Err(e) => { + warn!("failed to calculate priority_fee_levels: {:#?}", e); + return Err(ErrorObjectOwned::owned( + INTERNAL_ERROR_CODE, + INTERNAL_ERROR_MSG, + None::, + )); + } }; - let priority_fee_levels = self.priority_fee_tracker.calculate_priority_fee_details(&calc); - if let Err(e) = priority_fee_levels { - warn!("failed to calculate priority_fee_levels: {:#?}", e); - return Err(ErrorObjectOwned::owned(INTERNAL_ERROR_CODE, INTERNAL_ERROR_MSG, None::)); - } - let priority_fee_levels = priority_fee_levels.unwrap(); if let Some(options) = options.clone() { if options.include_all_priority_fee_levels == Some(true) { return Ok(GetPriorityFeeEstimateResponse { @@ -470,6 +534,107 @@ impl AtlasPriorityFeeEstimator { priority_fee_levels: None, }) } + + fn execute_priority_fee_details_coordinator( + &self, + get_priority_fee_estimate_request: GetPriorityFeeEstimateRequest, + is_v1: bool, + ) -> RpcResult { + let options = get_priority_fee_estimate_request.options.clone(); + let reason = validate_get_priority_fee_estimate_request(&get_priority_fee_estimate_request); + if let Some(reason) = reason { + return Err(reason); + } + let accounts = get_accounts(&self.rpc_client, get_priority_fee_estimate_request); + if let Err(e) = accounts { + return Err(e); + } + let accounts: Vec = accounts + .unwrap() + .iter() + .filter_map(|a| Pubkey::from_str(a).ok()) + .collect(); + let lookback_slots = options.clone().map(|o| o.lookback_slots).flatten(); + if let Some(lookback_slots) = &lookback_slots { + if *lookback_slots < 1 || *lookback_slots as usize > self.max_lookback_slots { + return Err(invalid_request("lookback_slots must be between 1 and 150")); + } + } + let include_vote = should_include_vote(&options); + let include_empty_slots = should_include_empty_slots(&options); + let calc = if is_v1 { + Calculations::new_calculation1( + &accounts, + include_vote, + include_empty_slots, + &lookback_slots, + ) + } else { + Calculations::new_calculation2( + &accounts, + include_vote, + include_empty_slots, + &lookback_slots, + ) + }; + let (total_priority_fee_levels, priority_fee_levels) = match self + .priority_fee_tracker + .calculate_priority_fee_details(&calc) + { + Ok(level) => level, + Err(e) => { + warn!("failed to calculate priority_fee_levels details: {:#?}", e); + return Err(ErrorObjectOwned::owned( + INTERNAL_ERROR_CODE, + INTERNAL_ERROR_MSG, + None::, + )); + } + }; + + let mut priority_fees: Vec<(String, MicroLamportPriorityFeeDetails)> = + priority_fee_levels.into_iter().collect(); + priority_fees.sort_by(|a, b| b.0.cmp(&a.0)); + + if let Some(options) = options.clone() { + if options.include_all_priority_fee_levels == Some(true) { + return Ok(GetPriorityFeeEstimateDetailsResponse { + priority_fee_estimate_details: priority_fees, + priority_fee_estimate: None, + priority_fee_levels: Some(total_priority_fee_levels), + }); + } + if let Some(priority_level) = options.priority_level { + let priority_fee = match priority_level { + PriorityLevel::Min => total_priority_fee_levels.min, + PriorityLevel::Low => total_priority_fee_levels.low, + PriorityLevel::Medium => total_priority_fee_levels.medium, + PriorityLevel::High => total_priority_fee_levels.high, + PriorityLevel::VeryHigh => total_priority_fee_levels.very_high, + PriorityLevel::UnsafeMax => total_priority_fee_levels.unsafe_max, + PriorityLevel::Default => total_priority_fee_levels.medium, + }; + return Ok(GetPriorityFeeEstimateDetailsResponse { + priority_fee_estimate_details: priority_fees, + priority_fee_estimate: Some(priority_fee), + priority_fee_levels: None, + }); + } + } + let recommended = options.map_or(false, |o: GetPriorityFeeEstimateOptions| { + o.recommended.unwrap_or(false) + }); + let priority_fee = if recommended { + get_recommended_fee(total_priority_fee_levels) + } else { + total_priority_fee_levels.medium + }; + Ok(GetPriorityFeeEstimateDetailsResponse { + priority_fee_estimate_details: priority_fees, + priority_fee_estimate: Some(priority_fee), + priority_fee_levels: None, + }) + } } // default to true for backwards compatibility. Recommended fee does not include vote txns From 3130956fae01fcfc66dbebfe42f1a7ad54e3f055 Mon Sep 17 00:00:00 2001 From: Dmitriy Khomitskiy Date: Mon, 18 Nov 2024 11:49:45 -0600 Subject: [PATCH 4/5] RPC-468 updating readme --- README.md | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/README.md b/README.md index 1300638..591e662 100644 --- a/README.md +++ b/README.md @@ -116,3 +116,73 @@ cargo run "id": "1" } ``` + +**Request the recommended priority fee details** +The purpose of this API is to understand what data is taken into consideration given the query. +The response shows the statistical distribution of data per account as well as how much data is available in each account. +The request is identical to the one used in getPriorityFeeEstimate request. This is to ensure that same request could be +reused during analysis and during dev / operational stages + +```json +{ + "id": "version1", + "jsonrpc": "2.0", + "method": "getPriorityFeeEstimateDetails", + "params": [ + { + "accountKeys": [ +"JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4" + ], + "options": {"recommended": true} + } + ] +} +``` + +**Response** + +```json +{ + "jsonrpc": "2.0", + "result": { + "priorityFeeEstimateDetails": [ + [ + "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4", + { + "estimates": { + "min": 0.0, + "low": 0.0, + "medium": 0.0, + "high": 0.0, + "veryHigh": 0.0, + "unsafeMax": 25113388.0 + }, + "mean": 717525.0, // mean fee payed for each transaction for account evaluated over last 150 (or less if requested less) slots + "stdev": 4244937.0, // standard deviation of fee payed for each transaction for account evaluated over last 150 (or less if requested less) slots , + "skew": null, // skew of fee payed for each transaction for account evaluated over last 150 (or less if requested less) slots. Null if data is randomly distributed and cannot calculate + "count": 35 // Number of transactions for account that were evaluated over last 150 (or less if requested less) slots + } + ], + [ + "Global", + { + "estimates": { + "min": 0.0, + "low": 0.0, + "medium": 20003.0, + "high": 2276532.0, + "veryHigh": 32352142.0, + "unsafeMax": 2000000000.0 + }, + "mean": 8118956.0, // mean fee payed for each transaction for all accounts evaluated over last 150 (or less if requested less) slots + "stdev": 53346050.0, // standard deviation of fee payed for each transaction for all account evaluated over last 150 (or less if requested less) slots , + "skew": null, // skew of fee payed for each transaction for all accounts evaluated over last 150 (or less if requested less) slots. Null if data is randomly distributed and cannot calculate + "count": 14877 // Number of transactions for all accounts that were evaluated over last 150 (or less if requested less) slots + } + ] + ], + "priorityFeeEstimate": 20003.0 + }, + "id": "version1" +} +``` \ No newline at end of file From e1d346e053da3da0dc2ad1fea0fed7fa809e6b01 Mon Sep 17 00:00:00 2001 From: Dmitriy Khomitskiy Date: Mon, 18 Nov 2024 12:05:59 -0600 Subject: [PATCH 5/5] RPC-468 fixing issue with tests --- src/priority_fee.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/priority_fee.rs b/src/priority_fee.rs index 70ba734..d8e1097 100644 --- a/src/priority_fee.rs +++ b/src/priority_fee.rs @@ -428,6 +428,7 @@ mod tests { tracker .calculate_priority_fee_details(&calc) .expect(format!("estimates for calc1 to be valid with {:?}", calc).as_str()) + .1 } fn calculation2( @@ -464,6 +465,7 @@ mod tests { tracker .calculate_priority_fee_details(&calc) .expect(format!("estimates for calc2 to be valid with {:?}", calc).as_str()) + .1 } #[tokio::test]