Skip to content

feat(dex): cap LP providers with deterministic replacement#478

Merged
andrewnguyen22 merged 9 commits into
mainfrom
feat/dex-lp-provider-cap
Jul 23, 2026
Merged

feat(dex): cap LP providers with deterministic replacement#478
andrewnguyen22 merged 9 commits into
mainfrom
feat/dex-lp-provider-cap

Conversation

@andrewnguyen22

@andrewnguyen22 andrewnguyen22 commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • cap liquidity pools at 5,000 providers and deterministically replace the lowest-point provider when a better-funded newcomer arrives
  • migrate legacy pools of up to 50,000 providers on the first capped deposit while enforcing the 5,000-provider certificate limit for every path, including liveness fallback
  • aggregate newcomer deposits by address, use receipt-hash-seeded tie breaking, give evictions distinct deterministic IDs, and expose the auto-withdrawal percentage to indexers
  • select and apply replacements in memory and persist the pool once, avoiding thousands of complete 5,000-provider pool rewrites
  • permit certificate-result transactions above the individual transaction limit while retaining the collective block/mempool byte bound

Validation

  • all non-audit packages: go list -e ./... | rg -v '/audit/' | xargs go test
  • go test -race ./fsm -run 'TestHandleBatch|Test.*Capped|Test.*Liquidity|TestHandleDexBatchOrdersEnforcesSettlementCap' -count=1
  • go test -race ./lib -run 'TestCertificateResultsMayExceedIndividualMaxTxSize|TestDexBatch' -count=1
  • two-node Docker integration: normal cross-chain DEX trade completed successfully
  • legacy migration: both 50,000-provider pools converged to the same 5,000-provider state
  • maximum newcomer batch on a 1 CPU / 2 GiB node: 1.67s proposal construction and 1.72s validation
  • indexing check: 4,999 accepted deposits and 4,999 unique 100% auto-withdrawals, with unique order IDs and no zero-point providers

The repository-wide go test ./... also passes all normal packages; its only failures are checked-in audit/**/artifacts repro directories that are not standalone buildable packages.

@andrewnguyen22
andrewnguyen22 force-pushed the feat/dex-lp-provider-cap branch from d664734 to 62aa4d5 Compare July 21, 2026 15:06
@andrewnguyen22
andrewnguyen22 force-pushed the feat/dex-lp-provider-cap branch from 62aa4d5 to 6799d24 Compare July 21, 2026 15:30
@andrewnguyen22 andrewnguyen22 self-assigned this Jul 21, 2026
@andrewnguyen22 andrewnguyen22 added the FSM Finite State Machine module label Jul 21, 2026
Comment thread lib/certificate.go Outdated
Comment thread fsm/dex.go Outdated
Comment thread fsm/dex.go
Comment thread fsm/dex.go
Comment thread fsm/dex.go
Comment thread fsm/dex.go Outdated
Comment thread fsm/dex.go
Comment thread fsm/dex.go Outdated

@rem1niscence rem1niscence left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check the comments left

Comment thread fsm/dex.go
}
}
// removes legacy zero-point “ghost” LPs
p.Points = slices.DeleteFunc(p.Points, func(point *lib.PoolPoints) bool { return point.Points == 0 })

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do this deletion at the beginning of the function when it will be called again after the withdrawals are processed? shouldn't the second call also clean the "ghost" LPs?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, the second pass cleans old ghosts too if we actually reach it. The catch is the early return above: if the batch only touches zero-point holders, totalPointsToRemove is 0 and we never get to that second pass. So the first one clears old ghosts, and the second clears LPs this withdrawal just took to zero.

Comment thread cmd/rpc/query.go
func (s *Server) Transactions(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
// create a slice to hold the incoming transactions
var txs []lib.TransactionI
var transactions []*lib.Transaction

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the logic behind using the concrete type? when I first made this change I used the interface in order to avoid the iteration done now to cast to the concrete type as a small optimization, any reason to do this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I wanted to keep the interface here too, but encoding/json can't instantiate entries in []lib.TransactionI, so normal tx objects fail to decode on the batch endpoint. Decoding into []*lib.Transaction gives it a concrete target. The loop is only wrapping pointers in the interface slice, not copying the txs, and keeps us from needing a custom decoder or a wider submitTxs change.

@andrewnguyen22
andrewnguyen22 merged commit a4c4163 into main Jul 23, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

FSM Finite State Machine module

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants