Skip to content

apollo_l1_gas_price,apollo_l1_gas_price_config,apollo_node: select oracle source per feed - #14944

Open
asaf-sw wants to merge 1 commit into
asaf/l1-oracle-03-chainlink-oracle-clientfrom
asaf/l1-oracle-05-select-oracle-source
Open

apollo_l1_gas_price,apollo_l1_gas_price_config,apollo_node: select oracle source per feed#14944
asaf-sw wants to merge 1 commit into
asaf/l1-oracle-03-chainlink-oracle-clientfrom
asaf/l1-oracle-05-select-oracle-source

Conversation

@asaf-sw

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

Copy link
Copy Markdown
Contributor

Stacked on #14942.

What

Makes ChainlinkOracleClient selectable from node config, per feed. #14942 landed it dark; this is the go-live switch, so enabling Chainlink is a config rollout, not a code deploy.

Both feeds default to Http. The schema diff is 14 additions, with no existing parameter's value, privacy, or description changed, verified by set-diffing the parsed JSON rather than eyeballing it.

The two feeds are independently switchable so they can be migrated one at a time.

Validation is now actually reachable

ChainlinkOracleConfig is nested under L1GasPriceProviderConfig with #[validate(nested)], which makes the cross-field validation added in #14942 live for the first time (main.rsload_and_validate_configvalidate_node_config.validate()). That matters: ChainlinkOracleClient::new expects a non-zero max_cache_size and lag_interval_seconds, so without this an operator zero surfaces as a node panic instead of a config error.

The transposition that tests could not see

Review found the original factory shape took rate_kind and the per-feed HTTP config as independent arguments. Swapping them compiled, ran, and passed all 99 tests — because both feeds default to a byte-identical ExchangeRateOracleConfig, and the assertions only checked the client's concrete type. The same was true of the two metrics constants: swapping those would have published every ETH/STRK reading under snip35_strk_usd_* and vice versa, silently pointing every dashboard and alert at the wrong series.

Fixed structurally rather than with more assertions: the factory takes the whole L1GasPriceProviderConfig and derives the HTTP config, metrics bundle, and config key from rate_kind, so the transposition is now unrepresentable. The rate-kind-to-metrics mapping lives in exactly one place (ChainlinkRateKind::metrics()), where it was previously duplicated between the factory and ChainlinkOracleClient::new.

Tests are behavioural rather than Debug-scraping: two mockito servers return distinct prices, and the test asserts both that each rate resolves to its own feed and that it lands on its own Prometheus series. Each fix was then verified by mutation — transposing the configs, the metrics, and the rate kinds each now fails a test named for it.

Misconfiguration behaviour

Selecting Chainlink where no batcher client exists fails at startup, naming every offending config key at once rather than only the first (which would have cost the operator two restart cycles for one mistake). Every shipped topology — consolidated, hybrid, distributed — gives the L1 service a batcher client, so this is reachable only by misconfiguration.

An invalid enum value fails loudly: the enum has no rename_all, so a lowercase "chainlink" is rejected at config load rather than silently falling back to Http and making a migration look successful while nothing changed. There's a test pinning that.

Operator-facing

The bound parameters are in micro units, which was explained only in a Rust doc comment and therefore never reached the schema. An operator would see value: 20000000 described as "micro-USD per ETH" and have to guess it means $20, where being wrong by 10^6 either disables the guard or rejects every reading. Every bound description now anchors the scale with a worked conversion, and both source descriptions state that selecting Chainlink without a batcher is a startup failure rather than a fallback.

Before flipping a feed

Selecting Chainlink in hybrid or distributed routes every rate query through a remote call to the batcher from the L1 pod, and the ETH/STRK branch issues two per uncached bucket. #14945 (batcher view-call resource bounds) should land before any feed is flipped to Chainlink; it is off main rather than in this stack.

🤖 Generated with Claude Code

@reviewable-StarkWare

Copy link
Copy Markdown

This change is Reviewable

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c0801bf. Configure here.

Comment thread crates/apollo_l1_gas_price_config/src/config.rs
@asaf-sw
asaf-sw force-pushed the asaf/l1-oracle-05-select-oracle-source branch from c0801bf to 4f1581b Compare August 11, 2026 10:05
@asaf-sw
asaf-sw force-pushed the asaf/l1-oracle-03-chainlink-oracle-client branch from 08b8979 to d0678cb Compare August 11, 2026 10:51
@asaf-sw
asaf-sw force-pushed the asaf/l1-oracle-05-select-oracle-source branch from 4f1581b to 131ddb0 Compare August 11, 2026 10:51

asaf-sw commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

…acle source per feed

Makes `ChainlinkOracleClient` selectable from node config, per feed. The previous
PR landed it dark; this is the go-live switch, so enabling Chainlink becomes a
config rollout rather than a code deploy.

Both feeds default to `Http`, so there is no production behavior change. The
schema diff is 14 additions, no existing parameter's value, privacy, or
description moved.

The two feeds are selected independently so they can be migrated one at a time.

`ChainlinkOracleConfig` is now nested under `L1GasPriceProviderConfig` with
`#[validate(nested)]`, which makes the validation added in the previous PR
actually reachable from the node config root. That matters: `ChainlinkOracleClient::new`
expects a non-zero `max_cache_size` and `lag_interval_seconds`, so without it an
operator zero would surface as a node panic instead of a config error.

The factory takes the whole `L1GasPriceProviderConfig` and derives each feed's
HTTP config and metrics bundle from its rate kind, so a feed cannot be paired
with another feed's config or metrics. Review found that the earlier shape, which
passed them as independent arguments, allowed a transposition that every test
still passed: both feeds default to an identical `ExchangeRateOracleConfig`, and
the assertions only checked the client's type. The rate-kind-to-metrics mapping
now exists in exactly one place.

Selecting `Chainlink` where no batcher client exists fails at startup, naming
every offending config key at once, rather than panicking later or silently
serving HTTP. Every shipped topology (consolidated, hybrid, distributed) gives
the L1 service a batcher client, so this is reachable only by misconfiguration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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