Skip to content

Commit d3c3966

Browse files
apollo_consensus_orchestrator: add SNIP-35 module
1 parent 9b92f95 commit d3c3966

2 files changed

Lines changed: 77 additions & 0 deletions

File tree

crates/apollo_consensus_orchestrator/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ pub mod cende;
1818
/// Fee market logic.
1919
pub mod fee_market;
2020

21+
/// SNIP-35 dynamic L2 gas pricing (consensus-level fee mechanism).
22+
pub mod snip35;
23+
2124
#[allow(missing_docs)]
2225
pub mod metrics;
2326

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
//! SNIP-35 dynamic L2 gas pricing primitives.
2+
//!
3+
//! This module implements the consensus-level fee mechanism described in SNIP-35:
4+
//! - `compute_fee_actual`: median of recent `fee_proposal` values (sliding window).
5+
//! - `compute_fee_target`: USD-denominated target converted to FRI via STRK/USD oracle.
6+
//! - `compute_fee_proposal`: honest proposer's recommended fee, clamped within a margin of
7+
//! `fee_actual`.
8+
//!
9+
//! See also: `fee_market` for EIP-1559-style base-fee adjustment, which receives
10+
//! `fee_actual` as a floor.
11+
12+
use ethnum::U256;
13+
use starknet_api::block::GasPrice;
14+
15+
/// Scale factor for 18-decimal fixed-point conversion (1 STRK = 10^18 FRI).
16+
const FRI_DECIMALS_SCALE: u128 = 10u128.pow(18);
17+
18+
/// Denominator for parts-per-thousand calculations in SNIP-35 fee_proposal bounds.
19+
pub(crate) const PPT_DENOMINATOR: u128 = 1000;
20+
21+
/// Compute fee_actual from the last `window_size` fee_proposal values (SNIP-35).
22+
/// Returns the average of the two middle values, rounded down.
23+
/// Returns `None` if fewer than `window_size` proposals are available.
24+
pub fn compute_fee_actual(proposals: &[GasPrice], window_size: usize) -> Option<GasPrice> {
25+
if proposals.len() < window_size || window_size < 2 {
26+
return None;
27+
}
28+
let window = &proposals[proposals.len() - window_size..];
29+
let mut sorted: Vec<u128> = window.iter().map(|p| p.0).collect();
30+
sorted.sort();
31+
// Median = average of the two middle values, rounded down.
32+
// Use overflow-safe averaging: a + (b - a) / 2 (safe because sorted, so b >= a).
33+
let mid = window_size / 2;
34+
let median = sorted[mid - 1] + (sorted[mid] - sorted[mid - 1]) / 2;
35+
// Return None if median is zero (e.g., pre-SNIP-35 blocks with fee_proposal=0).
36+
// This triggers the l2_gas_price fallback in both proposer and validator paths.
37+
if median == 0 { None } else { Some(GasPrice(median)) }
38+
}
39+
40+
/// Compute the fee target from STRK/USD price and a USD cost target (SNIP-35).
41+
/// `target_atto_usd_per_l2_gas` is in atto-USD (18-decimal fixed-point).
42+
/// `strk_usd_rate` is the STRK/USD price with 18 decimals (from oracle).
43+
/// Result is in FRI, clamped to `[floor_min_fri, floor_max_fri]`.
44+
pub fn compute_fee_target(
45+
target_atto_usd_per_l2_gas: u128,
46+
strk_usd_rate: u128,
47+
floor_min_fri: u128,
48+
floor_max_fri: u128,
49+
) -> GasPrice {
50+
if strk_usd_rate == 0 {
51+
return GasPrice(floor_max_fri);
52+
}
53+
// floor_fri = target_atto_usd_per_l2_gas * 10^18 / strk_usd_rate
54+
let numerator = U256::from(target_atto_usd_per_l2_gas) * U256::from(FRI_DECIMALS_SCALE);
55+
let floor = numerator / U256::from(strk_usd_rate);
56+
let floor_u128 = u128::try_from(floor).unwrap_or(u128::MAX);
57+
GasPrice(floor_u128.clamp(floor_min_fri, floor_max_fri))
58+
}
59+
60+
/// Compute the fee_proposal an honest proposer should publish (SNIP-35).
61+
/// - If oracle failed (`fee_target` is `None`): freeze at `fee_actual`.
62+
/// - Otherwise: clamp `fee_target` to within +/-`margin_ppt` parts per thousand of `fee_actual`.
63+
pub fn compute_fee_proposal(
64+
fee_target: Option<GasPrice>,
65+
fee_actual: GasPrice,
66+
margin_ppt: u128,
67+
) -> GasPrice {
68+
let Some(fee_target) = fee_target else {
69+
return fee_actual;
70+
};
71+
let upper = fee_actual.0.saturating_mul(PPT_DENOMINATOR + margin_ppt) / PPT_DENOMINATOR;
72+
let lower = fee_actual.0.saturating_mul(PPT_DENOMINATOR) / (PPT_DENOMINATOR + margin_ppt);
73+
GasPrice(fee_target.0.clamp(lower, upper))
74+
}

0 commit comments

Comments
 (0)