Hybrid solver cache key omits sender while on-chain discount depends on it #283

Closed
opened 2026-06-03 07:12:11 +00:00 by Brouie · 35 comments
Brouie commented 2026-06-03 07:12:11 +00:00 (Migrated from gitlab.com)

Severity: Low
Reachability: Two senders sharing the same trader value hitting the solver.
Affected: hybrid_cache_key (indexer/src/api/route_solver.rs).
Root cause: the solver's quote cache key omits sender, but the on-chain fee discount can depend on sender, so cached quotes can carry the wrong fee for a different sender.

Summary

hybrid_cache_key is keyed on solver_version | token_in | token_out | amount_bucket | max_maker_fills | trader — no sender. But maybe_simulate forwards both trader and sender to the chain sim, and the on-chain discount lookup is keyed (trader, sender) (the pair's discount cache and the registry's GetDiscount both take sender). So two different senders that pass the same trader get each other's cached quote, which may embed a different effective fee than they'd actually get.

It's a quote-accuracy bug, not a fund issue — the chain enforces the real fee at execution. Low.

Current codebase

  • route_solver.rs hybrid_cache_key: builds the key without sender; maybe_simulate passes sender through to simulation.
  • Include sender in the cache key when the simulation depends on it (or normalize so the key reflects everything the quote depends on).

Acceptance criteria

  • Two senders with the same trader but different discount eligibility get distinct, correct cached quotes.
**Severity:** Low **Reachability:** Two senders sharing the same `trader` value hitting the solver. **Affected:** `hybrid_cache_key` (`indexer/src/api/route_solver.rs`). **Root cause:** the solver's quote cache key omits `sender`, but the on-chain fee discount can depend on `sender`, so cached quotes can carry the wrong fee for a different sender. ## Summary `hybrid_cache_key` is keyed on `solver_version | token_in | token_out | amount_bucket | max_maker_fills | trader` — no `sender`. But `maybe_simulate` forwards both `trader` and `sender` to the chain sim, and the on-chain discount lookup is keyed `(trader, sender)` (the pair's discount cache and the registry's `GetDiscount` both take `sender`). So two different senders that pass the same `trader` get each other's cached quote, which may embed a different effective fee than they'd actually get. It's a quote-accuracy bug, not a fund issue — the chain enforces the real fee at execution. Low. ## Current codebase - `route_solver.rs` `hybrid_cache_key`: builds the key without `sender`; `maybe_simulate` passes `sender` through to simulation. ## Recommended direction - Include `sender` in the cache key when the simulation depends on it (or normalize so the key reflects everything the quote depends on). ## Acceptance criteria - [ ] Two senders with the same `trader` but different discount eligibility get distinct, correct cached quotes.
PlasticDigits commented 2026-06-03 10:50:45 +00:00 (Migrated from gitlab.com)

Cache key should be for the sender's discount tier, so senders with same discount tier can reuse same cache

Cache key should be for the sender's discount tier, so senders with same discount tier can reuse same cache
Brouie commented 2026-06-04 06:29:25 +00:00 (Migrated from gitlab.com)

mentioned in issue #279

mentioned in issue #279
Brouie commented 2026-06-04 06:29:26 +00:00 (Migrated from gitlab.com)

