Indexer: fix price-window has_more at band floor (in-band slice complete, book continues outside band) #270
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#270
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
Fix
has_moresemantics onGET /api/v1/pairs/{addr}/limit-bookwhenprice_from+price_toare set (price-window mode, GitLab #267). The returnedorders[]slice is correct; the bug is thathas_more: trueis emitted when the walk stops at the band floor even though all in-band orders fit in one page and only out-of-band book tail remains below (bids) or above (asks).Parent feature: #267 (closed). Consumers: #268 (ladder placement uses a single price-window fetch with
limit=100), any integrator paginating onhas_more.Current codebase
indexer/src/api/limit_book_lcd.rsfetch_limit_book_page(~L108–L182): paginated head→tail walk;has_more = current.is_some()is correct because every visited node is returned.order_in_price_window(~L399–L436): classifies each row as in-band (Some(true)), above-band skip (Some(false)), or past band (None).fetch_limit_book_price_window(~L438–L538): walks the chain, collects in-band rows, stops onpast_bandor pagecap. Today it setshas_more = current.is_some()(~L531) — same rule as full-book pagination, which conflates “more chain nodes exist” with “more in-band rows remain for this window.”indexer/src/api/pairs.rs—get_pair_limit_book(~L1069–L1106) routesprice_from/price_totofetch_limit_book_price_window.indexer/tests/api_limit_book_insert_hints.rs— price-window HTTP case (~L268–L279) uses mock book 1@2.0 → 2@1.5 → 3@1.5 → 4@1.0 (tail at band floor, no nodes below band). Asserts!has_more— passes today but does not cover the false-positive path.frontend-dapp/src/hooks/useLimitLadderPlacementPlan.ts— singlegetPairLimitBookPagewith price window (limit: 100); usesordersonly, nothas_moretoday, but spec-compliant clients and future pagination will.docs/integrators.md§ Insert hints & price window,docs/limit-orders.md,docs/adr/0002-limit-book-surfacing.md.Bug mechanism (two exit paths)
past_bandbreak: On first below-band (bid) / above-band (ask) row, the loop setspast_band = trueandbreaks beforecurrent = next.currentstill holds that out-of-band order id →current.is_some()→has_more: trueeven though the in-band slice is complete.Page cap at band floor: If
orders.len() == capon the last in-band row, thewhile orders.len() < cap && !past_bandexits without fetching the next node.currentpoints at the first out-of-band successor →has_more: truewithout any in-band rows left.Example (bids): chain
2.0 → 1.5 → 1.5 → 1.0 → 0.5, window[1.5, 1.0],limit=10. Returns orders 2,3,4 correctly; todayhas_more: truebecause id 5 @ 0.5 remains on-chain (should befalse).Why this is needed
has_moreis the contract for “fetch another page withafter_order_id.” A false positive forces spurious LCD-heavy pages, confuses integrators, and mirrors the pagination-gap hazard class from #267 (clients may treat trailing book depth as unfetched in-band depth).has_morebreaks trust in indexer completeness signals even whenordersare complete.Constraints / guardrails
LIMIT_BOOK_LCD_QUERY_BUDGET, or LCD query counting.has_morefor price window means: “another page may return additional in-band orders for the sameprice_from/price_to.” Not “the FIFO chain continues outside the band.”next_after_order_idwhenhas_more: truemust remain the keyset cursor for the next in-band page (last returned order id), unchanged from #267.fetch_limit_book_page(no price window) behavior unchanged.insert-hintsresolver is out of scope (separatepagination_gapsemantics).integrators.mdif behavior is clarified for price-window mode.Relevant files
indexer/src/api/limit_book_lcd.rs(fetch_limit_book_price_window,order_in_price_window)indexer/src/api/pairs.rsindexer/tests/api_limit_book_insert_hints.rs(extend) and/orindexer/tests/api_limit_book_lcd_mock.rsdocs/integrators.md,docs/limit-orders.md(price-windowhas_moredefinition)frontend-dapponly if adding defensive handling (not required for indexer fix)Recommended direction
fetch_limit_book_price_window:StoppedBecause::PastBand | PageFull | ChainEnd | Budget(enum or booleans).has_morerules (price window only):PastBandorChainEnd→has_more: false(in-band slice complete for this walk).PageFull→has_more: trueonly if the walk stopped solely due tocapand the next unprocessed node could still be in-band (still above-band skips on bids are not “more pages of results”).PageFullon the last in-band row and the successor is already known to be out-of-band (peek one row or classify without returning it), sethas_more: false.has_more = current.is_some() && !past_bandplus handle cap-at-floor by either one-step lookahead after fillingcapor continuing the loop untilpast_bandorcurrent.is_none()without pushing (classification-only), staying within budget.ordersunchanged andhas_more: false.has_more: trueand correctnext_after_order_id.Acceptance criteria
has_more: falsewhen the full in-band slice fits one page and the walk reaches the first out-of-band row (bid belowprice_to, ask aboveprice_to).has_more: falsewhen the in-band slice ends at chain tail inside the band.has_more: trueonly when more in-band orders exist beyondlimitfor the same band (second page returns remaining in-band rows only).orders[], prices, and ordering unchanged vs current behavior (regression on existing #267 tests).limit-bookwithoutprice_from/price_tounchanged.integrators.mdstates price-windowhas_moremeans “more in-band rows,” not “more book outside the band.”Test plan — all paths
limit≥ in-band counthas_more: falselimit≥ in-band countprice_to; no below-band nodeshas_more: false(existing mock)orders: [],has_more: falsewhen past band or chain endlimithas_more: true, validnext_after_order_id; page 2 withafter_order_id: rest of in-band only, thenhas_more: falselimitequals in-band count; next node out-of-bandhas_more: false(cap-at-floor regression)after_order_idmid-band continuationTest plan — attack / abuse / hack vectors
has_moreand hammersafter_order_idafter band completehas_morealonehas_moreas “band incomplete”ordersalready completelimit-bookwindow requests per integratorlimitrows per page;has_moretrue until band exhausted; no duplicate order ids across pagesVerification criteria
cargo test -p indexer api_limit_book_insert_hints(or newprice_window_has_moretest) green, including new below-band tail fixtures (bid + ask).insert_hints_and_price_window_httpandapi_limit_book_lcd_mock/api_limit_book_deepstill pass.cargo clippy -p indexer --all-targets -- -D warningsclean.curlagainst mock/local: window[1.5,1.0]on book with 0.5 tail →has_more: false, three orders.integrators.mdupdated with one sentence on price-windowhas_moresemantics.mentioned in merge request !735
Fixed #270. Branch qa/270-price-window-has-more off main, commit
8e0a53e, MR !735. Indexer-only, no wasm/contract change.Root cause confirmed in fetch_limit_book_price_window: it used has_more = current.is_some() (the full-book pagination rule), so on a price window it reported has_more=true whenever the chain continued past the band — even when the whole in-band slice already fit one page. The two exit paths in the issue both hit it: the past_band break leaves current pointing at the first out-of-band node, and a page-cap at the band floor leaves current pointing at the out-of-band successor.
Fix (price window only; fetch_limit_book_page untouched):
next_after_order_id stays the last returned in-band id; orders[], band rules, the budget constant, and query counting are unchanged.
Acceptance criteria:
Verification criteria:
clippy -p cl8y-dex-indexer --all-targets -- -D warnings— same host-toolchain drift I flagged on #267 (pre-existing lints in orderbook_sim.rs/oracle.rs/parser.rs: is_multiple_of, dead code, matches!). None are mine. Still worth that crate-wide clippy cleanup issue.Attack/abuse vectors:
One behavior note: on a page-full stop the peek validates the successor, so a broken/wrong-side immediate successor now surfaces its corrupt-book 400 one page earlier than the full-book route did. Same integrity check the walk already runs on every node; a healthy book never hits it. Documented in the fn doc-comment.
Ran the diff through an adversarial multi-agent review before pushing — correctness lens found no defect and all guardrails held; the test-coverage gaps it flagged (ask peek path, budget over-report branch, query-count assertion) are the extra tests above.
Needs your review/merge on !735, then close. @PlasticDigits
mentioned in commit
d8ed85583e