Hybrid solver does serial LCD fanout and its result cache is caller-bypassable #279
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:implement
agent:implement
agent:open_issues
agent:ready
agent:research
agent:security_audit
agent:verify
architecture
backend
blocker:hybrid
blocker:launch
blocker:limit-orders
blocker:v2
block:log_only
block:security
bug
ci
contracts
correctness
deploy
dev
devops
docs
documentation
duplicate
e2e
enhancement
epic
feature
frontend
functional-completion
gas
good first issue
governance
help wanted
high-risk
hooks
hybrid
indexer
infra
infrastructure
integrators
invalid
launch-blocker
limit-orders
localnet
localterra
low priority
missing-implementation
needs-design
ops
performance
priority
high
priority
medium
product
qa
QA
question
ready
ready
research
scripts
security
security-hardening
smartcontracts
tech-debt
testing
ux
UX
v2
verification
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-dex-terraclassic#279
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: Medium-High (performance + request amplification)
Reachability:
/route/solveand/route/solve/best— unauthenticated, though they do sit behind the lcd-heavy per-IP limit.Affected:
solve_global_best_execution(indexer/src/api/best_execution.rs) and the hybrid result cache (indexer/src/api/route_solver.rs).Root cause: the solver evaluates every candidate path with serial LCD calls, and the result cache key varies on caller-controlled inputs, so the cache is easy to miss on purpose.
Summary
This is the "snappy solver" item. Two things make the solver slow and cheap to amplify:
Serial fanout.
solve_global_best_executionloops over every candidate path and, inside the loop,awaitsoptimize_multihop_hybrid_joint(a per-hop LCD grid search) and thenmaybe_simulate(a router LCD sim) one path at a time. So a single request's latency is the sum of every candidate's LCD round-trips — hundreds of serial calls on a busy graph. Nothing here runs concurrently.Bypassable cache.
hybrid_cache_keyis built fromsolver_version | token_in | token_out | amount_bucket | max_maker_fills | trader.max_maker_fills, the amount bucket, andtraderare all caller-set, so bumpingmax_maker_fills1→2→3… or hopping amount buckets misses the cache every time and forces a fresh full fanout. The cache barely protects the hot path.The per-IP lcd-heavy limit caps request rate, but not per-request cost, and combined with the proxy-IP issue (separate report) the limit may not even bind. The fix is mostly performance work that also closes the amplification.
Current codebase
best_execution.rssolve_global_best_execution:for cand in &candidates { ... optimize_multihop_hybrid_joint(...).await ...; maybe_simulate(...).await ... }— serial, per candidate.route_solver.rshybrid_cache_key: includesmax_maker_fillsandtrader; both caller-controlled.Recommended direction
join_allwith a sane concurrency cap) instead of serially.max_maker_fillsfrom the key (or clamp it to a few discrete values), and bucket/normalizetraderso honest variation still hits.Acceptance criteria
max_maker_fills/ amount within normal ranges hits the cache instead of forcing fresh fanouts.Test plan (performance / abuse)
We must the orderbook and v2 pair data in postgres instead of relying on lcd calls for order simulation. There must be 0 lcd calls in the solver.
The indexer needs to simulate and optimize without any lcd calls.
Once that change is made, then can add concurrency, limit hops to 4, and fix the caching.
mentioned in issue #285
mentioned in merge request !744
Implementation plan (your "0-LCD solver" direction). Large — the architectural keystone; phase it.
Phase 1 (must land first): mirror state into Postgres so the solver does 0 LCD calls. Two NEW sqlx migrations:
pair_reserves— brand new; reserves aren't event-sourced today, they only live on-chain behind{pool:{}}. Mirror RESERVES + fee_bps per pair (+ snapshot_at, height).resting_limit_orders— a current-state materialized book, distinct from the append-onlylimit_order_placements/_cancellations/_fillslogs (those lack currentremaining+ FIFO prev/next the walk needs).revert/*.down.sql. No backfill — a background snapshot loop populates both.New modules: db queries (pair_reserves, resting_orders), a
book_snapshotloop (copyoracle.rs::run_oracle_loop/trader_tracker::run_tier_sync_loop), anddb_orderbook_sim(port the pool+book math — reuseorderbook_sim.rs/hybrid_orderbook_sim.rsalready built for CG/CMC #220). Rewirehybrid_route_opt(grid search → DB) +best_execution(serial loop → join_all).Phase 2: bump GET_DEFAULT_MAX_HOPS 3→4. Phase 3: cache-key fix — #283 folds in here, don't ship it separately.
Top risk: fidelity drift — if
db_orderbook_simdiverges from on-chainHybridSimulation(rounding, dust-flush, expiry, tier discount) the solver quotes wrong; keep the finalmaybe_simulateLCD validation on the single winning route. Snapshot staleness → tight interval + treat missing/old snapshot as degrade-not-error.quote_kindenum rename (LCD→indexed) is a BREAKING API change → coordinate the frontend. Re-check the 4-hop bump against #286's path-candidate budget. Strong precedent on all three halves (CG/CMC sim, the snapshot-loop pattern, the synced tier intraders.tier_id). @PlasticDigitsmentioned in issue #283
mentioned in merge request !751
Touching this for status. It's the keystone — two new PG migrations (pair_reserves + resting_limit_orders), and the quote_kind rename is breaking into the frontend; #283 folds into its cache phase (I shipped the standalone cache-key correctness part as MR !751). I'm deliberately not landing a breaking, migration-heavy 0-LCD solver rewrite in one shot — it wants its own focused pass off the phase plan in my earlier note. Ready to take Phase 1 (mirror reserves + resting orders into Postgres, solver does 0 LCD) as a dedicated piece whenever you want to sequence it. @PlasticDigits
Started it — Phase 1a is up (the schema foundation), MR above. Two new tables + their query layer, fully tested, and deliberately NOT wired into the solver yet so there's zero fidelity risk and nothing breaking:
Next, as separate pieces because they need live fidelity validation: 1b the book_snapshot loop (populate from chain, modeled on the oracle/tier-sync loops), 1c db_orderbook_sim + rewire hybrid_route_opt/best_execution to read DB not LCD — keeping the final maybe_simulate LCD validation on the winning route as the drift guard. The breaking quote_kind rename rides with 1c, so we coordinate the frontend at that point. @PlasticDigits
mentioned in merge request !761
As the scope has expanded, please take all pieces into seperate issues and close this parent issue
mentioned in merge request !764
mentioned in issue #306
mentioned in issue #319
marked as related to #319
mentioned in commit
8ea4bc1420mentioned in issue #322
mentioned in issue #323
mentioned in issue #324
Split into separate issues per your note. Mapping so nothing's lost — the parent's three acceptance items all carry over to the children:
0-LCD hybrid solver program:
8ea4bc1).Sequencing matches your direction (0-LCD first, then concurrency / 4 hops / caching): 1a done -> 1b #322 -> 1c #319 -> 2 #323 + 3 #324. Each child carries the code anchors, acceptance criteria, and the fidelity/freshness guards.
Closing this parent. @PlasticDigits
mentioned in merge request !787
mentioned in merge request !799
mentioned in merge request !809
mentioned in issue #335
mentioned in issue #684