Skip to content

Commit 5b171a5

Browse files
sirandreww-starkwareShahakShama
authored andcommitted
apollo_consensus_orchestrator: wire SNIP-35 fee_proposal into build_proposal
1 parent de30c2b commit 5b171a5

6 files changed

Lines changed: 311 additions & 78 deletions

File tree

crates/apollo_consensus_orchestrator/src/build_proposal.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ pub(crate) struct ProposalBuildArguments {
7676
pub override_l2_gas_price_fri: Option<u128>,
7777
pub min_l2_gas_price_per_height: Vec<PricePerHeight>,
7878
pub compare_retrospective_block_hash: bool,
79+
/// SNIP-35: proposer's fee_proposal for this block.
80+
pub fee_proposal: GasPrice,
81+
/// SNIP-35: current fee_actual from the sliding window.
82+
pub fee_actual: Option<GasPrice>,
7983
}
8084

8185
type BuildProposalResult<T> = Result<T, BuildProposalError>;
@@ -176,7 +180,7 @@ async fn initiate_build(args: &mut ProposalBuildArguments) -> BuildProposalResul
176180
starknet_version: starknet_api::block::StarknetVersion::LATEST,
177181
// TODO(Asmaa): Put the real value once we have it.
178182
version_constant_commitment: Default::default(),
179-
fee_proposal_fri: None,
183+
fee_proposal_fri: Some(args.fee_proposal),
180184
};
181185

