-
Notifications
You must be signed in to change notification settings - Fork 23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(bolt-sidecar): implement pricing models for preconfs #593
Changes from 4 commits
706b619
ca5d305
64b6175
22a34ab
7b70a02
62c43ae
d693fff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
/// Handles pricing calculations for preconfirmations | ||
#[derive(Debug)] | ||
pub struct PreconfPricing { | ||
block_gas_limit: u64, | ||
base_multiplier: f64, | ||
gas_scalar: f64, | ||
} | ||
|
||
/// Errors that can occur during pricing calculations | ||
#[derive(Debug, thiserror::Error)] | ||
pub enum PricingError { | ||
/// Preconfirmed gas exceeds the block limit | ||
#[error("Preconfirmed gas {0} exceeds block limit {1}")] | ||
ExceedsBlockLimit(u64, u64), | ||
/// Insufficient remaining gas for the incoming transaction | ||
#[error("Insufficient remaining gas: requested {requested}, available {available}")] | ||
/// Insufficient remaining gas for the incoming transaction | ||
InsufficientGas { | ||
/// Gas requested by the incoming transaction | ||
requested: u64, | ||
/// Gas available in the block | ||
available: u64, | ||
}, | ||
/// Incoming gas is zero | ||
#[error("Invalid gas limit: Incoming gas ({incoming_gas}) is zero")] | ||
InvalidGasLimit { | ||
/// Gas required by the incoming transaction | ||
incoming_gas: u64, | ||
}, | ||
} | ||
|
||
impl Default for PreconfPricing { | ||
fn default() -> Self { | ||
Self::new() | ||
} | ||
} | ||
|
||
impl PreconfPricing { | ||
/// Initializes a new PreconfPricing with default parameters. | ||
pub fn new() -> Self { | ||
Self { block_gas_limit: 30_000_000, base_multiplier: 0.019, gas_scalar: 1.02e-6 } | ||
} | ||
|
||
/// Calculate the minimum priority fee for a preconfirmation based on | ||
/// https://research.lido.fi/t/a-pricing-model-for-inclusion-preconfirmations/9136 | ||
/// | ||
/// # Arguments | ||
/// * `incoming_gas` - Gas required by the incoming transaction | ||
/// * `preconfirmed_gas` - Total gas already preconfirmed | ||
/// | ||
/// # Returns | ||
/// * `Ok(f64)` - The minimum priority fee in Wei | ||
/// * `Err(PricingError)` - If the calculation cannot be performed | ||
pub fn calculate_min_priority_fee( | ||
&self, | ||
incoming_gas: u64, | ||
preconfirmed_gas: u64, | ||
) -> Result<u64, PricingError> { | ||
// Check if preconfirmed gas exceeds block limit | ||
if preconfirmed_gas >= self.block_gas_limit { | ||
return Err(PricingError::ExceedsBlockLimit(preconfirmed_gas, self.block_gas_limit)); | ||
} | ||
|
||
// Validate incoming gas | ||
if incoming_gas == 0 { | ||
return Err(PricingError::InvalidGasLimit { incoming_gas }); | ||
} | ||
|
||
// Check if there is enough gas remaining in the block | ||
let remaining_gas = self.block_gas_limit - preconfirmed_gas; | ||
if incoming_gas > remaining_gas { | ||
return Err(PricingError::InsufficientGas { | ||
requested: incoming_gas, | ||
available: remaining_gas, | ||
}); | ||
} | ||
estensen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// T(IG,UG) = 0.019 * ln(1.02⋅10^-6(30M-UG)+1 / 1.02⋅10^-6(30M-UG-IG)+1) / IG | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reminder that these calculations can be performed directly using wei as unit of measure and not ether by multiplying the |
||
// where | ||
// IG = Gas used by the incoming transaction | ||
// UG = Gas already preconfirmed | ||
// T = Inclusion tip per gas | ||
// 30M = Current gas limit (36M soon?) | ||
let after_gas = remaining_gas - incoming_gas; | ||
|
||
// Calculate numerator and denominator for the logarithm | ||
let numerator = self.gas_scalar * (remaining_gas as f64) + 1.0; | ||
let denominator = self.gas_scalar * (after_gas as f64) + 1.0; | ||
|
||
// Calculate gas used | ||
let expected_val = self.base_multiplier * (numerator / denominator).ln(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider using |
||
|
||
// Calculate the inclusion tip | ||
let inclusion_tip_ether = expected_val / (incoming_gas as f64); | ||
let inclusion_tip_wei = (inclusion_tip_ether * 1e18) as u64; | ||
|
||
Ok(inclusion_tip_wei) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
||
#[test] | ||
fn test_min_priority_fee_zero_preconfirmed() { | ||
let pricing = PreconfPricing::new(); | ||
|
||
// Test minimum fee (21k gas ETH transfer, 0 preconfirmed) | ||
let incoming_gas = 21_000; | ||
let preconfirmed_gas = 0; | ||
let min_fee_wei = | ||
pricing.calculate_min_priority_fee(incoming_gas, preconfirmed_gas).unwrap(); | ||
|
||
// Expected fee: ~0.61 Gwei | ||
assert!( | ||
(min_fee_wei as f64 - 613_499_092.0).abs() < 1_000.0, | ||
"Expected ~610,000,000 Wei, got {} Wei", | ||
min_fee_wei | ||
); | ||
} | ||
|
||
#[test] | ||
fn test_min_priority_fee_medium_load() { | ||
let pricing = PreconfPricing::new(); | ||
|
||
// Test medium load (21k gas, 15M preconfirmed) | ||
let incoming_gas = 21_000; | ||
let preconfirmed_gas = 15_000_000; | ||
let min_fee_wei = | ||
pricing.calculate_min_priority_fee(incoming_gas, preconfirmed_gas).unwrap(); | ||
|
||
// Expected fee: ~1.17 Gwei | ||
assert!( | ||
(min_fee_wei as f64 - 1_189_738_950.0).abs() < 1_000.0, | ||
"Expected ~1,189,738,950 Wei, got {} Wei", | ||
min_fee_wei | ||
); | ||
} | ||
|
||
#[test] | ||
fn test_min_priority_fee_max_load() { | ||
let pricing = PreconfPricing::new(); | ||
|
||
// Test last preconfirmed transaction (21k gas, almost 30M preconfirmed) | ||
let incoming_gas = 21_000; | ||
let preconfirmed_gas = 30_000_000 - 21_000; | ||
let min_fee_wei = | ||
pricing.calculate_min_priority_fee(incoming_gas, preconfirmed_gas).unwrap(); | ||
|
||
// Expected fee: ~19 Gwei | ||
// This will likely never happen, since you want to reserve some gas | ||
// on top of the block for MEV, but enforcing this is not the responsibility | ||
// of the pricing model. | ||
Comment on lines
+171
to
+173
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd drop these types of comments since this might not be the intention of every proposer. |
||
assert!( | ||
(min_fee_wei as f64 - 19_175_357_339.0).abs() < 1_000.0, | ||
"Expected ~19,175,357,339 Wei, got {} Wei", | ||
min_fee_wei | ||
); | ||
} | ||
Comment on lines
+174
to
+179
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd add a disclaimer that these checks are mainly for tracking down whether some changes in the algo are highly different compared to previous result, but these target values are not to be considered "correct" since they have been produced with this function in the beginning. |
||
|
||
#[test] | ||
fn test_error_exceeds_block_limit() { | ||
let pricing = PreconfPricing::new(); | ||
|
||
let incoming_gas = 21_000; | ||
let preconfirmed_gas = 30_000_001; | ||
|
||
let result = pricing.calculate_min_priority_fee(incoming_gas, preconfirmed_gas); | ||
assert!(matches!(result, Err(PricingError::ExceedsBlockLimit(30_000_001, 30_000_000)))); | ||
} | ||
|
||
#[test] | ||
fn test_error_insufficient_gas() { | ||
let pricing = PreconfPricing::new(); | ||
|
||
let incoming_gas = 15_000_001; | ||
let preconfirmed_gas = 15_000_000; | ||
|
||
let result = pricing.calculate_min_priority_fee(incoming_gas, preconfirmed_gas); | ||
assert!(matches!( | ||
result, | ||
Err(PricingError::InsufficientGas { requested: 15_000_001, available: 15_000_000 }) | ||
)); | ||
} | ||
|
||
#[test] | ||
fn test_error_zero_incoming_gas() { | ||
let pricing = PreconfPricing::new(); | ||
|
||
let incoming_gas = 0; | ||
let preconfirmed_gas = 0; | ||
|
||
let result = pricing.calculate_min_priority_fee(incoming_gas, preconfirmed_gas); | ||
assert!(matches!(result, Err(PricingError::InvalidGasLimit { incoming_gas: 0 }))); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Follow-up to remove hardcoded gas limit as it will likely change to 36M soon