Skip to content

apollo_consensus_orchestrator,apollo_versioned_constants: cap the L2 gas price at 10x the minimum - #14946

Open
asaf-sw wants to merge 1 commit into
mainfrom
asaf/l1-oracle-06-cap-l2-gas-price
Open

apollo_consensus_orchestrator,apollo_versioned_constants: cap the L2 gas price at 10x the minimum#14946
asaf-sw wants to merge 1 commit into
mainfrom
asaf/l1-oracle-06-cap-l2-gas-price

Conversation

@asaf-sw

@asaf-sw asaf-sw commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Consensus-critical. Off main, not in the L1 oracle stack, but motivated by it.

Requirement

From Ohad Barta: cap the L2 gas price in both directions, to [minimum, 10x minimum], so a malicious or broken STRK/USD oracle cannot make the network unusable. His words: "I don't want a malicious oracle to be able to stop Starknet by causing everyone to pay $1000s on a transfer." A floor exists today; there was no ceiling anywhere.

Two cap sites, because one is not enough

Both were needed, and the trace is the interesting part:

  • The published price (calculate_next_l2_gas_price_for_fincalculate_next_base_gas_price) is pure EIP-1559 with max(adjusted, min) and no ceiling. At the shipped constants the growth factor is 1.0954/block, so sustained full blocks reach 10x the minimum in 26 blocks (~68 s) with no oracle involvement at all. Capping only the SNIP-35 band would have left the requirement unmet.
  • The fee-proposal band (fee_proposal_bounds) still needs capping, because fee_actual enters the fin path as a floor (effective_min = max(config_min, fee_actual)). An uncapped band lets the oracle push the floor above the ceiling. Capping there also gets the validator-side rejection free, since proposer and validator share that call site.

max_gas_price_multiplier: 10 is a versioned constant, gated on starknet_version >= V0_14_4, so replaying 0.14.3-and-earlier blocks is bit-identical.

Liveness: the cap must clamp the whole band, not just its top

upper = min(upper, cap) alone is a halt. If STRK genuinely collapses more than 10x, fee_actual legitimately rises above the cap, an honest proposer publishes cap, and the validator computes lower = fee_actual * 1000/1002 which is now above cap — so the honest proposal is rejected and blocks stop. Both edges are clamped, so the band collapses to the single point cap, every proposal is exactly cap, and within fee_proposal_window_size blocks every window entry is <= cap, hence fee_actual <= cap and the band reopens.

(Note the convergence argument is not median monotonicity, which is false: window [1,300,300,300,300,300,1,1,1,1] with cap 100 has the median rise 150 → 200. The correct argument is that upper <= cap bounds every accepted proposal, so the window drains to <= cap regardless.)

Review also caught that compute_fee_proposal returned fee_actual unclamped on the oracle-failure path. Harmless before a cap existed; a halt after it, on every oracle failure while the price is pressed against the ceiling. Both paths now go through the same clamp.

Version skew

Gated on the block's starknet_version, threaded explicitly so init and fin cannot disagree. Review found the gate was originally read through three different expressions (effective_starknet_version() on the proposer, StarknetVersion::LATEST on the validator, the block's version in the fin) — benign only because those coincide today, and a divergence the moment LATEST moves past V0_14_4. All three now use the block's version.

  • Capped proposer, uncapped validator: safe, the capped band is a subset of the uncapped one.
  • Uncapped proposer, capped validator: rejected rounds only while the price presses against the cap, and validate_proposal.rs rejects a version mismatch before the band check, so both sides are always on the same side of the gate for a given block.

Metrics for the forthcoming alerts

Ohad wants alerts on the calculation drifting outside the cap, not on the published price sitting at a bound (once capped, the published value carries no signal). Two counters, deliberately not gauges: CONSENSUS_L2_GAS_PRICE_CLAMPED (labeled minimum/maximum) and SNIP35_FEE_TARGET_ABOVE_MAXIMUM.

Counters because a registered-but-unset gauge renders as 0 in the Prometheus exporter, so a "below minimum" alert on a fresh gauge fires on every pod restart. A counter's 0 is instead semantically true, and increase(metric[window]) > 0 is defined across resets.

Testing

218 → 248 tests. Verified by mutation: replacing effective_min = min(snip35_min, cap) with effective_min = snip35_min previously left all 218 tests passing (its only observable effect is the clamp counter, and nothing asserted it). It now fails on the snip35_floor_above_the_ceiling case with the exact predicted miscount.

override_l2_gas_price_fri

Bypasses the published-price cap (matching how it already bypasses the floor, so Echonet can replay a mainnet block's own price), but not the fee-proposal band cap (the validator has no knowledge of the proposer's override config, so the band cannot depend on it).

Worth a separate discussion, not changed here

The ceiling derives from min_l2_gas_price_per_height, which is per-deployment config. Config drift is already fatal today whenever the floor binds — which is the steady state on a quiet chain — so this adds no new operational requirement. What it adds is a second binding regime and a config dependence in the fee-proposal band, which was previously config-independent. Folding the configured minimums into version_constant_commitment would convert a latent trap into a loud init rejection; worth filing separately.

Also worth flagging as a policy consequence rather than a defect: capping the published price means sustained congestion stops being price-rationed after ~1 minute. That is inherent to "cap the price in both directions" and may warrant a larger multiplier.

🤖 Generated with Claude Code

…gas price at 10x the minimum

A broken or malicious STRK/USD oracle reporting STRK far too cheap drives the
SNIP-35 fee target arbitrarily high, and sustained congestion drives the
EIP-1559 base price up ~9.5% per full block. Neither had a ceiling.

Cap the published L2 gas price and the SNIP-35 fee_proposal band at
max_gas_price_multiplier (10) times the minimum in force for the height, gated
on starknet_version >= V0_14_4 so proposer and validator switch together. The
ceiling caps both edges of the fee_proposal band, not just the upper one, so an
honest proposal pinned to the ceiling is still accepted when fee_actual sits
above it.

Both sides read the gate from the block's own starknet_version: the proposer
reads it once per proposal and threads it into the init, the band and the fin,
and the validator takes it from the init it is checking. A process whose
effective version differs from LATEST therefore cannot publish a proposal its
peers reject.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@reviewable-StarkWare

Copy link
Copy Markdown

This change is Reviewable

asaf-sw commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants