Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion fsm/dex.go
Original file line number Diff line number Diff line change
Expand Up @@ -815,13 +815,21 @@ func liquidityDepositPoints(totalPoints, x, y, amount uint64) (uint64, lib.Error
return lib.SafeMulDiv(totalPoints, newK-oldK, oldK), nil
}

// migrateLegacyLPs() performs the one-time migration of a pool above the provider limit.
// migrateLegacyLPs() drains a pool sitting above the provider limit down toward MaxLiquidityProviders.
// The drain is bounded to MaxLPEvictionsPerBlock per block so a large legacy pool (e.g. one that reached the
// previous 50k limit) is pruned across multiple blocks rather than in a single oversized block that can't be
// applied within a consensus phase. It runs on every deposit-batch cycle until the pool reaches the cap.
func (s *StateMachine) migrateLegacyLPs(batch *lib.DexBatch, p *Pool, chainId uint64, x, y *uint64, local bool) lib.ErrorI {
// calculate how many legacy providers must be removed to reach the limit
excess := len(p.Points) - lib.MaxLiquidityProviders
if excess <= 0 {
return nil
}
// bound the per-block eviction work to the consensus round budget (mirrors MaxOrdersSettledPerBlock);
// the remaining excess is drained on subsequent blocks until the pool reaches the cap
if excess > lib.MaxLPEvictionsPerBlock {
excess = lib.MaxLPEvictionsPerBlock
}
// rank the providers from lowest to highest points
sort.Slice(p.Points, func(i, j int) bool {
// rank by points first
Expand Down
28 changes: 24 additions & 4 deletions fsm/dex_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2898,7 +2898,8 @@ func TestHandleBatchDepositRanksNewcomersByProviderTotal(t *testing.T) {
func TestHandleBatchDepositMigratesPoolBeforeDeposits(t *testing.T) {
sm := newTestStateMachine(t)
chainID := uint64(2)
points := make([]*lib.PoolPoints, 50_000)
const initialProviders = 50_000
points := make([]*lib.PoolPoints, initialProviders)
var total uint64
for i := range points {
address := make([]byte, crypto.AddressSize)
Expand All @@ -2912,17 +2913,36 @@ func TestHandleBatchDepositMigratesPoolBeforeDeposits(t *testing.T) {
require.NoError(t, sm.SetPool(&Pool{Id: chainID + LiquidityPoolAddend, Amount: 1_000_000, Points: points, TotalPoolPoints: total}))

x, y := uint64(1_000_000), uint64(1_000_000)
require.NoError(t, sm.HandleBatchDeposit(&lib.DexBatch{Deposits: []*lib.DexLiquidityDeposit{{
// deposit from the highest-ranked provider so it always remains an incumbent across the drain
deposit := &lib.DexBatch{Deposits: []*lib.DexLiquidityDeposit{{
Address: points[len(points)-1].Address, Amount: 100,
}}}, chainID, &x, &y, false))
}}}

// the drain is bounded to MaxLPEvictionsPerBlock per block: a single batch prunes exactly that many
// lowest-ranked providers, NOT the whole excess at once (which would produce an unappliable block)
require.NoError(t, sm.HandleBatchDeposit(deposit, chainID, &x, &y, false))
pool, err := sm.GetPool(chainID + LiquidityPoolAddend)
require.NoError(t, err)
require.Len(t, pool.Points, lib.MaxLiquidityProviders)
require.Len(t, pool.Points, initialProviders-lib.MaxLPEvictionsPerBlock)
// the lowest-ranked providers are evicted first
for _, evicted := range points[1:3] {
_, err = pool.GetPointsFor(evicted.Address)
require.Error(t, err)
}

// subsequent blocks continue draining MaxLPEvictionsPerBlock at a time until the pool reaches the cap
iterations := 1
for len(pool.Points) > lib.MaxLiquidityProviders {
require.Less(t, iterations, 100, "drain failed to converge")
require.NoError(t, sm.HandleBatchDeposit(deposit, chainID, &x, &y, false))
pool, err = sm.GetPool(chainID + LiquidityPoolAddend)
require.NoError(t, err)
iterations++
}
require.Len(t, pool.Points, lib.MaxLiquidityProviders)
// the permanent dead address is never evicted
_, err = pool.GetPointsFor(deadAddr.Bytes())
require.NoError(t, err)
}

func TestHandleBatchDepositUsesReceiptHashForEqualStakeTie(t *testing.T) {
Expand Down
6 changes: 6 additions & 0 deletions lib/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ const (
// MaxOrdersSettledPerBlock caps DEX orders settled per block so begin_block can't exceed the consensus
// round budget; orders beyond the cap are failed (receipt 0) and refunded to the seller on the origin chain
MaxOrdersSettledPerBlock = 250
// MaxLPEvictionsPerBlock caps how many legacy liquidity providers are force-evicted per block when
// draining a pool that sits above MaxLiquidityProviders. Like MaxOrdersSettledPerBlock, this bounds the
// per-block work so applying the block stays within the consensus round budget: an over-cap pool drains
// toward the limit across multiple blocks instead of in a single oversized block that can't be
// applied+voted within a consensus phase (which livelocks the chain).
MaxLPEvictionsPerBlock = 5_000
)

// MaxBlockHeaderSize is a consensus breaking change because it affects how the state machine
Expand Down
Loading