Skip to content

fix: key the pricing quote cache by (chain_id, symbol) (RAI-2130) - #68

Open
hardyjosh wants to merge 1 commit into
feat/rai-1991-context-v7-chain-idfrom
fix/rai-2130-chain-scoped-quote-cache
Open

fix: key the pricing quote cache by (chain_id, symbol) (RAI-2130)#68
hardyjosh wants to merge 1 commit into
feat/rai-1991-context-v7-chain-idfrom
fix/rai-2130-chain-scoped-quote-cache

Conversation

@hardyjosh

@hardyjosh hardyjosh commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Closes RAI-2130.

The hazard

src/pricing_client.rs cached pricing quotes in a HashMap<Symbol, Quote>, inserting by bare symbol and looking up by bare symbol — discarding the chain_id that every PriceFrame already carries.

st0x.pricing is about to publish one frame per (chain, symbol). Under a symbol-only key, two frames for the same symbol on different chains are last-write-wins across chains: whichever landed most recently occupies the single slot, and this server would sign an oracle context against another chain's rate.

That failure is silent. Every frame involved is individually fresh, in-expiry and correctly signed, so no staleness check, no expiry check and no signature check anywhere downstream can see the difference. The only observable symptom would be a mispriced fill.

The change

  • The cache is keyed by (u64 chain_id, Symbol) — a QuoteCache type alias, since the type appears in four signatures.
  • Ingest stores each Price frame under the frame's own chain_id. Frames for other chains are retained, not dropped.
  • Reads (latest, snapshot_many, missing, newest_source_ts_ms) are all scoped to this server's configured chain, so other chains' frames are never served. A symbol cached only for another chain reads as absent — /status reports it under missing_symbols.
  • Halt eviction is chain-scoped too: a halt on another chain must not fail-closed the quote this deployment serves.
  • newest_source_ts_ms excludes other chains, so a fresh frame we would never serve cannot mask a frozen feed on the chain we do.

The configured chain comes from config.chain_id (default 8453), threaded through LiveClientConfig::new as a required argument rather than a builder option — silently defaulting the serving chain is the exact class of bug being fixed here.

No change to the signed-context schema or the wire format. st0x-pricing-types is unchanged — chain_id is already on the frame.

Stacked on #65

This builds on #65 (feat/rai-1991-context-v7-chain-id), which introduces config.chain_id. That field is reused as-is; #65's commit is untouched. Review/merge #65 first.

Tests

Three new unit tests in src/pricing_client.rs:

  1. frames_for_one_symbol_on_two_chains_do_not_clobber_each_other — both frames survive ingest, one entry per (chain, symbol).
  2. reads_serve_the_configured_chain_not_the_latest_frame — the chain-1 frame arrives last and would win under a symbol-only key; latest, snapshot_many and missing all serve/report the configured chain instead.
  3. halt_on_another_chain_does_not_evict_our_quote — halt eviction stays on its own chain.

Verified these are discriminating, not just compile-time-new: re-keying the cache to a constant chain id (reproducing the old symbol-only collision) fails all three, with test 2 returning the last-arrived chain's quote.

Local checks

Both mirror CI, run in the nix shell:

  • nix develop -c cargo test99 passed, 0 failed (66 lib incl. the 3 new, 29 integration, 1 KMS smoke, 3 prod smoke).
  • nix develop -c bash -c 'cargo fmt --all -- --check'clean.

Clippy caveat: cargo clippy could not be run locally. It fails on this machine with error[E0463]: can't find crate for num_enum_derive, inside the third-party num_enum-0.7.6 dependency. This reproduces on unmodified main with no diff applied — it is a local nix/proc-macro environment problem, not something this branch introduces. CI is the authority on oracle-rs-static.

🤖 Generated with Claude Code

@linear-code

linear-code Bot commented Aug 25, 2026

Copy link
Copy Markdown

RAI-2130

Copy link
Copy Markdown
Contributor Author

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@hardyjosh
hardyjosh force-pushed the fix/rai-2130-chain-scoped-quote-cache branch from d8ada81 to b9cb2be Compare August 26, 2026 16:54
st0x.pricing is about to publish one frame per (chain, symbol). Keyed by
bare symbol, two chains' frames for the same asset were last-write-wins,
so this server could sign an oracle context against another chain's rate.
Nothing downstream catches it: every frame involved is individually
fresh, in-expiry and correctly signed.

Ingest now stores each frame under its own chain_id and halts evict only
their own chain's entry; reads (latest, snapshot_many, missing,
newest_source_ts_ms) are all scoped to config.chain_id, so other chains'
frames are retained but never served.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hardyjosh
hardyjosh force-pushed the feat/rai-1991-context-v7-chain-id branch from 1b61e1e to 44a1741 Compare August 26, 2026 16:55
@hardyjosh
hardyjosh force-pushed the fix/rai-2130-chain-scoped-quote-cache branch from b9cb2be to 8c9c845 Compare August 26, 2026 16:55
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.

1 participant