Phase 2 — raise GET_DEFAULT_MAX_HOPS 3→4 for hybrid GET best-execution #323
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#323
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?
Summary
Bump
GET_DEFAULT_MAX_HOPSfrom 3 to 4 so the hybrid GET best-execution solver discovers and prices 4-hop routes, matching the pool-only escape hatch (GET_POOL_ONLY_MAX_HOPS = 4) and the on-chain router cap (MAX_HOPS = 4). Right now the default hybrid GET path is one hop short of what the router can actually execute, so any token pair whose only viable route is 4 hops is reachable viapool_only=truebut never gets a hybrid (book + pool) split quote.This is Phase 2 of the #279 0-LCD hybrid-solver program. It is deliberately gated behind Phase 1c (#319) so the 4th hop is priced from the DB mirror, not LCD: the per-hop simulation cost is the whole reason the default cap was held at 3, and that cost goes away once optimization reads
db_orderbook_siminstead of issuing liveHybridSimulationLCD calls.Current codebase
Hop caps live in
indexer/src/api/route_solver.rs:GET_DEFAULT_MAX_HOPS: usize = 3(route_solver.rs:31) — the default hybrid-aware GET cap (ADR 0001 / #191).GET_POOL_ONLY_MAX_HOPS: usize = 4(route_solver.rs:33) — legacy pool-only GET (pool_only=true/hybrid_optimize=false), selected byget_pool_only_max_hops(route_solver.rs:374) and fed intoresolve_route_with_max_hopsat the GET handler (route_solver.rs:704-706).MAX_HOPS: usize = 4insmartcontracts/contracts/router/src/contract.rs:21, enforced atcontract.rs:187,:476,:554. So 4 hops is already the executable ceiling; the indexer default is the only thing capped below it.The hybrid GET solver consumes the cap in exactly one place:
solve_global_best_execution(indexer/src/api/best_execution.rs:115) callsenumerate_path_candidates(&state.pool, token_in, token_out, GET_DEFAULT_MAX_HOPS)(best_execution.rs:125), which handsmax_hopstoroute_paths::find_paths_top_k(start, goal, &pair_rows, max_hops, MAX_PATH_CANDIDATES)(best_execution.rs:94-95).MAX_PATH_CANDIDATES = 5(best_execution.rs:18).best_execution.rs:130-202) runs serially over up to 5 candidates; each candidate awaitsoptimize_multihop_hybrid_joint(best_execution.rs:142) thenmaybe_simulate(best_execution.rs:159). LCD accounting per candidate isestimate_lcd_calls(hop_count)(best_execution.rs:222-226),= hop_count * (17 + 2*17)— i.e. linear in hop count.Budget constant that must move with the cap:
LCD_HYBRID_SIM_BUDGET = MAX_PATH_CANDIDATES * GET_DEFAULT_MAX_HOPS * (17 + 2 * 2 * 17)(best_execution.rs:26-27) — the documented worst-case pair-levelHybridSimulationupper bound. Today that is5 * 3 * 85 = 1275. Because it referencesGET_DEFAULT_MAX_HOPSdirectly, bumping the constant re-derives it to5 * 4 * 85 = 1700automatically — but the per-request cost grows ~33% and that needs to be acknowledged and bounded, not silently accepted. Thebudget_tests::lcd_budget_is_documented_constanttest (best_execution.rs:33-36) only asserts> 0, so it will not catch a regression in the magnitude.Path enumeration is already hardened against the extra hop (#286):
find_paths_top_k/find_paths_top_k_instrumented(indexer/src/api/route_paths.rs:74,:87) gate the DFS with ahop_distance_to_goalreachability map (route_paths.rs:48-67) so the search is O(V+E) and never expands an unreachable subtree — seeunreachable_goal_does_not_explodestyle tests (route_paths.rs:362-409). The instrumented expansion count is what proves the 4th hop does not blow up enumeration on a dense graph.Pool-only GET and POST already operate at 4 hops (
route_solver.rs:363,:737), so this change brings the default hybrid GET path into line with the rest of the surface.Why this is needed
pool_only=true— and pool-only throws away the book legs, so even then the quote is worse than what the chain can do.db_orderbook_sim). Once that lands, the 4th hop is essentially free per request, which removes the original reason the cap was pinned at 3. Plastic's call on #279: once the DB-priced change is in, then limit hops to 4.Constraints and guardrails
estimate_lcd_calls× up to 5 candidates), which is exactly the LCD load the bucketed cache was built to avoid. Don't ship this on LCD pricing.MAX_HOPS = 4; a 5-hop indexer route would be un-executable and would just burn enumeration + sim budget.GET_DEFAULT_MAX_HOPSandGET_POOL_ONLY_MAX_HOPSas distinct named constants even though they become equal — they carry different intent (default hybrid vs legacy pool-only escape hatch) and a future change may diverge them again. Do not collapse them into one symbol.hybrid_cache_key(route_solver.rs:532) keys onsolver_version | token_in | token_out | amount_bucket | max_maker_fills | trader | discount_tier; hop count is not and should not be a key component (the route is derived deterministically from token_in/out + the pair graph). Discount-tier keying is already correct per #283 (MR !751) — leave it alone.MAX_PATH_CANDIDATESstays at 5. This issue changes hop depth only, not candidate breadth — candidate-budget work is #286's scope.Recommended direction
GET_DEFAULT_MAX_HOPSto4atroute_solver.rs:31and update its doc comment + the module-level "max 3 hops" prose atroute_solver.rs:3.LCD_HYBRID_SIM_BUDGET(best_execution.rs:26-27). Since it now prices against the DB mirror rather than live LCD, also reconsider whether the constant's name/comment still reads as "LCD" worst-case or should be reframed as the DB-sim worst-case bound that #319 introduces. Either way, state the new numeric bound (5 * 4 * 85 = 1700) explicitly in the comment so reviewers see the per-request cost moved.enumerate_path_candidatesandfind_paths_top_kneed no signature change — they already takemax_hopsas a parameter, so the bump propagates from the single constant. Verify nothing else inbest_execution.rshard-codes3.get_pool_only_max_hops,route_solver.rs:374) — now numerically identical to the default, which is the intended convergence.Acceptance criteria
GET_DEFAULT_MAX_HOPS = 4inroute_solver.rs; module doc and theroute_solver.rs:3"max 3 hops" comment updated to 4.amount_in=…— not just viapool_only=true.LCD_HYBRID_SIM_BUDGETre-derived to the new bound and its doc comment updated to state the new magnitude and that pricing is DB-backed post-#319; the budget test asserts the actual expected value, not merely> 0.find_paths_top_kreachability/enumeration behavior atmax_hops = 4is unchanged in spirit: an unreachable goal still produces zero expansions (no O(branching^4) blow-up), proven via the instrumented expansion count (#286 guarantees preserved).hybrid_cache_keyshape or to discount-tier keying (#283 stays intact).api_route_solve.rs,route_paths.rsunit tests).Test plan
indexer/src/api/route_paths.rstests): add a 4-hop-only case modeled ondeep_route_within_budget_is_found(route_paths.rs:490-500) — a linear0-1-2-3-4graph with no shortcut, assertfind_paths_top_k(0, 4, &pairs, 4, 5)returns one 4-edge path, and thatmax_hops = 3excludes it. Add an instrumented unreachable-goal assertion atmax_hops = 4to confirm bounded expansions.best_execution.rsbudget_tests): assertLCD_HYBRID_SIM_BUDGET == 5 * 4 * 85after the bump so the magnitude is pinned and a future cap change forces a deliberate update.indexer/tests/api_route_solve.rs): extend the existing route-solve coverage (the"3 hops" || "4 hops"assertion atapi_route_solve.rs:73already anticipates the 4-hop message) with a default-GET case over a seeded 4-hop graph asserting a quote is returned rather thanno route.cargo test -p indexer route(or the indexer test target) green; no LCD calls required for the enumeration/budget tests since path discovery is graph-only.Related
db_orderbook_sim+ rewire optimizer to read DB not LCD) — https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/issues/319 — this must land first so the 4th hop is DB-priced.mentioned in issue #279
mentioned in issue #319
marked as related to #319
mentioned in commit
0bc6390cecmentioned in merge request !796
Implementation complete — MR !796.
What changed
GET_DEFAULT_MAX_HOPSraised 3 → 4 (route_solver.rs)LCD_HYBRID_SIM_BUDGETnow 1700 (5×4×85); unit test pins exact valueseed_route_solve_4hop)check_route_solver_docs.pyupdatedAcceptance results
GET_DEFAULT_MAX_HOPS = 4+ docsfour_hop_only_route_within_budget_is_found;route_solve_get_default_hybrid_four_hopsadded — needs PostgresLCD_HYBRID_SIM_BUDGET = 1700+ pinned testcargo test --lib lcd_budget_is_documented_constantunreachable_goal_at_four_hops_does_zero_enumerationcargo test --lib route_paths::tests; no Postgres on agent VMBlocker for merge
#319 (Phase 1c /
db_orderbook_sim) is not onmainyet. Per issue constraints, merge !796 only after #319 lands so the 4th hop is DB-priced rather than LCD-backed.Issue left open pending #319 + CI/integration verification on Postgres.
mentioned in commit
7f2f716ac3mentioned in commit
e0f9e00ca2mentioned in commit
0583223a6amentioned in merge request !797
mentioned in merge request !798
mentioned in merge request !808
mentioned in commit
666d7f4c4bVerification complete — #323
Implementation from !796 is on
main. Dependency #319 (DB mirror pricing) is also merged.Acceptance results
GET_DEFAULT_MAX_HOPS = 4+ module docsroute_solver.rs:31= 4; module doc line 3 updatedcargo test --test api_route_solve route_solve_get_default_hybrid_four_hopsLCD_HYBRID_SIM_BUDGET = 1700+ pinned testcargo test --lib lcd_budget_is_documented_constantmax_hops = 4four_hop_only_route_within_budget_is_found,unreachable_goal_at_four_hops_does_zero_enumerationhybrid_cache_keychangecargo test --test api_route_solve(23/23),cargo test --lib route_paths(12/12)python3 scripts/check_route_solver_docs.py— was failing onSOLVER_VERSIONrename from #319Docs drift fix
Verification found stale 3-hop references and a broken drift guard (
SOLVER_VERSION→SOLVER_VERSION_LCD/SOLVER_VERSION_DB). Fixed in !808.Issue left open pending MR !808 merge.
mentioned in commit
e41f61bb11#323 verified — checked on current main (the merged #323 commits
0bc6390+ the666d7f4doc cleanup). Pure indexer route-solver, so this is source + unit + the Postgres-backed integration tests; no chain redeploy needed. I ran the integration case the cloud agent had to skip (no Postgres on its side), and it's green.Acceptance criteria, each mapped to what actually ran:
= 4; the module doc (route_solver.rs:3-6) now reads "max 4 hops" (the old "max 3 hops" prose is gone), and docs/route-solver.md + ADR 0002 carry the 3->4 note.0bc6390touches zero cache-key lines (checked the diff); #283 tier isolation still green (route_solve_get_cache_tier_isolation).All six hold. @PlasticDigits — over to you for the verify-agent + close. (Phase map for #279: 1a done, 1b #322, 1c #319 merged, 2 = this, 3 = #324.)
Verification complete — #323
Independent re-verification on branch
main(2026-06-06). Implementation from !796 + doc cleanup !808. Dependency #319 (DB mirror pricing) is merged onmain.Acceptance results
GET_DEFAULT_MAX_HOPS = 4+ module docsroute_solver.rs:31= 4; module doc lines 3–6 read "max 4 hops"cargo test --test api_route_solve route_solve_get_default_hybrid_four_hops— quote via defaultamount_inGET (notpool_only), 4 hops with hybrid opsLCD_HYBRID_SIM_BUDGET = 1700+ pinned testbest_execution.rs:36-41documents DB-mirror pricing post-#319 and5 × 4 × 85 = 1700;cargo test --lib lcd_budget_is_documented_constantasserts== 5*4*85max_hops = 4cargo test --lib route_paths::tests—four_hop_only_route_within_budget_is_found,unreachable_goal_at_four_hops_does_zero_enumeration(0 expansions)hybrid_cache_keychange / #283 intactcargo test --lib hybrid_cache_key(4/4); key shapesolver_version|token_in|token_out|amount_bucket|mmf|d{tier}unchangedcargo test --test api_route_solve(23/23),cargo test --test api_route_solve_db_hybrid(3/3),cargo test --lib route_paths::tests(12/12)python3 scripts/check_route_solver_docs.py— OKCommands run
All six acceptance criteria hold. Closing as verified.
mentioned in commit
88b50f1519mentioned in commit
17b9bba754mentioned in issue #485
mentioned in issue #690