182186
let retrospective_block_hash = wait_for_retrospective_block_hash(
@@ -319,7 +323,7 @@ async fn get_proposal_content(
319323
info.l2_gas_used,
320324
args.override_l2_gas_price_fri,
321325
&args.min_l2_gas_price_per_height,
322-
None,
326+
args.fee_actual,
323327
);
324328
let fin_payload = ProposalFinPayload {
325329
commitment_parts: CommitmentParts::from(&info),

crates/apollo_consensus_orchestrator/src/sequencer_consensus_context.rs

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ use tokio::task::JoinHandle;
7575
use tokio::time::sleep;
7676
use tokio_util::sync::CancellationToken;
7777
use tokio_util::task::AbortOnDropHandle;
78-
use tracing::{error, error_span, info, instrument, trace, warn, Instrument};
78+
use tracing::{debug, error, error_span, info, instrument, trace, warn, Instrument};
7979

8080
use crate::build_proposal::{build_proposal, BuildProposalError, ProposalBuildArguments};
8181
use crate::cende::{
@@ -94,8 +94,19 @@ use crate::metrics::{
9494
record_validate_proposal_failure,
9595
register_metrics,
9696
CONSENSUS_L2_GAS_PRICE,
97+
SNIP35_FEE_ACTUAL,
98+
SNIP35_FEE_PROPOSAL,
99+
SNIP35_FEE_TARGET,
100+
SNIP35_STRK_USD_RATE,
101+
};
102+
use crate::snip35::{
103+
compute_fee_actual,
104+
compute_fee_proposal,
105+
compute_fee_target,
106+
FEE_PROPOSAL_MARGIN_PPT,
107+
FEE_PROPOSAL_WINDOW_SIZE,
108+
TARGET_ATTO_USD_PER_L2_GAS,
97109
};
98-
use crate::snip35::FEE_PROPOSAL_WINDOW_SIZE;
99110
use crate::utils::{
100111
convert_to_sn_api_block_info,
101112
make_gas_price_params,
@@ -398,6 +409,7 @@ impl SequencerConsensusContext {
398409
sequencer,
399410
timestamp: BlockTimestamp(init.timestamp),
400411
l1_da_mode: init.l1_da_mode,
412+
fee_proposal_fri: init.fee_proposal_fri,
401413
// TODO(guy.f): Figure out where/if to get the values below from and fill them.
402414
..Default::default()
403415
};
@@ -416,16 +428,59 @@ impl SequencerConsensusContext {
416428
/// Returns the next L2 gas price without mutating context. Used when building the fin and when
417429
/// updating at decision time.
418430
fn calculate_next_l2_gas_price(&self, height: BlockNumber, l2_gas_used: GasAmount) -> GasPrice {
431+
let fee_actual = compute_fee_actual(&self.fee_proposals_window, height);
419432
calculate_next_l2_gas_price_for_fin(
420433
self.l2_gas_price,
421434
height,
422435
l2_gas_used,
423436
self.config.dynamic_config.override_l2_gas_price_fri,
424437
&self.config.dynamic_config.min_l2_gas_price_per_height,
425-
None,
438+
fee_actual,
426439
)
427440
}
428441

442+
/// SNIP-35 fee_proposal: clamp the oracle's `fee_target` to a margin around `fee_actual`.
443+
/// When `fee_actual` is `None` (window incomplete), freeze at `l2_gas_price`; the validator
444+
/// derives the same fallback so both sides agree.
445+
async fn compute_snip35_fee_proposal(
446+
&self,
447+
fee_actual: Option<GasPrice>,
448+
timestamp: u64,
449+
) -> GasPrice {
450+
let Some(fee_actual) = fee_actual else {
451+
warn!("fee_actual unavailable, freezing fee_proposal at l2_gas_price");
452+
SNIP35_FEE_PROPOSAL.set_lossy(self.l2_gas_price.0);
453+
return self.l2_gas_price;
454+
};
455+
SNIP35_FEE_ACTUAL.set_lossy(fee_actual.0);
456+
457+
let fee_target = match &self.deps.strk_to_usd_oracle {
458+
Some(oracle) => match oracle.fetch_rate(timestamp).await {
459+
Ok(rate) => {
460+
SNIP35_STRK_USD_RATE.set_lossy(rate);
461+
let target = compute_fee_target(TARGET_ATTO_USD_PER_L2_GAS, rate);
462+
match target {
463+
Some(t) => SNIP35_FEE_TARGET.set_lossy(t.0),
464+
None => warn!("STRK/USD oracle returned zero rate, freezing fee_proposal"),
465+
}
466+
target
467+
}
468+
Err(e) => {
469+
warn!("STRK/USD oracle error: {e:?}, freezing fee_proposal");
470+
None
471+
}
472+
},
473+
None => {
474+
debug!("No STRK/USD oracle configured, freezing fee_proposal");
475+
None
476+
}
477+
};
478+
479+
let proposal = compute_fee_proposal(fee_target, fee_actual, FEE_PROPOSAL_MARGIN_PPT);
480+
SNIP35_FEE_PROPOSAL.set_lossy(proposal.0);
481+
proposal
482+
}
483+
429484
fn update_l2_gas_price(&mut self, height: BlockNumber, l2_gas_used: GasAmount) {
430485
self.l2_gas_price = self.calculate_next_l2_gas_price(height, l2_gas_used);
431486
let gas_price_u64 = u64::try_from(self.l2_gas_price.0).unwrap_or(u64::MAX);
@@ -656,6 +711,9 @@ impl ConsensusContext for SequencerConsensusContext {
656711
BehaviorMode::Echonet => true,
657712
BehaviorMode::Starknet => false,
658713
};
714+
let fee_actual = compute_fee_actual(&self.fee_proposals_window, build_param.height);
715+
let fee_proposal =
716+
self.compute_snip35_fee_proposal(fee_actual, self.deps.clock.unix_now()).await;
659717
let round = build_param.round;
660718
let args = ProposalBuildArguments {
661719
deps: self.deps.clone(),
@@ -689,6 +747,8 @@ impl ConsensusContext for SequencerConsensusContext {
689747
.config
690748
.dynamic_config
691749
.compare_retrospective_block_hash,
750+
fee_proposal,
751+
fee_actual,
692752
};
693753

694754
let handle = tokio::spawn(

crates/apollo_consensus_orchestrator/src/sequencer_consensus_context_test.rs

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ use apollo_l1_gas_price_types::errors::{
2727
L1GasPriceClientError,
2828
L1GasPriceProviderError,
2929
};
30-
use apollo_l1_gas_price_types::{MockL1GasPriceProviderClient, PriceInfo};
30+
use apollo_l1_gas_price_types::{
31+
MockExchangeRateOracleClientTrait,
32+
MockL1GasPriceProviderClient,
33+
PriceInfo,
34+
};
3135
use apollo_protobuf::consensus::{
3236
BuildParam,
3337
CommitmentParts,
@@ -89,6 +93,7 @@ fn expected_l2_gas_info_for_build_proposal_defaults() -> L2GasInfo {
8993
l2_gas_used: GasAmount(0),
9094
}
9195
}
96+
use crate::snip35::{compute_fee_actual, FEE_PROPOSAL_WINDOW_SIZE};
9297
use crate::utils::{apply_fee_transformations, make_gas_price_params};
9398

9499
const TEST_PROPOSAL_COMMITMENT: ProposalCommitment = ProposalCommitment(PARTIAL_BLOCK_HASH.0);
@@ -1601,3 +1606,137 @@ async fn test_initialize_fee_proposals_window(
16011606
context.initialize_fee_proposals_window(start_height).await.unwrap();
16021607
assert_eq!(context.fee_proposals_window, expected_window);
16031608
}
1609+
1610+
#[derive(Clone)]
1611+
enum OracleBehavior {
1612+
/// No oracle is configured (`strk_to_usd_oracle = None`).
1613+
NotConfigured,
1614+
/// Oracle is configured and `fetch_rate` returns `Ok(rate)`.
1615+
Ok(u128),
1616+
/// Oracle is configured and `fetch_rate` returns `Err(_)`.
1617+
Err,
1618+
}
1619+
1620+
// fee_actual = 10 gwei => margin bounds [9_980_039_920, 10_020_000_000].
1621+
#[rstest]
1622+
#[case::no_fee_actual_freezes_at_l2_gas_price(
1623+
None,
1624+
OracleBehavior::NotConfigured,
1625+
GasPrice(7_000_000_000),
1626+
GasPrice(7_000_000_000)
1627+
)]
1628+
#[case::no_oracle_freezes_at_fee_actual(
1629+
Some(GasPrice(10_000_000_000)),
1630+
OracleBehavior::NotConfigured,
1631+
GasPrice(7_000_000_000),
1632+
GasPrice(10_000_000_000)
1633+
)]
1634+
#[case::oracle_zero_rate_freezes_at_fee_actual(
1635+
Some(GasPrice(10_000_000_000)),
1636+
OracleBehavior::Ok(0),
1637+
GasPrice(7_000_000_000),
1638+
GasPrice(10_000_000_000)
1639+
)]
1640+
#[case::oracle_err_freezes_at_fee_actual(
1641+
Some(GasPrice(10_000_000_000)),
1642+
OracleBehavior::Err,
1643+
GasPrice(7_000_000_000),
1644+
GasPrice(10_000_000_000)
1645+
)]
1646+
#[case::oracle_target_in_bounds_returns_target(
1647+
Some(GasPrice(10_000_000_000)),
1648+
OracleBehavior::Ok(300_000_000_000_000_000),
1649+
GasPrice(7_000_000_000),
1650+
GasPrice(10_000_000_000)
1651+
)]
1652+
#[case::oracle_target_above_clamps_to_upper(
1653+
Some(GasPrice(10_000_000_000)),
1654+
OracleBehavior::Ok(1),
1655+
GasPrice(7_000_000_000),
1656+
GasPrice(10_020_000_000)
1657+
)]
1658+
#[case::oracle_target_below_clamps_to_lower(
1659+
Some(GasPrice(10_000_000_000)),
1660+
OracleBehavior::Ok(1_000_000_000_000_000_000_000),
1661+
GasPrice(7_000_000_000),
1662+
GasPrice(9_980_039_920)
1663+
)]
1664+
#[tokio::test]
1665+
async fn test_compute_snip35_fee_proposal(
1666+
#[case] fee_actual: Option<GasPrice>,
1667+
#[case] oracle_behavior: OracleBehavior,
1668+
#[case] l2_gas_price: GasPrice,
1669+
#[case] expected_fee_proposal: GasPrice,
1670+
) {
1671+
let (mut deps, _network) = create_test_and_network_deps();
1672+
deps.setup_default_expectations();
1673+
deps.strk_to_usd_oracle = match oracle_behavior {
1674+
OracleBehavior::NotConfigured => None,
1675+
OracleBehavior::Ok(rate) => {
1676+
let mut mock = MockExchangeRateOracleClientTrait::new();
1677+
mock.expect_fetch_rate().returning(move |_| Ok(rate));
1678+
Some(Arc::new(mock))
1679+
}
1680+
OracleBehavior::Err => {
1681+
let mut mock = MockExchangeRateOracleClientTrait::new();
1682+
mock.expect_fetch_rate().returning(|_| {
1683+
Err(ExchangeRateOracleClientError::RequestError("test".to_string()))
1684+
});
1685+
Some(Arc::new(mock))
1686+
}
1687+
};
1688+
1689+
let mut context = deps.build_context();
1690+
context.l2_gas_price = l2_gas_price;
1691+
let proposal = context.compute_snip35_fee_proposal(fee_actual, 0).await;
1692+
assert_eq!(proposal, expected_fee_proposal);
1693+
}
1694+
1695+
#[tokio::test]
1696+
async fn test_compute_snip35_fee_proposal_converges_to_oracle_target() {
1697+
// (strk_usd_rate, fee_target, n_blocks_until_convergence_with_buffer).
1698+
// 75 gwei bootstrap -> 100 gwei target at +33% reaches by block ~795.
1699+
// 100 gwei -> 60 gwei at -40% reaches by block ~1410.
1700+
let phases: [(u128, GasPrice, u64); 2] = [
1701+
(30_000_000_000_000_000, GasPrice(100_000_000_000), 800),
1702+
(50_000_000_000_000_000, GasPrice(60_000_000_000), 1420),
1703+
];
1704+
1705+
let (mut deps, _network) = create_test_and_network_deps();
1706+
deps.setup_default_expectations();
1707+
let mut mock = MockExchangeRateOracleClientTrait::new();
1708+
let mut seq = mockall::Sequence::new();
1709+
for &(rate, _, n_blocks) in &phases {
1710+
mock.expect_fetch_rate()
1711+
.times(usize::try_from(n_blocks).unwrap())
1712+
.in_sequence(&mut seq)
1713+
.returning(move |_| Ok(rate));
1714+
}
1715+
deps.strk_to_usd_oracle = Some(Arc::new(mock));
1716+
let mut context = deps.build_context();
1717+
1718+
// Bootstrap the window with 75 gwei (the $0.04 target).
1719+
let window_size = u64::try_from(FEE_PROPOSAL_WINDOW_SIZE).unwrap();
1720+
for h in 0..window_size {
1721+
context.record_fee_proposal(BlockNumber(h), Some(GasPrice(75_000_000_000)));
1722+
}
1723+
1724+
let mut height = window_size;
1725+
for (phase_idx, (_, fee_target, n_blocks)) in phases.into_iter().enumerate() {
1726+
for _ in 0..n_blocks {
1727+
let h = BlockNumber(height);
1728+
let fee_actual = compute_fee_actual(&context.fee_proposals_window, h)
1729+
.expect("window stays complete across the loop");
1730+
let proposal = context.compute_snip35_fee_proposal(Some(fee_actual), 0).await;
1731+
context.record_fee_proposal(h, Some(proposal));
1732+
height += 1;
1733+
}
1734+
let final_fee_actual =
1735+
compute_fee_actual(&context.fee_proposals_window, BlockNumber(height))
1736+
.expect("window stays complete across the loop");
1737+
assert_eq!(
1738+
final_fee_actual, fee_target,
1739+
"phase {phase_idx}: fee_actual did not reach fee_target after {n_blocks} blocks",
1740+
);
1741+
}
1742+
}

0 commit comments

Comments
 (0)