Implementation plan (your "key on the resolved discount tier" direction). Small — and it folds into #279.

  • In route_solver.rs: hybrid_cache_key is built from solver_version|token_in|token_out|amount_bucket|max_maker_fills|trader_key and never reads sender, yet maybe_simulate forwards sender and the on-chain discount branches on it. Resolve the effective tier in the caller (it has &state.pool) from traders.tier_id (already synced by trader_tracker, no extra LCD), append a single tier segment to the key; keep hybrid_cache_key pure/sync. Extend the existing hybrid_cache_key_tests (#245 precedent): same tier → equal key, different tier → distinct.

Caveats (Low, accepted-class): tier_id syncs every ~600s, so it can disagree with live discount for ~10min (needs_deregister drift) — same quote-accuracy staleness #283 already accepts (chain enforces the real fee). And the indexer doesn't track is_trusted_router, so on the router path (sender≠trader) key on BOTH tiers to avoid a wrong-fee cache collision.

Recommendation: land as a narrow interim with the tier resolved through ONE helper + a single appended key segment, so #279's solver rework can swap the source (traders.tier_id → full discount-state table) without touching the key format — OR just do it inside #279's Phase 3. Your call on standalone-now vs fold-into-#279. @PlasticDigits

Implementation plan (your "key on the resolved discount tier" direction). Small — and it folds into #279. - In `route_solver.rs`: `hybrid_cache_key` is built from `solver_version|token_in|token_out|amount_bucket|max_maker_fills|trader_key` and never reads `sender`, yet `maybe_simulate` forwards `sender` and the on-chain discount branches on it. Resolve the effective tier in the caller (it has `&state.pool`) from `traders.tier_id` (already synced by `trader_tracker`, no extra LCD), append a single tier segment to the key; keep `hybrid_cache_key` pure/sync. Extend the existing `hybrid_cache_key_tests` (#245 precedent): same tier → equal key, different tier → distinct. **Caveats (Low, accepted-class):** `tier_id` syncs every ~600s, so it can disagree with live discount for ~10min (needs_deregister drift) — same quote-accuracy staleness #283 already accepts (chain enforces the real fee). And the indexer doesn't track `is_trusted_router`, so on the router path (sender≠trader) key on BOTH tiers to avoid a wrong-fee cache collision. Recommendation: land as a narrow interim with the tier resolved through ONE helper + a single appended key segment, so #279's solver rework can swap the source (traders.tier_id → full discount-state table) without touching the key format — OR just do it inside #279's Phase 3. Your call on standalone-now vs fold-into-#279. @PlasticDigits
Brouie commented 2026-06-05 01:35:20 +00:00 (Migrated from gitlab.com)

mentioned in merge request !751

mentioned in merge request !751
Brouie commented 2026-06-05 01:35:26 +00:00 (Migrated from gitlab.com)

Shipped your "key on the discount tier" direction — MR !751.

The gap: hybrid_cache_key keyed on trader only, but maybe_simulate forwards both trader and sender to the router and the on-chain discount is the subject's tier. So two callers with trader unset and different senders on different tiers collided on the key and could be served each other's quote.

Fix: resolve the discount subject (trader if set, else sender), look up its tier from the synced traders.tier_id (no extra LCD), and fold the tier into the cache key. Same-tier callers still share the cache (what you asked for); different tiers can't collide. Kept hybrid_cache_key pure/sync — the tier lookup sits in the caller. Added hybrid_cache_key_distinguishes_discount_tier.

Scoped to the cache-key correctness; the deeper sender / is_trusted_router routing tracking I'm keeping in #279's cache phase as flagged. @PlasticDigits

Shipped your "key on the discount tier" direction — MR !751. The gap: `hybrid_cache_key` keyed on `trader` only, but `maybe_simulate` forwards both `trader` and `sender` to the router and the on-chain discount is the subject's tier. So two callers with `trader` unset and different senders on different tiers collided on the key and could be served each other's quote. Fix: resolve the discount subject (`trader` if set, else `sender`), look up its tier from the synced `traders.tier_id` (no extra LCD), and fold the tier into the cache key. Same-tier callers still share the cache (what you asked for); different tiers can't collide. Kept `hybrid_cache_key` pure/sync — the tier lookup sits in the caller. Added `hybrid_cache_key_distinguishes_discount_tier`. Scoped to the cache-key correctness; the deeper sender / is_trusted_router routing tracking I'm keeping in #279's cache phase as flagged. @PlasticDigits
PlasticDigits commented 2026-06-05 03:20:43 +00:00 (Migrated from gitlab.com)

mentioned in commit 013b51a77b

mentioned in commit 013b51a77bd9af16387e4232a2d9f67277a28f6b
Brouie commented 2026-06-05 03:46:20 +00:00 (Migrated from gitlab.com)

mentioned in merge request !761

mentioned in merge request !761
ghost1 commented 2026-06-05 04:03:29 +00:00 (Migrated from gitlab.com)

mentioned in commit faccef2512

mentioned in commit faccef2512ed21801d7798e3290580b84884330b
PlasticDigits commented 2026-06-05 04:03:59 +00:00 (Migrated from gitlab.com)

mentioned in merge request !764

mentioned in merge request !764
PlasticDigits commented 2026-06-05 04:08:16 +00:00 (Migrated from gitlab.com)

mentioned in issue #306

mentioned in issue #306
PlasticDigits commented 2026-06-05 04:14:43 +00:00 (Migrated from gitlab.com)

mentioned in commit 6420a7747d

mentioned in commit 6420a7747d6465f85e980e6e3856c3ea1d6e4cdf
PlasticDigits commented 2026-06-05 04:19:52 +00:00 (Migrated from gitlab.com)

mentioned in issue #319

mentioned in issue #319
PlasticDigits commented 2026-06-05 04:19:53 +00:00 (Migrated from gitlab.com)

marked as related to #319

marked as related to #319
Brouie commented 2026-06-05 06:14:42 +00:00 (Migrated from gitlab.com)

This one's covered on main — fix MR !751 (013b51a) and the docs cross-link MR !764 (faccef2) both landed.

Checked the acceptance criterion ("two senders with the same trader but different discount eligibility get distinct, correct cached quotes") against the code + test:

  • route_solver.rs resolves the discount subject's tier via resolve_discount_tier (no extra LCD, reads synced traders.tier_id) and folds it into hybrid_cache_key, so the key now carries (… , trader|none, discount_tier).
  • Unit test hybrid_cache_key_distinguishes_discount_tier (in the hybrid_cache_key_tests module): tier0 ≠ tier5 ≠ tier9 for the same trader, and same-tier-different-sender shares the key. Ran it: passes.

The deeper sender / is_trusted_router routing-state tracking I flagged stays out of scope here — that's tracked under #279's cache phase, and the HTTP-level cache-isolation integration test is #306. The cache-key correctness this issue is about is done.

Good to close from my side. @PlasticDigits

This one's covered on main — fix MR !751 (013b51a) and the docs cross-link MR !764 (faccef2) both landed. Checked the acceptance criterion ("two senders with the same trader but different discount eligibility get distinct, correct cached quotes") against the code + test: - route_solver.rs resolves the discount subject's tier via resolve_discount_tier (no extra LCD, reads synced traders.tier_id) and folds it into hybrid_cache_key, so the key now carries (… , trader|none, discount_tier). - Unit test hybrid_cache_key_distinguishes_discount_tier (in the hybrid_cache_key_tests module): tier0 ≠ tier5 ≠ tier9 for the same trader, and same-tier-different-sender shares the key. Ran it: passes. The deeper sender / is_trusted_router routing-state tracking I flagged stays out of scope here — that's tracked under #279's cache phase, and the HTTP-level cache-isolation integration test is #306. The cache-key correctness this issue is about is done. Good to close from my side. @PlasticDigits
Brouie commented 2026-06-05 08:23:01 +00:00 (Migrated from gitlab.com)

mentioned in issue #322

mentioned in issue #322
Brouie commented 2026-06-05 08:23:02 +00:00 (Migrated from gitlab.com)

mentioned in issue #323

mentioned in issue #323
Brouie commented 2026-06-05 08:23:04 +00:00 (Migrated from gitlab.com)

mentioned in issue #324

mentioned in issue #324
PlasticDigits commented 2026-06-05 09:49:06 +00:00 (Migrated from gitlab.com)

Verification complete — issue #283

Issue: Hybrid solver cache key omits sender while on-chain discount depends on it

Verified on branch main at 531d00e (fix landed in MR !751 a6ac683, docs cross-link MR !764 faccef2).

Acceptance criteria

Item Result How verified
Two senders with the same trader but different discount eligibility get distinct, correct cached quotes PASS (agreed scope) resolve_discount_tier resolves the discount subject (trader if set, else sender) from synced traders.tier_id and folds it into hybrid_cache_key as …|trader|none|t{tier}. Different tiers → distinct keys; same tier → shared cache (per @PlasticDigits direction on the issue). Unit test hybrid_cache_key_distinguishes_discount_tier asserts tier 0 ≠ 5 ≠ 9 and same-tier/different-sender key equality.

Automated checks

cd indexer && cargo test hybrid_cache_key --lib -- --quiet
# running 2 tests ..  ok. 2 passed

python3 scripts/check_fee_discount_tier_docs.py
# OK: 11 tiers aligned

Code / docs alignment

  • indexer/src/api/route_solver.rs: resolve_discount_tier + tier segment in hybrid_cache_key; caller resolves tier before cache lookup in execute_hybrid_route_solve.
  • docs/indexer-invariants.md, docs/integrators.md, skills/AGENTS_HYBRID_QUOTING.md, skills/AGENTS_INDEXER_HYBRID_BEST_EXECUTION.md — all document the discount_tier cache-key segment (#283).

Follow-up (out of scope for this issue, already tracked)

Trusted-router path where sender ≠ trader and on-chain GetDiscount branches on is_trusted_router remains in #279 cache phase; HTTP-level cache-isolation integration test tracked in #306. Accepted per implementer notes on this issue.

Closing — cache-key correctness for tier-based isolation is implemented, tested, and documented. No repo changes from this verification pass.

## Verification complete — issue #283 **Issue:** [Hybrid solver cache key omits sender while on-chain discount depends on it](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/283) Verified on branch `main` at `531d00e` (fix landed in MR !751 `a6ac683`, docs cross-link MR !764 `faccef2`). ### Acceptance criteria | Item | Result | How verified | |------|--------|--------------| | Two senders with the same `trader` but different discount eligibility get distinct, correct cached quotes | **PASS** (agreed scope) | `resolve_discount_tier` resolves the discount subject (`trader` if set, else `sender`) from synced `traders.tier_id` and folds it into `hybrid_cache_key` as `…\|trader\|none\|t{tier}`. Different tiers → distinct keys; same tier → shared cache (per @PlasticDigits direction on the issue). Unit test `hybrid_cache_key_distinguishes_discount_tier` asserts tier 0 ≠ 5 ≠ 9 and same-tier/different-sender key equality. | ### Automated checks ```bash cd indexer && cargo test hybrid_cache_key --lib -- --quiet # running 2 tests .. ok. 2 passed python3 scripts/check_fee_discount_tier_docs.py # OK: 11 tiers aligned ``` ### Code / docs alignment - `indexer/src/api/route_solver.rs`: `resolve_discount_tier` + tier segment in `hybrid_cache_key`; caller resolves tier before cache lookup in `execute_hybrid_route_solve`. - `docs/indexer-invariants.md`, `docs/integrators.md`, `skills/AGENTS_HYBRID_QUOTING.md`, `skills/AGENTS_INDEXER_HYBRID_BEST_EXECUTION.md` — all document the `discount_tier` cache-key segment ([#283](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/283)). ### Follow-up (out of scope for this issue, already tracked) Trusted-router path where `sender ≠ trader` and on-chain `GetDiscount` branches on `is_trusted_router` remains in [#279](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/279) cache phase; HTTP-level cache-isolation integration test tracked in [#306](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/306). Accepted per implementer notes on this issue. **Closing** — cache-key correctness for tier-based isolation is implemented, tested, and documented. No repo changes from this verification pass.
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-06-05 09:49:10 +00:00
PlasticDigits commented 2026-06-05 11:09:55 +00:00 (Migrated from gitlab.com)

mentioned in merge request !796

mentioned in merge request !796
ghost1 commented 2026-06-05 11:11:49 +00:00 (Migrated from gitlab.com)

mentioned in commit 662cab6523

mentioned in commit 662cab65231b9c8f767f2e2a273180ab2db9c3f3
PlasticDigits commented 2026-06-05 11:12:21 +00:00 (Migrated from gitlab.com)

mentioned in merge request !797

mentioned in merge request !797
PlasticDigits commented 2026-06-05 11:22:17 +00:00 (Migrated from gitlab.com)

mentioned in merge request !798

mentioned in merge request !798
PlasticDigits commented 2026-06-05 13:47:17 +00:00 (Migrated from gitlab.com)

mentioned in merge request !809

mentioned in merge request !809
PlasticDigits commented 2026-06-05 13:56:15 +00:00 (Migrated from gitlab.com)

mentioned in issue #335

mentioned in issue #335
ghost1 commented 2026-06-05 14:07:49 +00:00 (Migrated from gitlab.com)

mentioned in commit ebb64ecb25

mentioned in commit ebb64ecb25e8a8f54339dfb9de9a62079b1125ac
ghost1 commented 2026-06-05 14:08:24 +00:00 (Migrated from gitlab.com)

mentioned in merge request !818

mentioned in merge request !818
PlasticDigits commented 2026-06-08 08:43:13 +00:00 (Migrated from gitlab.com)

mentioned in commit 8c8b629ee6

mentioned in commit 8c8b629ee6042d374f19d2efd60f57216d97364e
PlasticDigits commented 2026-06-08 13:42:27 +00:00 (Migrated from gitlab.com)

mentioned in commit 8bc8886287

mentioned in commit 8bc8886287d9b6010dc43d724b4035d7bf179cc7
PlasticDigits commented 2026-06-12 05:05:45 +00:00 (Migrated from gitlab.com)

mentioned in issue #364

mentioned in issue #364
PlasticDigits commented 2026-06-12 05:26:36 +00:00 (Migrated from gitlab.com)

mentioned in merge request !876

mentioned in merge request !876
PlasticDigits commented 2026-06-13 07:09:04 +00:00 (Migrated from gitlab.com)

mentioned in issue #376

mentioned in issue #376
PlasticDigits commented 2026-06-13 07:56:27 +00:00 (Migrated from gitlab.com)

mentioned in issue #379

mentioned in issue #379
PlasticDigits commented 2026-07-12 07:14:42 +00:00 (Migrated from gitlab.com)

mentioned in issue #477

mentioned in issue #477
PlasticDigits commented 2026-07-13 10:33:12 +00:00 (Migrated from gitlab.com)

mentioned in issue #485

mentioned in issue #485
PlasticDigits commented 2026-08-24 00:35:17 +00:00 (Migrated from gitlab.com)

mentioned in issue #615

mentioned in issue #615
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-dex-terraclassic#283
No description provided.