apollo_l1_gas_price,apollo_l1_gas_price_config,apollo_node: build oracle clients per feed source - #14996
Conversation
|
Migrated review comment from #14944, closed in favor of this split. @matanl-starkware wrote on
This PR owns the code the comment lands on. The enum is gone, replaced by the |
|
Migrated review comment from #14944, closed in favor of this split. @matanl-starkware wrote on
This PR owns that signature. |
|
Migrated review comment from #14944, closed in favor of this split. @matanl-starkware wrote on
This PR owns the per-feed source selection the comment lands on: |
1e1b594 to
c943195
Compare
9b08295 to
8838a94
Compare
63bef79 to
b0466ac
Compare
ce1a0fb to
841a503
Compare
2e568f4 to
ce1c4cb
Compare
841a503 to
7aa8e2c
Compare
ce1c4cb to
ee93c99
Compare
7aa8e2c to
0dbadcf
Compare
ee93c99 to
0df0ab2
Compare
0dbadcf to
6e811cd
Compare
0df0ab2 to
ef7f3e6
Compare
6e811cd to
2929bac
Compare
ef7f3e6 to
e80265b
Compare
6f727f9 to
ee2fc27
Compare
3e0d809 to
8dda4a3
Compare
2d6060f to
3e48217
Compare
8dda4a3 to
eff067f
Compare
3e48217 to
c60e35e
Compare
eff067f to
a0a3b49
Compare
c60e35e to
875b621
Compare
matanl-starkware
left a comment
There was a problem hiding this comment.
@matanl-starkware made 3 comments.
Reviewable status: 0 of 7 files reviewed, 3 unresolved discussions (waiting on asaf-sw).
crates/apollo_l1_gas_price/src/l1_gas_price_provider.rs line 73 at r2 (raw file):
// [Temporary comment] Deleted in C1 together with the two config params they name. const ETH_TO_STRK_ORACLE_SOURCE_CONFIG_KEY: &str = "l1_gas_price_provider_config.eth_to_strk_oracle_source";
Is it the same string that appears in the KEYS_TO_BE_REPLACED?
Can you make it a single source of truth?
Code quote:
"l1_gas_price_provider_config.eth_to_strk_oracle_source"crates/apollo_l1_gas_price/src/l1_gas_price_provider.rs line 91 at r2 (raw file):
// Every feed that selects `Chainlink`, so that one startup reports the whole misconfiguration. source_config_keys: Vec<&'static str>, }
Seems like an overkill to me. It's OK if the Operator gets 2 different errors.
Code quote:
/// Whether a batcher client is available is decided by the composition root, not by config, so this
/// is raised while the components are built rather than caught by config validation.
// [Temporary comment] Deleted in C1: once Chainlink is the only source, a provider without a
// batcher client cannot serve a rate at all, so there is no source selection left to reject.
#[derive(Debug, Eq, Error, PartialEq)]
#[error(
"Chainlink is selected as the oracle source by {}, but this service has no batcher client. \
Chainlink feeds are read through the batcher, so either run the batcher alongside this \
service or select Http instead.",
.source_config_keys.join(" and ")
)]
pub struct MissingBatcherClientError {
// Every feed that selects `Chainlink`, so that one startup reports the whole misconfiguration.
source_config_keys: Vec<&'static str>,
}crates/apollo_l1_gas_price/src/l1_gas_price_provider.rs line 123 at r2 (raw file):
batcher_client: Option<SharedBatcherClient>, ) -> Result<Self, MissingBatcherClientError> { // Both feeds are resolved before either failure is reported, so an operator who
I don't really think it's necessary.
Why do we even allow the batcher_client to be None under the "new_with_oracle" function?
This flow has no meaning without a client.
Code quote:
Both feeds are resolved before either failure is reporteda0a3b49 to
66d9c9a
Compare
asaf-sw
left a comment
There was a problem hiding this comment.
@asaf-sw+AGNT made 3 comments.
Reviewable status: 0 of 7 files reviewed, 3 unresolved discussions (waiting on matanl-starkware).
crates/apollo_l1_gas_price/src/l1_gas_price_provider.rs line 73 at r2 (raw file):
Previously, matanl-starkware (Matan Lior) wrote…
Is it the same string that appears in the KEYS_TO_BE_REPLACED?
Can you make it a single source of truth?
Done, and all three of these collapse into one deletion.
new_with_oracle now requires the batcher client and is infallible:
pub fn new_with_oracle(
config: L1GasPriceProviderConfig,
batcher_client: SharedBatcherClient,
) -> Selfcomponents.rs does the expect at the composition root. MissingBatcherClientError is gone, and with it the resolve-both-feeds logic, so there is no second error for an operator to receive either.
The two *_ORACLE_SOURCE_CONFIG_KEY consts existed only to name the offending key in that error message, so they are deleted too. That removes the string duplicated against KEYS_TO_BE_REPLACED rather than trying to share it, which was not reachable anyway: apollo_deployments has no dependency on apollo_l1_gas_price, and its phf_set! holds plain literals for every component key.
Net -111 lines, 98 tests pass in the crate.
crates/apollo_l1_gas_price/src/l1_gas_price_provider.rs line 91 at r2 (raw file):
Previously, matanl-starkware (Matan Lior) wrote…
Seems like an overkill to me. It's OK if the Operator gets 2 different errors.
Agreed, and it is gone entirely rather than reduced to two errors. new_with_oracle requires the batcher client now, so there is no failure to report: see the reply on the config-key thread for the shape.
crates/apollo_l1_gas_price/src/l1_gas_price_provider.rs line 123 at r2 (raw file):
Previously, matanl-starkware (Matan Lior) wrote…
I don't really think it's necessary.
Why do we even allow the batcher_client to be None under the "new_with_oracle" function?
This flow has no meaning without a client.
You are right, there is no meaning to this flow without a client. batcher_client is now SharedBatcherClient rather than Option, and components.rs resolves the Option from get_batcher_shared_client() with an expect at the composition root.
Checked before removing it: get_batcher_shared_client() returns None only for Client::Disabled, and every shipped topology gives the L1 service a batcher client, including distributed, where l1.json has components.batcher.execution_mode = Remote.
This also deleted the two tests that only existed to cover the removed error.
875b621 to
0bcb2c2
Compare
66d9c9a to
a84a96f
Compare
matanl-starkware
left a comment
There was a problem hiding this comment.
@matanl-starkware reviewed 7 files and all commit messages, and resolved 3 discussions.
Reviewable status:complete! all files reviewed, all discussions resolved (waiting on asaf-sw).
a84a96f to
7f24328
Compare
0bcb2c2 to
3bebf17
Compare
7f24328 to
053eaa6
Compare
3bebf17 to
5d46bbd
Compare
asaf-sw
left a comment
There was a problem hiding this comment.
@asaf-sw reviewed 7 files and all commit messages.
Reviewable status:complete! all files reviewed, all discussions resolved (waiting on asaf-sw).
053eaa6 to
beb6a2a
Compare
5d46bbd to
6e9bf2b
Compare
…cle clients per feed source `new_with_oracle` builds each feed's oracle client from the source selected for that feed, so enabling Chainlink is a config rollout rather than a code deploy. Both feeds default to `Http`, so there is no production behavior change. The factory is generic over the feed's rate kind, and the feed's source, HTTP config, metrics bundle, config key, and the Chainlink feeds and bounds it reads all come from that kind, so a feed's client cannot be built from the other feed's fields and the pair-to-metrics mapping stays in one place. Selecting `Chainlink` where no batcher client exists fails while the components are built, naming every offending config key at once, rather than falling back to `Http` or failing per proposal. Every shipped topology gives the L1 service a batcher client, so this is reachable only by misconfiguration. `MissingBatcherClientError`, the factory's `Http` arm and both config-key consts are deleted in C1 together with the HTTP oracle. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
beb6a2a to
e3d4bec
Compare
Merge activity
|
matanl-starkware
left a comment
There was a problem hiding this comment.
@matanl-starkware made 1 comment.
Reviewable status:complete! all files reviewed, all discussions resolved (waiting on asaf-sw).
|
Security scan complete — no issues detected. Generated by Claude Code |

Builds each feed's oracle client from the source #14995 made configurable, so enabling Chainlink for a feed becomes a config rollout rather than a code deploy.
Both feeds default to
Http, so merging changes nothing at runtime — the same twoExchangeRateOracleClients are built from the same two configs as before.Also closes a transposition the previous tests could not see: crossing a feed's config with another feed's metrics bundle would have published every ETH/STRK reading under
snip35_strk_usd_*, pointing every dashboard and alert at the wrong series. The factory now has no positional argument left to cross, and every assertion is a number rather than aDebugstring.Stacked on #14995. Part of the L1 price oracle replacement split of #14942. Everything the source switch adds is deleted with the HTTP source.
Detailed Summary for AI Bots
What
new_with_oraclebuilds each feed's client from the source #14995 made configurable. Both feeds default toHttp, so merging changes nothing at runtime.The factory is generic over the feed's rate kind, and everything about the feed comes from that kind: its selected source, the HTTP config that source reads, the metrics bundle it publishes on, the Chainlink feeds and bounds it reads, and the config key named when the selection cannot be honoured.
new_with_oraclepasses no per-feed argument at all, only the whole config and the optional batcher client.The transposition that tests could not see
new_with_oracleon the base branch pairs each feed's HTTP config with that feed's metrics constant positionally, and no test constructs it at all, so crossing either pair is invisible: every ETH/STRK reading would publish undersnip35_strk_usd_*and vice versa, pointing every dashboard and alert at the wrong series.The factory in #14944 kept that shape one level down. It took the feed's source, HTTP config and source config key as positional arguments independent of the rate kind, so any two of them could still be crossed. Its HTTP cases read the rates and the series back per feed, but its Chainlink cases asserted
format!("{client:?}")substrings,starts_with("ChainlinkOracleClient")andpair: EthStrk, over byte-identical default configs, and never read a rate through a Chainlink client.This PR closes it from both sides. Structurally, a private
SourceSelectableFeedimpl per rate kind holds that feed's source, HTTP config and config key, so the factory has no argument left to transpose, andChainlinkRate::metrics()remains the only place a pair maps to a metrics bundle. Behaviourally, every assertion is now a number rather than aDebugstring:mockitoservers and a mocked batcher are wired in every case ofeach_feed_is_served_by_the_source_selected_for_it, and the four rates the two sources serve are distinct. A feed built from the other feed's source, config or rate kind resolves to a number named for that other feed.each_feed_publishes_on_its_own_metrics_seriesruns on each source and reads both rates back off a local Prometheus recorder, so a client holding the other feed's bundle fails on the series whichever source built that client.Each fix was verified by mutation: transposing the per-feed source, the per-feed HTTP config, the rate kind at a call site, the source config key, and the metrics bundle each fails a test named for it, as do hardcoding one pair's bundle where
ChainlinkOracleClient::newreadsKind::metrics()and aMissingBatcherClientErrormessage that stops naming the offending keys. Nothing in this PR asserts on aDebugstring.One deleted test, and why
#14944 had
chainlink_clients_sample_on_their_own_feeds_interval, pinning that a Chainlink client samples on its feed'sExchangeRateOracleConfig::lag_interval_seconds. #14983 moved the Chainlink cadence intoChainlinkOracleConfig::sampling_interval_seconds, which both feeds share, so the per-source cadence distinction that test protected is gone by design and the factory threads no interval at all. That also removes theNonZeroU64conversion #14944 needed, and with it therange(min = 1)that PR had to add tolag_interval_secondsto keep an operator zero from surfacing as a node panic.Misconfiguration behaviour
Selecting
Chainlinkwhere no batcher client exists fails while the components are built, rather than falling back toHttpor failing per proposal.MissingBatcherClientErrorresolves both feeds before reporting either, so one startup names every offending key: an operator who set both feeds wrong learns of both at once instead of paying 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.components.rspanics on the error, at the composition root where a config mistake should stop the process.Everything here has a deletion date
The HTTP source removal deletes
MissingBatcherClientError, the factory'sHttparm, and both*_ORACLE_SOURCE_CONFIG_KEYconsts, together with theSourceSelectableFeedselection that exists only to choose between the two sources. All are marked. The error type is worth its lines even so: during the window an operator is actively editing these keys, and without it a misconfigured node dies on anexpectinside the factory, with a message that names neither the feed that was misconfigured nor the key that misconfigured it.The two
ChainlinkRateimpls get their production callers here:ChainlinkOracleClientwas constructed only by tests until now, soKind::metrics()and the guard counters were reachable only undercfg(test).Testing
10 new cases, 99 in the crate. The four
each_feed_is_served_by_the_source_selected_for_itcases cover both feeds on each source and each feed alone on Chainlink; the twoeach_feed_publishes_on_its_own_metrics_seriescases cover the metrics wiring under each source;http_sources_do_not_need_a_batcher_clientand the threechainlink_source_without_a_batcher_client_is_rejectedcases cover the batcher requirement, the rejection cases each asserting that the rendered error names every offending config key verbatim.The Chainlink cases read through the batcher fixtures the
chainlink_oracletests already use, whose two feed answers derive to one rate for ETH/STRK and read as another for STRK/USD. Only the entry point composing them is new; thetest_utilsmodule and the three fixture consts the provider test names widened topub(crate).🤖 Generated with Claude Code