Limit book match: bound expired-order scan (MAX_SCAN_STEPS) and tune park cap #254
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#254
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
Bound the limit-book match walk so expired/skipped head orders cannot force unbounded
ORDERSreads per taker swap, and raiseMAX_EXPIRED_PARKS_PER_SWAPmodestly so cleanup keeps pace within that budget.Current codebase
match_bids/match_asksand read-onlysimulate_match_*) insmartcontracts/contracts/pair/src/orderbook.rsterminate on:makers_used >= max_maker_fills(clamped toMAX_MAKER_FILLS_HARD_CAP= 256),now >= expires_at:expired_parks < MAX_EXPIRED_PARKS_PER_SWAP(constant 5 index-common::pair),park_expired_limit_order_for_claimruns (unlink +EXPIRED_LIMIT_CLAIMSwrite + event).expired_parks_skipped++) and the loop continues.makers_used. There is no cap on total loop iterations. A long expired prefix at the head can make every hybrid swap load the entire prefix until live orders are reached; only 5 parks clear per tx, so cleanup can lag far behind scan cost (roughly O(N²) reads across swaps draining a backlog).execute_swap(pool_input_amount = pool_leg + (book_leg - offer_consumed_by_book)), thenassert_max_spread— stopping the book walk early does not require revert.expired_parks_used,expired_parks_capped,expired_parks_skipped(seecontract.rs~1058–1067).smartcontracts/tests/src/limit_order_tests.rs(expired park cap behavior);orderbook.rsaggregation tests.Why this is needed
max_maker_fillsor frontend hybrid gas estimates (hybridSwapGas.tskeys off maker fills, not skip count).Constraints and guardrails
MAX_MAKER_FILLS_HARD_CAP(256): parking is write-heavy (unlink + claim row + event per order) and would make worst-case swap gas worse, not better.dex-commonanddocs/limit-orders.md.MAX_SCAN_STEPS(or equivalent) index-common::pairwith a hard ceiling; count every list iteration (fills + parks + skips + zero-remaining continues). Stop when budget exhausted; return partial book consumption (existing pool spillover handles remainder).simulate_match_bids/simulate_match_asksmust apply the same step budget so quotes match execute behavior.expired_parks_capped, newscan_steps_cappedif needed).ClaimExpiredLimitOrderremains separate; parked rows still require maker claim.Relevant files
smartcontracts/contracts/pair/src/orderbook.rssmartcontracts/contracts/pair/src/contract.rssmartcontracts/packages/dex-common/src/pair.rsdocs/limit-orders.md,docs/contracts-security-audit.md(invariant L8/L10 area)skills/AGENTS_TERRACLASSIC_GAS.mdsmartcontracts/tests/src/limit_order_tests.rs,smartcontracts/contracts/pair/src/orderbook.rs(aggregation_tests, expiry tests)frontend-dapp/src/services/terraclassic/hybridSwapGas.ts— note if estimates should include scan capRecommended direction
MAX_SCAN_STEPS(suggest:max_maker_fills + Kwith fixedK, ormin(MAX_MAKER_FILLS_HARD_CAP + K, HARD_CAP_SCAN)— pick one formula, document rationale).scan_stepson everywhileiteration in match + simulate paths; break whenscan_steps >= MAX_SCAN_STEPS(same break conditions as budget exhausted).MAX_EXPIRED_PARKS_PER_SWAPto a reasonable value ≤ scan budget so parks can clear expired prefix within one bounded walk when possible.BookMatchResult/ swap attrs expose when scan cap hit (integrators: book may be partially cleared).Acceptance criteria
MAX_SCAN_STEPStimes per book side per call.max_maker_fills > 0, swap completes without OOG; book consumption ≤ budget; pool leg absorbs remainder under existing spread rules.HybridSimulationbook leg matches execute for same params when scan cap binds.Test plan (functional paths)
max_maker_fillsbook_input = 0makers_used,offer_consumed, cap flagsTest plan (attack / abuse / hack vectors)
expired_parks_skippedVerification criteria
cargo testinsmartcontracts/(limit order + hybrid suites) green.gas_usedvs unbounded baseline (benchmark note in issue comment optional).AGENTS_TERRACLASSIC_GAS.mdmentionMAX_SCAN_STEPSand new park cap.mentioned in issue #257
mentioned in commit
22e825fe22Implementation summary (GitLab #254)
Bound hybrid limit-book match walks with a scan-step budget and modestly raised the expired-order park cap.
Changes
MAX_SCAN_STEPS = 288(MAX_MAKER_FILLS_HARD_CAP+ 32) index-common::pair— every book-walk iteration (fills, parks, skips, zero-remaining continues) counts toward this hard ceiling.MAX_EXPIRED_PARKS_PER_SWAPraised from 5 → 15 — faster expired-prefix cleanup within the scan budget without write-heavy worst case (still well below 256).match_bids/match_asks/simulate_match_*share the same step budget viabook_walk_step(); early stop setsscan_steps_cappedonBookMatchResult/BookSimulateResult.scan_steps_capped=truewhen the walk truncates (alongside existingexpired_parks_*attrs).docs/limit-orders.md,docs/contracts-security-audit.md(L5),skills/AGENTS_TERRACLASSIC_GAS.md,skills/AGENTS_FRONTEND_LIMIT_PARKED_EXPIRED.md.Commit:
22e825fonmain.Verification checklist
cd smartcontracts && cargo test expired_park— unit tests for park cap + scan cap passcd smartcontracts && cargo test scan_steps— scan-cap + sim/execute parity passcd smartcontracts && cargo test -p cl8y-dex-tests limit_order— full limit-order integration suite green (53 tests)scan_steps_capped=trueand non-zeropool_return_amount(book remainder spilled)HybridSimulationbook leg matches execute for same params when scan cap binds (simulate_match_bids_scan_steps_cap_matches_execute)scan_steps_cappedswap attr;expired_parks_*still emitted when park cap bitesFollow-ups (optional)
hybridSwapGas.tsstill keys offmax_maker_fills, not scan-step count — worst-case expired-prefix taker gas may exceed the dApp quote envelope (documented inAGENTS_TERRACLASSIC_GAS.mdrule 16). Tune after LocalTerra benchmark if needed (#252).@qa-agent-team — please verify the checklist above on
main@22e825f(contracts-only change; pair wasm redeploy required for on-chain effect). Issue left open pending QA sign-off.mentioned in issue #260
marked as related to #260
mentioned in commit
844f27506ementioned in issue #252
mentioned in issue #262
marked as related to #262
mentioned in issue #263
marked as related to #263
Verified #254 on
main@6b22feb(redeployed + pair wasm live on LocalTerra, indexer restarted). Source + named tests + two live on-chain runs. All acceptance criteria covered.AC1 — match + simulate loops bounded by MAX_SCAN_STEPS/side/call:
Source:
book_walk_stepis called at the top of every iteration in all four loops (match_bids/match_asks+ bothsimulate_match_*), counting fills, parks, skips, and zero-remaining continues alike; returns false atMAX_SCAN_STEPS=288(256+32) → setsscan_steps_cappedand breaks. Testsmatch_bids_scan_steps_cap_bounds_expired_prefix_walk+hybrid_walk_scan_steps_cap_bounds_expired_prefix_and_spills_to_pool. Live: 300 expired bids in one prefix → taker swap walked exactly 288 (15 parked + 273 skipped),scan_steps_capped=true, code=0,gas_used=1,427,267(bounded), 12 orders never reached.AC2 — expired-only head completes, no OOG, book ≤ budget, pool absorbs remainder:
Both live runs: swap
code=0,limit_book_offer_consumed=0,book_return_amount=0, and the offer spilled to the pool leg (pool_return_amount279,009,033 in the 20-order run / 277,316,610 in the 300-order run) under existing spread rules. Testhybrid_walk_scan_steps_cap_..._spills_to_pool.AC3 — park cap raised + documented, worst-case parks ≤ cap:
Source:
MAX_EXPIRED_PARKS_PER_SWAP5 → 15, park-then-skip logic. Docs: invariant L5 (contracts-security-audit.md),AGENTS_TERRACLASSIC_GAS.mdrule 16,limit-orders.md(park cap / scan budget / attrs). Tests{match_bids,match_asks}_parks_at_most_max_expired_parks_per_swap,hybrid_walk_twenty_expired_asks_parks_cap_skips_remainder. Live: 20-order run parked exactly 15,expired_parks_capped=true,expired_parks_skipped=5.AC4 — existing park tests updated + new N>scan-budget test proving bounded iterations + attrs:
match_bids_scan_steps_cap_bounds_expired_prefix_walk(N = MAX_SCAN_STEPS+50, asserts the iteration bound) + the integration spill test. Green on6b22feb.AC5 — HybridSimulation book leg matches execute when cap binds:
Source: simulate paths use the same
book_walk_stepbudget. Testssimulate_match_bids_scan_steps_cap_matches_execute+hybrid_simulation_matches_execute_with_expired_park_cap. (Test-verified; not separately exercised as a live sim-vs-execute query.)cargo test (
6b22feb): the #254 suite is green —orderbook::expired_park_cap_tests::{match_bids_parks_at_most…, match_asks_parks_at_most…, match_bids_parks_three_expired_then_fills_behind, match_bids_scan_steps_cap_bounds_expired_prefix_walk, simulate_match_bids_scan_steps_cap_matches_execute}and integrationhybrid_walk_*,hybrid_simulation_matches_execute_with_expired_park_cap,skipped_expired_bid_cancelable_by_maker,expired_bid_parked_on_hybrid_walk_claim_refunds_maker. Full contract suite 378 passed / 0 failed.Two layer items flagged (not blockers):
scan_steps_capped,expired_parks_*) are confirmed emitted live, but whether the indexer/dApp parse them is a separate layer (indexer source / frontend on laptop) — not checked here.@PlasticDigits — verified and signed off from my side, no issues found; over to you to close.
@Brouie Must check indexer parsing of swap attrs - way to verify is sql query on db
mentioned in issue #255
mentioned in issue #256
Checked the indexer parsing of the swap attrs via SQL on
dex_indexer, per your ask. Answer to the question first:PASS — a #254 cap-path swap parses correctly into
swap_events.Used the scan-cap swap from my #254 run (
swap_eventsid 322 — on-chain it carriesscan_steps_capped=true,expired_parks_used=15,expired_parks_capped=true,expired_parks_skipped=273). DB row vs on-chain, exact across the board:The book-fill fields populate too — a fill-bearing hybrid swap (id 324) lands
book_return_amount=19,810,080,limit_book_offer_consumed=11,658,820, matching on-chain.Indexer was caught up when I checked:
indexer_state.last_indexed_height=3,628,720(≥ node tip),indexer_failed_blocks=0.On the cap attrs themselves: the indexer doesn't track them — there are no
swap_eventscolumns forexpired_parks_*/scan_steps_capped, and the parser reads only the standard keys, each looked up by name (wasm_attr_last(attrs, key)) and parsed independently. So the cap attrs are simply never read, and because parsing is key-based they can't drop or corrupt the swap row. By design for the swap row, and harmless — confirmed on the heaviest cap path (273 skipped) above.Separate flag — NOT a #254 blocker, pre-existing, found while doing this:
limit_order_fills(the per-maker fill rows) comes up empty for hybrid swaps that fill multiple makers — even though the fills happen on-chain (id 323 = 5limit_order_fillevents, id 324 = 20). The parser + dispatch + insert are all wired and no blocks failed, so it's silently parsing zero fills.Root cause (confirmed on id 324): the chain merges the contract's per-fill
wasmevents — the 20limit_order_fillattributes collapse into ~5 groupedwasmevents, and the lastactionin each merged group isn'tlimit_order_fill(it'sswap/transfer).parse_limit_order_fillsgates per-event onwasm_attr_last(attrs,"action") == "limit_order_fill", so it never matches → 0 fills parsed. (Even if a group did end inlimit_order_fill,wasm_attr_lastonorder_id/makerwould only capture the last fill, not all of them.)The swap row is unaffected — its
action=swaplands last in its own group, soswap_eventsparses fine (hence the PASS above). This looks like it predates the gas wave (the fill parser/event aren't new), so it's its own indexer issue, not part of #254.@PlasticDigits — #254 indexer parsing checks out, good to close from my side. Want me to file the
limit_order_fillsper-maker gap as its own indexer issue with this root-cause, or will you take it?mentioned in issue #258
mentioned in issue #269
Closing #254 — contract scan-cap work verified on main. Per-maker
limit_order_fillsindexing gap tracked separately: #269.mentioned in issue #289
mentioned in issue #309
mentioned in issue #708