Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions crates/apollo_consensus_orchestrator/src/build_proposal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ pub(crate) struct ProposalBuildArguments {
pub override_l2_gas_price_fri: Option<u128>,
pub min_l2_gas_price_per_height: Vec<PricePerHeight>,
pub compare_retrospective_block_hash: bool,
/// SNIP-35: proposer's fee_proposal for this block.
pub fee_proposal: GasPrice,
/// SNIP-35: current fee_actual from the sliding window.
pub fee_actual: Option<GasPrice>,
}

type BuildProposalResult<T> = Result<T, BuildProposalError>;
Expand Down Expand Up @@ -176,7 +180,7 @@ async fn initiate_build(args: &mut ProposalBuildArguments) -> BuildProposalResul
starknet_version: starknet_api::block::StarknetVersion::LATEST,
// TODO(Asmaa): Put the real value once we have it.
version_constant_commitment: Default::default(),
fee_proposal_fri: None,
fee_proposal_fri: Some(args.fee_proposal),
Comment thread
cursor[bot] marked this conversation as resolved.
};

let retrospective_block_hash = wait_for_retrospective_block_hash(
Expand Down Expand Up @@ -319,7 +323,7 @@ async fn get_proposal_content(
info.l2_gas_used,
args.override_l2_gas_price_fri,
&args.min_l2_gas_price_per_height,
None,
args.fee_actual,
);
let fin_payload = ProposalFinPayload {
commitment_parts: CommitmentParts::from(&info),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ use tokio::task::JoinHandle;
use tokio::time::sleep;
use tokio_util::sync::CancellationToken;
use tokio_util::task::AbortOnDropHandle;
use tracing::{error, error_span, info, instrument, trace, warn, Instrument};
use tracing::{debug, error, error_span, info, instrument, trace, warn, Instrument};

use crate::build_proposal::{build_proposal, BuildProposalError, ProposalBuildArguments};
use crate::cende::{
Expand All @@ -94,8 +94,19 @@ use crate::metrics::{
record_validate_proposal_failure,
register_metrics,
CONSENSUS_L2_GAS_PRICE,
SNIP35_FEE_ACTUAL,
SNIP35_FEE_PROPOSAL,
SNIP35_FEE_TARGET,
SNIP35_STRK_USD_RATE,
};
use crate::snip35::{
compute_fee_actual,
compute_fee_proposal,
compute_fee_target,
FEE_PROPOSAL_MARGIN_PPT,
FEE_PROPOSAL_WINDOW_SIZE,
TARGET_ATTO_USD_PER_L2_GAS,
};
use crate::snip35::FEE_PROPOSAL_WINDOW_SIZE;
use crate::utils::{
convert_to_sn_api_block_info,
make_gas_price_params,
Expand Down Expand Up @@ -398,6 +409,7 @@ impl SequencerConsensusContext {
sequencer,
timestamp: BlockTimestamp(init.timestamp),
l1_da_mode: init.l1_da_mode,
fee_proposal_fri: init.fee_proposal_fri,
// TODO(guy.f): Figure out where/if to get the values below from and fill them.
..Default::default()
};
Expand All @@ -416,16 +428,59 @@ impl SequencerConsensusContext {
/// Returns the next L2 gas price without mutating context. Used when building the fin and when
/// updating at decision time.
fn calculate_next_l2_gas_price(&self, height: BlockNumber, l2_gas_used: GasAmount) -> GasPrice {
let fee_actual = compute_fee_actual(&self.fee_proposals_window, height);
calculate_next_l2_gas_price_for_fin(
self.l2_gas_price,
height,
l2_gas_used,
self.config.dynamic_config.override_l2_gas_price_fri,
&self.config.dynamic_config.min_l2_gas_price_per_height,
None,
fee_actual,
)
}

/// SNIP-35 fee_proposal: clamp the oracle's `fee_target` to a margin around `fee_actual`.
/// When `fee_actual` is `None` (window incomplete), freeze at `l2_gas_price`; the validator
/// derives the same fallback so both sides agree.
async fn compute_snip35_fee_proposal(
&self,
fee_actual: Option<GasPrice>,
timestamp: u64,
) -> GasPrice {
let Some(fee_actual) = fee_actual else {
warn!("fee_actual unavailable, freezing fee_proposal at l2_gas_price");
SNIP35_FEE_PROPOSAL.set_lossy(self.l2_gas_price.0);
return self.l2_gas_price;
};
SNIP35_FEE_ACTUAL.set_lossy(fee_actual.0);
Comment thread
cursor[bot] marked this conversation as resolved.

let fee_target = match &self.deps.strk_to_usd_oracle {
Some(oracle) => match oracle.fetch_rate(timestamp).await {
Ok(rate) => {
SNIP35_STRK_USD_RATE.set_lossy(rate);
let target = compute_fee_target(TARGET_ATTO_USD_PER_L2_GAS, rate);
match target {
Some(t) => SNIP35_FEE_TARGET.set_lossy(t.0),
None => warn!("STRK/USD oracle returned zero rate, freezing fee_proposal"),
}
target
}
Err(e) => {
warn!("STRK/USD oracle error: {e:?}, freezing fee_proposal");
None
}
},
None => {
debug!("No STRK/USD oracle configured, freezing fee_proposal");
None
}
};

let proposal = compute_fee_proposal(fee_target, fee_actual, FEE_PROPOSAL_MARGIN_PPT);
SNIP35_FEE_PROPOSAL.set_lossy(proposal.0);
proposal
}

fn update_l2_gas_price(&mut self, height: BlockNumber, l2_gas_used: GasAmount) {
self.l2_gas_price = self.calculate_next_l2_gas_price(height, l2_gas_used);
let gas_price_u64 = u64::try_from(self.l2_gas_price.0).unwrap_or(u64::MAX);
Expand Down Expand Up @@ -656,6 +711,9 @@ impl ConsensusContext for SequencerConsensusContext {
BehaviorMode::Echonet => true,
BehaviorMode::Starknet => false,
};
let fee_actual = compute_fee_actual(&self.fee_proposals_window, build_param.height);
let fee_proposal =
self.compute_snip35_fee_proposal(fee_actual, self.deps.clock.unix_now()).await;
let round = build_param.round;
let args = ProposalBuildArguments {
deps: self.deps.clone(),
Expand Down Expand Up @@ -689,6 +747,8 @@ impl ConsensusContext for SequencerConsensusContext {
.config
.dynamic_config
.compare_retrospective_block_hash,
fee_proposal,
fee_actual,
};

let handle = tokio::spawn(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ use apollo_l1_gas_price_types::errors::{
L1GasPriceClientError,
L1GasPriceProviderError,
};
use apollo_l1_gas_price_types::{MockL1GasPriceProviderClient, PriceInfo};
use apollo_l1_gas_price_types::{
MockExchangeRateOracleClientTrait,
MockL1GasPriceProviderClient,
PriceInfo,
};
use apollo_protobuf::consensus::{
BuildParam,
CommitmentParts,
Expand Down Expand Up @@ -89,6 +93,7 @@ fn expected_l2_gas_info_for_build_proposal_defaults() -> L2GasInfo {
l2_gas_used: GasAmount(0),
}
}
use crate::snip35::{compute_fee_actual, FEE_PROPOSAL_WINDOW_SIZE};
use crate::utils::{apply_fee_transformations, make_gas_price_params};

const TEST_PROPOSAL_COMMITMENT: ProposalCommitment = ProposalCommitment(PARTIAL_BLOCK_HASH.0);
Expand Down Expand Up @@ -1601,3 +1606,137 @@ async fn test_initialize_fee_proposals_window(
context.initialize_fee_proposals_window(start_height).await.unwrap();
assert_eq!(context.fee_proposals_window, expected_window);
}

#[derive(Clone)]
enum OracleBehavior {
/// No oracle is configured (`strk_to_usd_oracle = None`).
NotConfigured,
/// Oracle is configured and `fetch_rate` returns `Ok(rate)`.
Ok(u128),
/// Oracle is configured and `fetch_rate` returns `Err(_)`.
Err,
}

// fee_actual = 10 gwei => margin bounds [9_980_039_920, 10_020_000_000].
#[rstest]
#[case::no_fee_actual_freezes_at_l2_gas_price(
None,
OracleBehavior::NotConfigured,
GasPrice(7_000_000_000),
GasPrice(7_000_000_000)
)]
#[case::no_oracle_freezes_at_fee_actual(
Some(GasPrice(10_000_000_000)),
OracleBehavior::NotConfigured,
GasPrice(7_000_000_000),
GasPrice(10_000_000_000)
)]
#[case::oracle_zero_rate_freezes_at_fee_actual(
Some(GasPrice(10_000_000_000)),
OracleBehavior::Ok(0),
GasPrice(7_000_000_000),
GasPrice(10_000_000_000)
)]
#[case::oracle_err_freezes_at_fee_actual(
Some(GasPrice(10_000_000_000)),
OracleBehavior::Err,
GasPrice(7_000_000_000),
GasPrice(10_000_000_000)
)]
#[case::oracle_target_in_bounds_returns_target(
Some(GasPrice(10_000_000_000)),
OracleBehavior::Ok(300_000_000_000_000_000),
GasPrice(7_000_000_000),
GasPrice(10_000_000_000)
)]
#[case::oracle_target_above_clamps_to_upper(
Some(GasPrice(10_000_000_000)),
OracleBehavior::Ok(1),
GasPrice(7_000_000_000),
GasPrice(10_020_000_000)
)]
#[case::oracle_target_below_clamps_to_lower(
Some(GasPrice(10_000_000_000)),
OracleBehavior::Ok(1_000_000_000_000_000_000_000),
GasPrice(7_000_000_000),
GasPrice(9_980_039_920)
)]
#[tokio::test]
async fn test_compute_snip35_fee_proposal(
#[case] fee_actual: Option<GasPrice>,
#[case] oracle_behavior: OracleBehavior,
#[case] l2_gas_price: GasPrice,
#[case] expected_fee_proposal: GasPrice,
) {
let (mut deps, _network) = create_test_and_network_deps();
deps.setup_default_expectations();
deps.strk_to_usd_oracle = match oracle_behavior {
OracleBehavior::NotConfigured => None,
OracleBehavior::Ok(rate) => {
let mut mock = MockExchangeRateOracleClientTrait::new();
mock.expect_fetch_rate().returning(move |_| Ok(rate));
Some(Arc::new(mock))
}
OracleBehavior::Err => {
let mut mock = MockExchangeRateOracleClientTrait::new();
mock.expect_fetch_rate().returning(|_| {
Err(ExchangeRateOracleClientError::RequestError("test".to_string()))
});
Some(Arc::new(mock))
}
};

let mut context = deps.build_context();
context.l2_gas_price = l2_gas_price;
let proposal = context.compute_snip35_fee_proposal(fee_actual, 0).await;
assert_eq!(proposal, expected_fee_proposal);
}

#[tokio::test]
async fn test_compute_snip35_fee_proposal_converges_to_oracle_target() {
// (strk_usd_rate, fee_target, n_blocks_until_convergence_with_buffer).
// 75 gwei bootstrap -> 100 gwei target at +33% reaches by block ~795.
// 100 gwei -> 60 gwei at -40% reaches by block ~1410.
let phases: [(u128, GasPrice, u64); 2] = [
(30_000_000_000_000_000, GasPrice(100_000_000_000), 800),
(50_000_000_000_000_000, GasPrice(60_000_000_000), 1420),
];

let (mut deps, _network) = create_test_and_network_deps();
deps.setup_default_expectations();
let mut mock = MockExchangeRateOracleClientTrait::new();
let mut seq = mockall::Sequence::new();
for &(rate, _, n_blocks) in &phases {
mock.expect_fetch_rate()
.times(usize::try_from(n_blocks).unwrap())
.in_sequence(&mut seq)
.returning(move |_| Ok(rate));
}
deps.strk_to_usd_oracle = Some(Arc::new(mock));
let mut context = deps.build_context();

// Bootstrap the window with 75 gwei (the $0.04 target).
let window_size = u64::try_from(FEE_PROPOSAL_WINDOW_SIZE).unwrap();
for h in 0..window_size {
context.record_fee_proposal(BlockNumber(h), Some(GasPrice(75_000_000_000)));
}

let mut height = window_size;
for (phase_idx, (_, fee_target, n_blocks)) in phases.into_iter().enumerate() {
for _ in 0..n_blocks {
let h = BlockNumber(height);
let fee_actual = compute_fee_actual(&context.fee_proposals_window, h)
.expect("window stays complete across the loop");
let proposal = context.compute_snip35_fee_proposal(Some(fee_actual), 0).await;
context.record_fee_proposal(h, Some(proposal));
height += 1;
}
let final_fee_actual =
compute_fee_actual(&context.fee_proposals_window, BlockNumber(height))
.expect("window stays complete across the loop");
assert_eq!(
final_fee_actual, fee_target,
"phase {phase_idx}: fee_actual did not reach fee_target after {n_blocks} blocks",
);
}
}
Loading
Loading