From 8f37be3e23179a92b48a27c04c227c97024de33a Mon Sep 17 00:00:00 2001 From: sampocs Date: Wed, 27 Dec 2023 09:05:29 -0600 Subject: [PATCH] sort unbonding prioritization by validator capacity (#1018) --- x/stakeibc/keeper/unbonding_records.go | 154 ++++++++-- ...ords_get_host_zone_unbondings_msgs_test.go | 281 ++++++++++++++---- 2 files changed, 354 insertions(+), 81 deletions(-) diff --git a/x/stakeibc/keeper/unbonding_records.go b/x/stakeibc/keeper/unbonding_records.go index 676f5fae77..30866cd61a 100644 --- a/x/stakeibc/keeper/unbonding_records.go +++ b/x/stakeibc/keeper/unbonding_records.go @@ -160,13 +160,7 @@ func SortUnbondingCapacityByPriority(validatorUnbondCapacity []ValidatorUnbondCa validatorA := validatorUnbondCapacity[i] validatorB := validatorUnbondCapacity[j] - balanceRatioValA, _ := validatorA.GetBalanceRatio() - balanceRatioValB, _ := validatorB.GetBalanceRatio() - - // Sort by the balance ratio first - in ascending order - so the more unbalanced validators appear first - if !balanceRatioValA.Equal(balanceRatioValB) { - return balanceRatioValA.LT(balanceRatioValB) - } + // TODO: Once more than 32 validators are supported, change back to using balance ratio first // If the ratio's are equal, use the capacity as a tie breaker // where the larget capacity comes first @@ -191,6 +185,7 @@ func (k Keeper) GetUnbondingICAMessages( hostZone types.HostZone, totalUnbondAmount sdkmath.Int, prioritizedUnbondCapacity []ValidatorUnbondCapacity, + batchSize int, ) (msgs []proto.Message, unbondings []*types.SplitDelegation, err error) { // Loop through each validator and unbond as much as possible remainingUnbondAmount := totalUnbondAmount @@ -208,16 +203,7 @@ func (k Keeper) GetUnbondingICAMessages( } else { unbondAmount = remainingUnbondAmount } - remainingUnbondAmount = remainingUnbondAmount.Sub(unbondAmount) - unbondToken := sdk.NewCoin(hostZone.HostDenom, unbondAmount) - - // Build the undelegate ICA messages - msgs = append(msgs, &stakingtypes.MsgUndelegate{ - DelegatorAddress: hostZone.DelegationIcaAddress, - ValidatorAddress: validatorCapacity.ValidatorAddress, - Amount: unbondToken, - }) // Build the validator splits for the callback unbondings = append(unbondings, &types.SplitDelegation{ @@ -226,6 +212,30 @@ func (k Keeper) GetUnbondingICAMessages( }) } + // If the number of messages exceeds the batch size, shrink it down the the batch size + // by re-distributing the exceess + if len(unbondings) > batchSize { + unbondings, err = k.ConsolidateUnbondingMessages(totalUnbondAmount, unbondings, prioritizedUnbondCapacity, batchSize) + if err != nil { + return msgs, unbondings, errorsmod.Wrapf(err, "unable to consolidate unbonding messages") + } + + // Sanity check that the number of messages is now under the batch size + if len(unbondings) > batchSize { + return msgs, unbondings, errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, + fmt.Sprintf("too many undelegation messages (%d) for host zone %s", len(msgs), hostZone.ChainId)) + } + } + + // Build the undelegate ICA messages from the splits + for _, unbonding := range unbondings { + msgs = append(msgs, &stakingtypes.MsgUndelegate{ + DelegatorAddress: hostZone.DelegationIcaAddress, + ValidatorAddress: unbonding.Validator, + Amount: sdk.NewCoin(hostZone.HostDenom, unbonding.Amount), + }) + } + // Sanity check that we had enough capacity to unbond if !remainingUnbondAmount.IsZero() { return msgs, unbondings, @@ -235,6 +245,107 @@ func (k Keeper) GetUnbondingICAMessages( return msgs, unbondings, nil } +// In the event that the number of generated undelegate messages exceeds the batch size, +// reduce the number of messages by dividing any excess amongst proportionally based on +// the remaining delegation +// This will no longer be necessary after undelegations to 32+ validators is supported +// NOTE: This assumes unbondCapacities are stored in order of capacity +func (k Keeper) ConsolidateUnbondingMessages( + totalUnbondAmount sdkmath.Int, + initialUnbondings []*types.SplitDelegation, + unbondCapacities []ValidatorUnbondCapacity, + batchSize int, +) (finalUnbondings []*types.SplitDelegation, err error) { + // Grab the first {batch_size} number of messages from the list + // This will consist of the validators with the most capacity + unbondingsBatch := initialUnbondings[:batchSize] + + // Calculate the amount that was initially meant to be unbonded from that batch, + // and determine the remainder that needs to be redistributed + initialUnbondAmountFromBatch := sdkmath.ZeroInt() + initialUnbondAmountFromBatchByVal := map[string]sdkmath.Int{} + for _, unbonding := range unbondingsBatch { + initialUnbondAmountFromBatch = initialUnbondAmountFromBatch.Add(unbonding.Amount) + initialUnbondAmountFromBatchByVal[unbonding.Validator] = unbonding.Amount + } + totalExcessAmount := totalUnbondAmount.Sub(initialUnbondAmountFromBatch) + + // Store the delegation of each validator that was expected *after* the originally + // planned unbonding went through + // e.g. If the validator had 10 before unbonding, and in the first pass, 3 was + // supposed to be unbonded, their delegation after the first pass is 7 + totalRemainingDelegationsAcrossBatch := sdk.ZeroDec() + remainingDelegationsInBatchByVal := map[string]sdk.Dec{} + for _, capacity := range unbondCapacities { + // Only add validators that were in the initial unbonding plan + // The delegation after the first pass is calculated by taking the "current delegation" + // (aka delegation before unbonding) and subtracting the unbond amount + if initialUnbondAmount, ok := initialUnbondAmountFromBatchByVal[capacity.ValidatorAddress]; ok { + remainingDelegation := sdk.NewDecFromInt(capacity.CurrentDelegation.Sub(initialUnbondAmount)) + + remainingDelegationsInBatchByVal[capacity.ValidatorAddress] = remainingDelegation + totalRemainingDelegationsAcrossBatch = totalRemainingDelegationsAcrossBatch.Add(remainingDelegation) + } + } + + // This is to protect against a division by zero error, but this would technically be possible + // if the 32 validators with the most capacity were all 0 weight and we wanted to unbond more + // than their combined delegation + if totalRemainingDelegationsAcrossBatch.IsZero() { + return finalUnbondings, errors.New("no delegations to redistribute during consolidation") + } + + // Before we start dividing up the excess, make sure we have sufficient stake in the capped set to cover it + if sdk.NewDecFromInt(totalExcessAmount).GT(totalRemainingDelegationsAcrossBatch) { + return finalUnbondings, errors.New("not enough exisiting delegation in the batch to cover the excess") + } + + // Loop through the original unbonding messages and proportionally divide out + // the excess amongst the validators in the set + excessRemaining := totalExcessAmount + for i := range unbondingsBatch { + unbonding := unbondingsBatch[i] + remainingDelegation, ok := remainingDelegationsInBatchByVal[unbonding.Validator] + if !ok { + return finalUnbondings, fmt.Errorf("validator %s not found in initial unbonding plan", unbonding.Validator) + } + + var validatorUnbondIncrease sdkmath.Int + if i != len(unbondingsBatch)-1 { + // For all but the last validator, calculate their unbonding increase by + // splitting the excess proportionally in line with their remaining delegation + unbondIncreaseProportion := remainingDelegation.Quo(totalRemainingDelegationsAcrossBatch) + validatorUnbondIncrease = sdk.NewDecFromInt(totalExcessAmount).Mul(unbondIncreaseProportion).TruncateInt() + + // Decrement excess + excessRemaining = excessRemaining.Sub(validatorUnbondIncrease) + } else { + // The last validator in the set should get any remainder from int truction + // First confirm the validator has sufficient remaining delegation to cover this + if sdk.NewDecFromInt(excessRemaining).GT(remainingDelegation) { + return finalUnbondings, + fmt.Errorf("validator %s does not have enough remaining delegation (%v) to cover the excess (%v)", + unbonding.Validator, remainingDelegation, excessRemaining) + } + validatorUnbondIncrease = excessRemaining + } + + // Build the updated message with the new amount + finalUnbondings = append(finalUnbondings, &types.SplitDelegation{ + Validator: unbonding.Validator, + Amount: unbonding.Amount.Add(validatorUnbondIncrease), + }) + } + + // Sanity check that we've accounted for all the excess + if excessRemaining.IsZero() { + return finalUnbondings, fmt.Errorf("Unable to redistribute all excess - initial: %v, remaining: %v", + totalExcessAmount, excessRemaining) + } + + return finalUnbondings, nil +} + // Submits undelegation ICA messages for a given host zone // // First, the total unbond amount is determined from the epoch unbonding records @@ -300,7 +411,12 @@ func (k Keeper) UnbondFromHostZone(ctx sdk.Context, hostZone types.HostZone) err } // Get the undelegation ICA messages and split delegations for the callback - msgs, unbondings, err := k.GetUnbondingICAMessages(hostZone, totalUnbondAmount, prioritizedUnbondCapacity) + msgs, unbondings, err := k.GetUnbondingICAMessages( + hostZone, + totalUnbondAmount, + prioritizedUnbondCapacity, + UndelegateICABatchSize, + ) if err != nil { return err } @@ -310,10 +426,6 @@ func (k Keeper) UnbondFromHostZone(ctx sdk.Context, hostZone types.HostZone) err return errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "Target unbonded amount was 0 for each validator") } - if len(msgs) > UndelegateICABatchSize { - return errorsmod.Wrapf(sdkerrors.ErrInvalidRequest, fmt.Sprintf("too many undelegation messages (%d) for host zone %s", len(msgs), hostZone.ChainId)) - } - // Send the messages in batches so the gas limit isn't exceedeed for start := 0; start < len(msgs); start += UndelegateICABatchSize { end := start + UndelegateICABatchSize diff --git a/x/stakeibc/keeper/unbonding_records_get_host_zone_unbondings_msgs_test.go b/x/stakeibc/keeper/unbonding_records_get_host_zone_unbondings_msgs_test.go index 15e2b36881..653976663c 100644 --- a/x/stakeibc/keeper/unbonding_records_get_host_zone_unbondings_msgs_test.go +++ b/x/stakeibc/keeper/unbonding_records_get_host_zone_unbondings_msgs_test.go @@ -189,12 +189,10 @@ func (s *KeeperTestSuite) TestUnbondFromHostZone_Successful_UnbondOnlyZeroWeight {Address: "valG", Weight: 15, Delegation: sdkmath.NewInt(160)}, } + // TODO: Change back to two messages after 32+ validators are supported expectedUnbondings := []ValidatorUnbonding{ - // valC has #1 priority - unbond up to capacity at 40 - {Validator: "valC", UnbondAmount: sdkmath.NewInt(40)}, - // 50 - 40 = 10 unbond remaining - // valE has #2 priority - unbond up to remaining - {Validator: "valE", UnbondAmount: sdkmath.NewInt(10)}, + // valF has the most capacity (80) so it takes the full unbonding + {Validator: "valF", UnbondAmount: sdkmath.NewInt(50)}, } tc := s.SetupTestUnbondFromHostZone(totalWeight, totalStake, totalUnbondAmount, validators) @@ -274,15 +272,16 @@ func (s *KeeperTestSuite) TestUnbondFromHostZone_Successful_UnbondTotalLessThanT {Address: "valG", Weight: 15, Delegation: sdkmath.NewInt(160)}, } + // TODO: Change back to two messages after 32+ validators are supported expectedUnbondings := []ValidatorUnbonding{ - // valC has #1 priority - unbond up to capacity at 40 + // valF has highest capacity - 90 + {Validator: "valF", UnbondAmount: sdkmath.NewInt(90)}, + // 150 - 90 = 60 unbond remaining + // valC has next highest capacity - 40 {Validator: "valC", UnbondAmount: sdkmath.NewInt(40)}, - // 150 - 40 = 110 unbond remaining - // valE has #2 priority - unbond up to capacity at 30 - {Validator: "valE", UnbondAmount: sdkmath.NewInt(30)}, - // 150 - 40 - 30 = 80 unbond remaining - // valF has #3 priority - unbond up to remaining - {Validator: "valF", UnbondAmount: sdkmath.NewInt(80)}, + // 60 - 40 = 20 unbond remaining + // valB has next highest capacity - 35, unbond up to remainder of 20 + {Validator: "valB", UnbondAmount: sdkmath.NewInt(20)}, } tc := s.SetupTestUnbondFromHostZone(totalWeight, totalStake, totalUnbondAmount, validators) @@ -324,26 +323,27 @@ func (s *KeeperTestSuite) TestUnbondFromHostZone_Successful_UnbondTotalGreaterTh {Address: "valG", Weight: 15, Delegation: sdkmath.NewInt(160)}, } + // TODO: Change back to two messages after 32+ validators are supported expectedUnbondings := []ValidatorUnbonding{ - // valC has #1 priority - unbond up to capacity at 40 - {Validator: "valC", UnbondAmount: sdkmath.NewInt(40)}, - // 350 - 40 = 310 unbond remaining - // valE has #2 priority - unbond up to capacity at 30 - {Validator: "valE", UnbondAmount: sdkmath.NewInt(30)}, - // 310 - 30 = 280 unbond remaining - // valF has #3 priority - unbond up to capacity at 110 + // valF has highest capacity - 110 {Validator: "valF", UnbondAmount: sdkmath.NewInt(110)}, - // 280 - 110 = 170 unbond remaining - // valB has #4 priority - unbond up to capacity at 105 + // 350 - 110 = 240 unbond remaining + // valB has next highest capacity - 105 {Validator: "valB", UnbondAmount: sdkmath.NewInt(105)}, - // 170 - 105 = 65 unbond remaining - // valG has #5 priority - unbond up to capacity at 25 - {Validator: "valG", UnbondAmount: sdkmath.NewInt(25)}, - // 65 - 25 = 40 unbond remaining - // valD has #6 priority - unbond up to capacity at 30 + // 240 - 105 = 135 unbond remaining + // valC has next highest capacity - 40 + {Validator: "valC", UnbondAmount: sdkmath.NewInt(40)}, + // 135 - 40 = 95 unbond remaining + // valD has next highest capacity - 30 {Validator: "valD", UnbondAmount: sdkmath.NewInt(30)}, - // 40 - 30 = 10 unbond remaining - // valA has #7 priority - unbond up to remaining + // 95 - 30 = 65 unbond remaining + // valE has next highest capacity - 30 + {Validator: "valE", UnbondAmount: sdkmath.NewInt(30)}, + // 65 - 30 = 35 unbond remaining + // valG has next highest capacity - 25 + {Validator: "valG", UnbondAmount: sdkmath.NewInt(25)}, + // 35 - 25 = 10 unbond remaining + // valA covers the remainder up to it's capacity {Validator: "valA", UnbondAmount: sdkmath.NewInt(10)}, } @@ -636,38 +636,8 @@ func (s *KeeperTestSuite) TestGetValidatorUnbondCapacity() { func (s *KeeperTestSuite) TestSortUnbondingCapacityByPriority() { // First we define what the ideal list will look like after sorting + // TODO: Change back to sorting by unbond ratio after 32+ validators are supported expectedSortedCapacities := []keeper.ValidatorUnbondCapacity{ - // Zero-weight validator's - { - // (1) Ratio: 0, Capacity: 100 - ValidatorAddress: "valE", - BalancedDelegation: sdkmath.NewInt(0), - CurrentDelegation: sdkmath.NewInt(100), // ratio = 0/100 - Capacity: sdkmath.NewInt(100), - }, - { - // (2) Ratio: 0, Capacity: 25 - ValidatorAddress: "valC", - BalancedDelegation: sdkmath.NewInt(0), - CurrentDelegation: sdkmath.NewInt(25), // ratio = 0/25 - Capacity: sdkmath.NewInt(25), - }, - { - // (3) Ratio: 0, Capacity: 25 - // Same ratio and capacity as above but name is tie breaker - ValidatorAddress: "valD", - BalancedDelegation: sdkmath.NewInt(0), - CurrentDelegation: sdkmath.NewInt(25), // ratio = 0/25 - Capacity: sdkmath.NewInt(25), - }, - // Non-zero-weight validator's - { - // (4) Ratio: 0.1 - ValidatorAddress: "valB", - BalancedDelegation: sdkmath.NewInt(1), - CurrentDelegation: sdkmath.NewInt(10), // ratio = 1/10 - Capacity: sdkmath.NewInt(9), - }, { // (5) Ratio: 0.25 ValidatorAddress: "valH", @@ -675,6 +645,13 @@ func (s *KeeperTestSuite) TestSortUnbondingCapacityByPriority() { CurrentDelegation: sdkmath.NewInt(1000), // ratio = 250/1000 Capacity: sdkmath.NewInt(750), }, + { + // (1) Ratio: 0, Capacity: 100 + ValidatorAddress: "valE", + BalancedDelegation: sdkmath.NewInt(0), + CurrentDelegation: sdkmath.NewInt(100), // ratio = 0/100 + Capacity: sdkmath.NewInt(100), + }, { // (6) Ratio: 0.5, Capacity: 100 ValidatorAddress: "valF", @@ -698,6 +675,28 @@ func (s *KeeperTestSuite) TestSortUnbondingCapacityByPriority() { CurrentDelegation: sdkmath.NewInt(100), // ratio = 50/100 Capacity: sdkmath.NewInt(50), }, + { + // (2) Ratio: 0, Capacity: 25 + ValidatorAddress: "valC", + BalancedDelegation: sdkmath.NewInt(0), + CurrentDelegation: sdkmath.NewInt(25), // ratio = 0/25 + Capacity: sdkmath.NewInt(25), + }, + { + // (3) Ratio: 0, Capacity: 25 + // Same ratio and capacity as above but name is tie breaker + ValidatorAddress: "valD", + BalancedDelegation: sdkmath.NewInt(0), + CurrentDelegation: sdkmath.NewInt(25), // ratio = 0/25 + Capacity: sdkmath.NewInt(25), + }, + { + // (4) Ratio: 0.1 + ValidatorAddress: "valB", + BalancedDelegation: sdkmath.NewInt(1), + CurrentDelegation: sdkmath.NewInt(10), // ratio = 1/10 + Capacity: sdkmath.NewInt(9), + }, { // (9) Ratio: 0.6 ValidatorAddress: "valA", @@ -770,11 +769,24 @@ func (s *KeeperTestSuite) TestGetUnbondingICAMessages() { DelegationIcaAddress: delegationAddress, } + batchSize := 4 validatorCapacities := []keeper.ValidatorUnbondCapacity{ {ValidatorAddress: "val1", Capacity: sdkmath.NewInt(100)}, {ValidatorAddress: "val2", Capacity: sdkmath.NewInt(200)}, {ValidatorAddress: "val3", Capacity: sdkmath.NewInt(300)}, {ValidatorAddress: "val4", Capacity: sdkmath.NewInt(400)}, + + // This validator will fall out of the batch and will be redistributed + {ValidatorAddress: "val5", Capacity: sdkmath.NewInt(1000)}, + } + + // Set the current delegation to 1000 + capacity so after their delegation + // after the first pass at unbonding is left at 1000 + // This is so that we can simulate consolidating messages after reaching + // the capacity of the first four validators + for i, capacity := range validatorCapacities[:batchSize] { + capacity.CurrentDelegation = sdkmath.NewInt(1000).Add(capacity.Capacity) + validatorCapacities[i] = capacity } testCases := []struct { @@ -841,9 +853,20 @@ func (s *KeeperTestSuite) TestGetUnbondingICAMessages() { {Validator: "val4", UnbondAmount: sdkmath.NewInt(400)}, }, }, + { + name: "unbonding requires message consolidation", + totalUnbondAmount: sdkmath.NewInt(2000), // excess of 1000 + expectedUnbondings: []ValidatorUnbonding{ + // Redistributed excess denoted after plus sign + {Validator: "val1", UnbondAmount: sdkmath.NewInt(100 + 250)}, + {Validator: "val2", UnbondAmount: sdkmath.NewInt(200 + 250)}, + {Validator: "val3", UnbondAmount: sdkmath.NewInt(300 + 250)}, + {Validator: "val4", UnbondAmount: sdkmath.NewInt(400 + 250)}, + }, + }, { name: "insufficient delegation", - totalUnbondAmount: sdkmath.NewInt(1001), + totalUnbondAmount: sdkmath.NewInt(2001), expectedError: "unable to unbond full amount", }, } @@ -855,6 +878,7 @@ func (s *KeeperTestSuite) TestGetUnbondingICAMessages() { hostZone, tc.totalUnbondAmount, validatorCapacities, + batchSize, ) // If this is an error test case, check the error message @@ -888,3 +912,140 @@ func (s *KeeperTestSuite) TestGetUnbondingICAMessages() { }) } } + +func (s *KeeperTestSuite) TestConsolidateUnbondingMessages_Success() { + batchSize := 4 + totalUnbondAmount := int64(1501) + excessUnbondAmount := int64(101) + + validatorMetadata := []struct { + address string + initialUnbondAmount int64 + remainingDelegation int64 + expectedDelegationIncrease int64 + }{ + // Total Remaining Portion: 1000 + 500 + 250 + 250 = 2000 + // Excess Portion = Remaining Delegation / Total Remaining Portion + + // Excess Portion: 1000 / 2000 = 50% + // Delegation Increase: 50% * 101 = 50 + {address: "val1", initialUnbondAmount: 500, remainingDelegation: 1000, expectedDelegationIncrease: 50}, + // Excess Portion: 500 / 2000 = 25% + // Delegation Increase: 25% * 101 = 25 + {address: "val2", initialUnbondAmount: 400, remainingDelegation: 500, expectedDelegationIncrease: 25}, + // Excess Portion: 250 / 2000 = 12.5% + // Delegation Increase: 12.5% * 101 = 12 + {address: "val3", initialUnbondAmount: 300, remainingDelegation: 250, expectedDelegationIncrease: 12}, + // Excess Portion: 250 / 2000 = 12.5% (gets overflow) + // Delegation Increase (overflow): 101 - 25 - 12 = 14 + {address: "val4", initialUnbondAmount: 200, remainingDelegation: 250, expectedDelegationIncrease: 14}, + + // Total Excess: 51 + 50 = 101 + {address: "val5", initialUnbondAmount: 51}, // excess + {address: "val6", initialUnbondAmount: 50}, // excess + } + + // Validate test setup - amounts in the list should match the expected totals + totalCheck := int64(0) + excessCheckFromInitial := int64(0) + excessCheckFromExpected := int64(0) + for i, validator := range validatorMetadata { + totalCheck += validator.initialUnbondAmount + if i >= batchSize { + excessCheckFromInitial += validator.initialUnbondAmount + excessCheckFromExpected += validator.initialUnbondAmount + } + } + s.Require().Equal(totalUnbondAmount, totalCheck, + "mismatch in test case setup - sum of initial unbond amount does not match total") + s.Require().Equal(excessUnbondAmount, excessCheckFromInitial, + "mismatch in test case setup - sum of excess from initial unbond amount does not match total excess") + s.Require().Equal(excessUnbondAmount, excessCheckFromExpected, + "mismatch in test case setup - sum of excess from expected delegation increase does not match total excess") + + // Retroactively build validator capacities and messages + // Also build the expected unbondings after the consolidation + initialUnbondings := []*types.SplitDelegation{} + expectedUnbondings := []*types.SplitDelegation{} + validatorCapacities := []keeper.ValidatorUnbondCapacity{} + for i, validator := range validatorMetadata { + // The "unbondings" are the initial unbond amounts + initialUnbondings = append(initialUnbondings, &types.SplitDelegation{ + Validator: validator.address, + Amount: sdkmath.NewInt(validator.initialUnbondAmount), + }) + + // The "capacity" should also be the initial unbond amount + // (we're assuming all validators tried to unbond to capacity) + // The "current delegation" is their delegation before the unbonding, + // which equals the initial unbond amount + the remainder + validatorCapacities = append(validatorCapacities, keeper.ValidatorUnbondCapacity{ + ValidatorAddress: validator.address, + Capacity: sdkmath.NewInt(validator.initialUnbondAmount), + CurrentDelegation: sdkmath.NewInt(validator.initialUnbondAmount + validator.remainingDelegation), + }) + + // The expected unbondings is their initial unbond amount plus the increase + if i < batchSize { + expectedUnbondings = append(expectedUnbondings, &types.SplitDelegation{ + Validator: validator.address, + Amount: sdkmath.NewInt(validator.initialUnbondAmount + validator.expectedDelegationIncrease), + }) + } + } + + // Call the consolidation function + finalUnbondings, err := s.App.StakeibcKeeper.ConsolidateUnbondingMessages( + sdkmath.NewInt(totalUnbondAmount), + initialUnbondings, + validatorCapacities, + batchSize, + ) + s.Require().NoError(err, "no error expected when consolidating unbonding messages") + + // Validate the final messages matched expectations + s.Require().Equal(batchSize, len(finalUnbondings), "number of consolidated unbondings") + + for i := range finalUnbondings { + validator := validatorMetadata[i] + initialUnbonding := initialUnbondings[i] + expectedUnbonding := expectedUnbondings[i] + finalUnbonding := finalUnbondings[i] + + s.Require().Equal(expectedUnbonding.Validator, finalUnbonding.Validator, + "validator address of output message - %d", i) + s.Require().Equal(expectedUnbonding.Amount.Int64(), finalUnbonding.Amount.Int64(), + "%s - validator final unbond amount should have increased by %d from %d", + expectedUnbonding.Validator, validator.expectedDelegationIncrease, initialUnbonding.Amount.Int64()) + } +} + +func (s *KeeperTestSuite) TestConsolidateUnbondingMessages_Failure() { + batchSize := 4 + totalUnbondAmount := sdkmath.NewInt(1000) + + // Setup the capacities such that after the first pass, there is 1 token remaining amongst the batch + capacities := []keeper.ValidatorUnbondCapacity{ + {ValidatorAddress: "val1", Capacity: sdkmath.NewInt(100), CurrentDelegation: sdkmath.NewInt(100 + 1)}, // extra token + {ValidatorAddress: "val2", Capacity: sdkmath.NewInt(100), CurrentDelegation: sdkmath.NewInt(100)}, + {ValidatorAddress: "val3", Capacity: sdkmath.NewInt(100), CurrentDelegation: sdkmath.NewInt(100)}, + {ValidatorAddress: "val4", Capacity: sdkmath.NewInt(100), CurrentDelegation: sdkmath.NewInt(100)}, + + // Excess + {ValidatorAddress: "val5", Capacity: sdkmath.NewInt(600), CurrentDelegation: sdkmath.NewInt(600)}, + } + + // Create the unbondings such that they align with the above and each validtor unbonds their full amount + unbondings := []*types.SplitDelegation{} + for _, capacitiy := range capacities { + unbondings = append(unbondings, &types.SplitDelegation{ + Validator: capacitiy.ValidatorAddress, + Amount: capacitiy.Capacity, + }) + } + + // Call consolidate - it should fail because there is not enough remaining delegation + // on each validator to cover the excess + _, err := s.App.StakeibcKeeper.ConsolidateUnbondingMessages(totalUnbondAmount, unbondings, capacities, batchSize) + s.Require().ErrorContains(err, "not enough exisiting delegation in the batch to cover the excess") +}