Skip to content

Commit 09938eb

Browse files
apollo_consensus_orchestrator: add SNIP-35 module
1 parent c649130 commit 09938eb

2 files changed

Lines changed: 82 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: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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 median: for even `window_size`, the average of the two middle values rounded
23+
/// down; for odd `window_size`, the single middle value.
24+
/// Returns `None` if fewer than `window_size` proposals are available.
25+
pub fn compute_fee_actual(proposals: &[GasPrice], window_size: usize) -> Option<GasPrice> {
26+
if proposals.len() < window_size || window_size < 2 {
27+
return None;
28+
}
29+
let window = &proposals[proposals.len() - window_size..];
30+
let mut sorted: Vec<u128> = window.iter().map(|p| p.0).collect();
31+
sorted.sort();
32+
let mid = window_size / 2;
33+
let median = if window_size.is_multiple_of(2) {
34+
// Even: average of the two middle values, rounded down.
35+
// Overflow-safe averaging: a + (b - a) / 2 (safe because sorted, so b >= a).
36+
sorted[mid - 1] + (sorted[mid] - sorted[mid - 1]) / 2
37+
} else {
38+
sorted[mid]
39+
};
40+
// Return None if median is zero (e.g., pre-SNIP-35 blocks with fee_proposal=0).
41+
// This triggers the l2_gas_price fallback in both proposer and validator paths.
42+
if median == 0 { None } else { Some(GasPrice(median)) }
43+
}
44+
45+
/// Compute the fee target from STRK/USD price and a USD cost target (SNIP-35).
46+
/// `target_atto_usd_per_l2_gas` is in atto-USD (18-decimal fixed-point).
47+
/// `strk_usd_rate` is the STRK/USD price with 18 decimals (from oracle).
48+
/// Result is in FRI, clamped to `[floor_min_fri, floor_max_fri]`.
49+
pub fn compute_fee_target(
50+
target_atto_usd_per_l2_gas: u128,
51+
strk_usd_rate: u128,
52+
floor_min_fri: u128,
53+
floor_max_fri: u128,
54+
) -> GasPrice {
55+
if strk_usd_rate == 0 {
56+
return GasPrice(floor_max_fri);
57+
}
58+
// floor_fri = target_atto_usd_per_l2_gas * 10^18 / strk_usd_rate
59+
let numerator = U256::from(target_atto_usd_per_l2_gas) * U256::from(FRI_DECIMALS_SCALE);
60+
let floor = numerator / U256::from(strk_usd_rate);
61+
let floor_u128 = u128::try_from(floor).unwrap_or(u128::MAX);
62+
GasPrice(floor_u128.clamp(floor_min_fri, floor_max_fri))
63+
}
64+
65+
/// Compute the fee_proposal an honest proposer should publish (SNIP-35).
66+
/// - If oracle failed (`fee_target` is `None`): freeze at `fee_actual`.
67+
/// - Otherwise: clamp `fee_target` to within +/-`margin_ppt` parts per thousand of `fee_actual`.
68+
pub fn compute_fee_proposal(
69+
fee_target: Option<GasPrice>,
70+
fee_actual: GasPrice,
71+
margin_ppt: u128,
72+
) -> GasPrice {
73+
let Some(fee_target) = fee_target else {
74+
return fee_actual;
75+
};
76+
let upper = fee_actual.0.saturating_mul(PPT_DENOMINATOR + margin_ppt) / PPT_DENOMINATOR;
77+
let lower = fee_actual.0.saturating_mul(PPT_DENOMINATOR) / (PPT_DENOMINATOR + margin_ppt);
78+
GasPrice(fee_target.0.clamp(lower, upper))
79+
}

0 commit comments

Comments
 (0)