Skip to content

apollo_consensus_orchestrator,apollo_l1_gas_price: bound the per-block eth to fri rate change - #14973

Open
asaf-sw wants to merge 1 commit into
asaf/l1-oracle-08-oracle-staleness-alertfrom
asaf/l1-oracle-10-rate-change-bound
Open

apollo_consensus_orchestrator,apollo_l1_gas_price: bound the per-block eth to fri rate change#14973
asaf-sw wants to merge 1 commit into
asaf/l1-oracle-08-oracle-staleness-alertfrom
asaf/l1-oracle-10-rate-change-bound

Conversation

@asaf-sw

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

Copy link
Copy Markdown
Contributor

Bounds how far the ETH/FRI exchange rate may move from one block to the next, and clamps a fresh oracle rate into that band.

Why

The Chainlink oracle (#14942) guards each reading with absolute sanity bounds, but those are deliberately wide: the STRK/USD band spans five decades, because its job is wrong-feed detection. A manipulated but plausible feed value passes them. Only a bound relative to the previously accepted rate catches that, and #14944 names this bound as a precondition before any feed is flipped to the on-chain oracle.

Why the orchestrator and not the oracle client

A bound anchored to the Chainlink client's own last read is node-local history: two validators with different read histories would accept and reject different proposals, which is exactly what the original TODO ruled out.

The bound is implemented in apollo_consensus_orchestrator::utils, on the oracle-fetch path, where the previous block is already in hand. The anchor is the rate implied by the previous block's recorded wei and fri prices, derived with the existing calculate_eth_to_fri_rate. That makes the bound a function of the block header and of config alone: every validator derives the same band and maps a given oracle reading to the same rate. It also covers both oracle sources, HTTP and Chainlink, which a bound inside the Chainlink client would not.

The TODO in chainlink_oracle.rs is replaced with a comment pointing at where the relative bound now lives.

Clamp, not reject

A rate outside the band is clamped to the band's edge, following the shape of the existing per-block L2 gas price clamp (calculate_next_l2_gas_price_for_fin), including multiply-before-divide in U256 and a truncating division that makes the band marginally tighter, never looser. Clamping bounds per-block damage and forces a manipulation to be sustained across many blocks to move the price far.

Default

max_eth_to_fri_rate_change_ppt defaults to 50, i.e. 5% per block.

  • Not too tight: the ETH and STRK feeds update on deviation thresholds of roughly 0.5% and 1%, so their ratio steps by at most ~1.5% between consecutive blocks. A normal feed update never trips the bound, so the metric stays at zero in healthy operation. A genuinely sharp market move is tracked within a few blocks.
  • Not too loose: one manipulated reading moves the rate by at most 5%, and doubling it requires holding the feed for about 15 consecutive blocks (~40 seconds at mainnet block times), long enough for the oracle staleness alert (apollo_dashboard: alert on the Chainlink oracle guard trips #14948) and the guard-trip alert to fire.

No previous block

At startup, and on the path that falls back to the minimal config values, there is no anchor, so the oracle rate is used as is. That branch is explicit and commented rather than implicit.

Observability

Clamping increments consensus_eth_to_fri_rate_clamped (registered alongside its neighbours) and logs a warning naming the oracle rate, the previous block's implied rate, and the clamped result.

Config wiring

max_eth_to_fri_rate_change_ppt is on ContextDynamicConfig, and is present in the node config schema, consensus_manager_config.json, and replacer_consensus_manager_config.json (a param in the schema but missing from the app configs crash-loops every node).

Tests

utils_test.rs: rate inside the bound passes through unchanged; rate above the bound clamps to the ceiling; rate below clamps to the floor; boundary exactly at each edge is unchanged; no previous block means no clamping; the metric increments only when clamping; and two independently constructed instances, given the same previous block and the same oracle rate, produce the same result even when they have made a different number of oracle calls.

🤖 Generated with Claude Code

@reviewable-StarkWare

Copy link
Copy Markdown

This change is Reviewable

asaf-sw commented Aug 16, 2026

Copy link
Copy Markdown
Contributor Author

@asaf-sw
asaf-sw force-pushed the asaf/l1-oracle-08-oracle-staleness-alert branch from f124225 to c42e211 Compare August 17, 2026 05:32
@asaf-sw
asaf-sw force-pushed the asaf/l1-oracle-10-rate-change-bound branch from e435c9a to 097cc5d Compare August 17, 2026 05:32
@asaf-sw
asaf-sw force-pushed the asaf/l1-oracle-08-oracle-staleness-alert branch from c42e211 to 695ba3e Compare August 17, 2026 06:06
@asaf-sw
asaf-sw force-pushed the asaf/l1-oracle-10-rate-change-bound branch from 097cc5d to af148bd Compare August 17, 2026 06:06
@asaf-sw
asaf-sw force-pushed the asaf/l1-oracle-08-oracle-staleness-alert branch from 695ba3e to 0c9e86b Compare August 17, 2026 06:14
@asaf-sw
asaf-sw force-pushed the asaf/l1-oracle-10-rate-change-bound branch from af148bd to d4a39ef Compare August 17, 2026 06:15
@asaf-sw
asaf-sw force-pushed the asaf/l1-oracle-08-oracle-staleness-alert branch from 0c9e86b to 3699473 Compare August 17, 2026 06:22
@asaf-sw
asaf-sw force-pushed the asaf/l1-oracle-10-rate-change-bound branch from d4a39ef to dc5ac9b Compare August 17, 2026 06:23
@asaf-sw
asaf-sw force-pushed the asaf/l1-oracle-08-oracle-staleness-alert branch from 3699473 to eeac62e Compare August 17, 2026 09:45
@asaf-sw
asaf-sw force-pushed the asaf/l1-oracle-10-rate-change-bound branch from dc5ac9b to 8875f7e Compare August 17, 2026 09:45
@asaf-sw
asaf-sw force-pushed the asaf/l1-oracle-08-oracle-staleness-alert branch from eeac62e to 08bbb1d Compare August 17, 2026 11:17
@asaf-sw
asaf-sw force-pushed the asaf/l1-oracle-10-rate-change-bound branch from 8875f7e to 543b60b Compare August 17, 2026 11:18
@asaf-sw
asaf-sw force-pushed the asaf/l1-oracle-08-oracle-staleness-alert branch from 08bbb1d to f315ddb Compare August 17, 2026 11:51
@asaf-sw
asaf-sw force-pushed the asaf/l1-oracle-10-rate-change-bound branch from 543b60b to b045861 Compare August 17, 2026 11:51
…k eth to fri rate change

The Chainlink oracle guards each reading with absolute sanity bounds, but those are
deliberately wide (the STRK/USD band spans five decades) because their job is wrong-feed
detection. A manipulated but plausible feed value passes them. A bound relative to the
previously accepted rate is what catches that, and #14944 names it as a precondition before
any feed is switched to the on-chain oracle.

The bound lives in the orchestrator, not in the Chainlink client. A bound anchored to the
client's own last read is node-local history: two validators with different read histories
would accept and reject different proposals. Anchoring instead on the rate implied by the
previous block's recorded wei and fri prices (calculate_eth_to_fri_rate) makes the bound a
function of the block header and of config alone, so every validator derives the same band
and maps a given oracle reading to the same rate. It also covers both oracle sources, HTTP
and Chainlink, which a bound inside the Chainlink client would not.

A fresh rate outside the band is clamped to the band's edge rather than rejected, following
the shape of the per-block L2 gas price clamp: clamping bounds per-block damage and forces a
manipulation to be sustained across many blocks to move the price far.

max_eth_to_fri_rate_change_ppt defaults to 50 (5% per block). The ETH and STRK feeds update
on deviation thresholds of roughly 0.5% and 1%, so the ratio steps by at most ~1.5% between
consecutive blocks and a normal feed update never trips the bound. On the other side, one
manipulated reading moves the rate by at most 5%, and doubling it requires holding the feed
for about 15 consecutive blocks (~40 seconds at mainnet block times), long enough for the
oracle staleness and guard alerts to fire. A genuine sharp move is tracked within a few
blocks.

When there is no previous block (startup, or the path that falls back to the minimal config
values) there is no anchor, so the oracle rate is used as is. That path is explicit.

Clamping increments consensus_eth_to_fri_rate_clamped and logs a warning.
@asaf-sw
asaf-sw force-pushed the asaf/l1-oracle-08-oracle-staleness-alert branch from f315ddb to 4f379db Compare August 17, 2026 13:52
@asaf-sw
asaf-sw force-pushed the asaf/l1-oracle-10-rate-change-bound branch from b045861 to a4a9809 Compare August 17, 2026 13:52
@asaf-sw
asaf-sw marked this pull request as ready for review August 17, 2026 14:05
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