Skip to content

Commit dcf0feb

Browse files
apollo_consensus_orchestrator: add SNIP-35 fee_proposal validation
1 parent 1550bb9 commit dcf0feb

4 files changed

Lines changed: 59 additions & 6 deletions

File tree

crates/apollo_consensus_orchestrator/src/sequencer_consensus_context.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -784,6 +784,7 @@ impl ConsensusContext for SequencerConsensusContext {
784784
.map(GasPrice)
785785
.unwrap_or(self.l2_gas_price),
786786
starknet_version: StarknetVersion::LATEST,
787+
fee_actual: self.compute_fee_actual(),
787788
};
788789
self.validate_current_round_proposal(
789790
init,
@@ -1079,6 +1080,7 @@ impl ConsensusContext for SequencerConsensusContext {
10791080
.map(GasPrice)
10801081
.unwrap_or(self.l2_gas_price),
10811082
starknet_version: StarknetVersion::LATEST,
1083+
fee_actual: self.compute_fee_actual(),
10821084
};
10831085
self.validate_current_round_proposal(
10841086
init,

crates/apollo_consensus_orchestrator/src/test_utils.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -430,11 +430,12 @@ pub(crate) fn proposal_init(height: BlockNumber, round: u32) -> ProposalInit {
430430
l1_data_gas_price_wei,
431431
starknet_version: starknet_api::block::StarknetVersion::LATEST,
432432
version_constant_commitment: Default::default(),
433-
// Match the proposer's default-fallback fee_proposal: empty sliding window →
434-
// `compute_fee_actual` returns `None` → `compute_snip35_fee_proposal` falls back to
435-
// `self.l2_gas_price` (= `min_gas_price` = 8 gwei). Validator-side tests reuse this
436-
// helper to fabricate an init that matches what the proposer emits, so proposer and
437-
// validator agree on the proposal commitment hash.
433+
// SNIP-35: required because LATEST >= V0_14_3. Match the proposer's default-fallback
434+
// fee_proposal: empty sliding window → `compute_fee_actual` returns `None` →
435+
// `compute_snip35_fee_proposal` falls back to `self.l2_gas_price`
436+
// (= `min_gas_price` = 8 gwei). Validator-side tests reuse this helper to fabricate
437+
// an init that matches what the proposer emits, so proposer and validator agree on
438+
// the proposal commitment hash.
438439
fee_proposal_fri: Some(GasPrice(8_000_000_000)),
439440
}
440441
}

crates/apollo_consensus_orchestrator/src/validate_proposal.rs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ use crate::metrics::{
4242
CONSENSUS_PROPOSAL_FIN_MISMATCH,
4343
};
4444
use crate::sequencer_consensus_context::{BuiltProposals, SequencerConsensusContextDeps};
45-
use crate::snip35::proposal_commitment_from;
45+
use crate::snip35::{fee_proposal_bounds, proposal_commitment_from, FEE_PROPOSAL_MARGIN_PPT};
4646
use crate::utils::{
4747
convert_to_sn_api_block_info,
4848
get_l1_prices_in_fri_and_wei,
@@ -78,6 +78,9 @@ pub(crate) struct ProposalInitValidation {
7878
pub l1_da_mode: L1DataAvailabilityMode,
7979
pub l2_gas_price_fri: GasPrice,
8080
pub starknet_version: StarknetVersion,
81+
/// SNIP-35: fee_actual from the sliding window. `None` during initiation, before the window
82+
/// has accumulated `FEE_PROPOSAL_WINDOW_SIZE` entries.
83+
pub fee_actual: Option<GasPrice>,
8184
}
8285

8386
/// Parameters for deadline and cancellation handling during proposal finalization.
@@ -333,6 +336,52 @@ async fn is_proposal_init_valid(
333336
),
334337
));
335338
}
339+
340+
// SNIP-35: fee_proposal is required iff Starknet version >= V0_14_3.
341+
let snip35_active = init_proposed.starknet_version >= StarknetVersion::V0_14_3;
342+
match (init_proposed.fee_proposal_fri, snip35_active) {
343+
(Some(_), false) => {
344+
return Err(ValidateProposalError::InvalidProposalInit(
345+
init_proposed.clone(),
346+
proposal_init_validation.clone(),
347+
format!(
348+
"fee_proposal must be absent before V0_14_3, got Some at version {}",
349+
init_proposed.starknet_version
350+
),
351+
));
352+
}
353+
(None, true) => {
354+
return Err(ValidateProposalError::InvalidProposalInit(
355+
init_proposed.clone(),
356+
proposal_init_validation.clone(),
357+
format!(
358+
"fee_proposal is required at V0_14_3+, got None at version {}",
359+
init_proposed.starknet_version
360+
),
361+
));
362+
}
363+
_ => {}
364+
}
365+
366+
// SNIP-35: validate fee_proposal is within the configured margin of fee_actual.
367+
// During initiation (fee_actual is None, <window_size blocks), bounds are not enforced.
368+
if let (Some(fee_actual), Some(fee_proposal)) =
369+
(proposal_init_validation.fee_actual, init_proposed.fee_proposal_fri)
370+
{
371+
let (lower_bound, upper_bound) = fee_proposal_bounds(fee_actual, FEE_PROPOSAL_MARGIN_PPT);
372+
if fee_proposal.0 < lower_bound || fee_proposal.0 > upper_bound {
373+
return Err(ValidateProposalError::InvalidProposalInit(
374+
init_proposed.clone(),
375+
proposal_init_validation.clone(),
376+
format!(
377+
"Fee proposal out of SNIP-35 bounds: fee_actual={}, fee_proposal={}, allowed \
378+
range=[{lower_bound}, {upper_bound}]",
379+
fee_actual.0, fee_proposal.0
380+
),
381+
));
382+
}
383+
}
384+
336385
Ok(())
337386
}
338387

crates/apollo_consensus_orchestrator/src/validate_proposal_test.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ fn create_proposal_validate_arguments()
103103
l1_da_mode: L1DataAvailabilityMode::Blob,
104104
l2_gas_price_fri: VersionedConstants::latest_constants().min_gas_price,
105105
starknet_version: StarknetVersion::LATEST,
106+
fee_actual: None,
106107
};
107108
let proposal_id = ProposalId(1);
108109
let timeout = TIMEOUT;

0 commit comments

Comments
 (0)