diff --git a/oracle/oracle.go b/oracle/oracle.go index 3b96fef..e4a83cf 100644 --- a/oracle/oracle.go +++ b/oracle/oracle.go @@ -4,6 +4,7 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" + "sort" "strconv" "fmt" @@ -300,7 +301,16 @@ func (or *Oracle) ValidatorCleanup(slot uint64) error { // Iterate over all validators. If two or more validators exit or get slashed in the same slot, // this cleanup will eventually set both of their pending rewards to 0 and share them among the pool rewardsToDistribute := big.NewInt(0) - for _, validator := range validatorInfo { + + keys := make([]int, 0, len(validatorInfo)) + for k := range validatorInfo { + keys = append(keys, int(k)) + } + sort.Ints(keys) + + for _, k := range keys { + validator := validatorInfo[phase0.ValidatorIndex(k)] + // If a validator is subscribed but not active onchain, we have to unsubscribe it and treat it as a ban: // this means setting the validator rewards to 0 and sharing them among the pool idx := uint64(validator.Index) @@ -316,6 +326,7 @@ func (or *Oracle) ValidatorCleanup(slot uint64) error { }).Info("Cleaning up validator") or.advanceStateMachine(idx, Unsubscribe) + // sourceToTarget map will only be populated if the oracle is past the Electra fork and there are pending consolidations if targetIdx, ok := sourceToTarget[idx]; ok { log.WithFields(log.Fields{ diff --git a/oracle/oracle_test.go b/oracle/oracle_test.go index 1fe7140..a02b85f 100644 --- a/oracle/oracle_test.go +++ b/oracle/oracle_test.go @@ -3330,6 +3330,107 @@ func Test_ValidatorCleanup_Consolidations(t *testing.T) { require.Equal(t, NotSubscribed, oracle.state.Validators[80].ValidatorStatus) }) + t.Run("Test8: Consolidation from not subscribed validators", func(t *testing.T) { + oracle := NewOracle(&Config{Network: "mainnet"}) + oracle.state.Validators[80] = &ValidatorInfo{PendingRewardsWei: big.NewInt(999), ValidatorStatus: NotSubscribed} + oracle.state.Validators[81] = &ValidatorInfo{PendingRewardsWei: big.NewInt(0), ValidatorStatus: NotSubscribed} + + oracle.SetGetSetOfValidatorsFunc(func(_ []phase0.ValidatorIndex, _ string, _ ...retry.Option) (map[phase0.ValidatorIndex]*v1.Validator, error) { + return map[phase0.ValidatorIndex]*v1.Validator{ + 50: {Index: 80, Status: v1.ValidatorStateExitedUnslashed}, + 60: {Index: 81, Status: v1.ValidatorStateExitedUnslashed}, + }, nil + }) + oracle.GetPendingConsolidationsFunc(func(stateID string, opts ...retry.Option) (*PendingConsolidationsResponse, error) { + return &PendingConsolidationsResponse{Data: []PendingConsolidation{ + {SourceIndex: 80, TargetIndex: 81}, + }}, nil + }) + + err := oracle.ValidatorCleanup(mainnetElectra + 1) + require.NoError(t, err) + require.Equal(t, big.NewInt(0), oracle.state.Validators[80].PendingRewardsWei) + require.Equal(t, big.NewInt(0), oracle.state.Validators[81].PendingRewardsWei) + require.Equal(t, big.NewInt(999), oracle.state.PoolAccumulatedFees) + require.Equal(t, NotSubscribed, oracle.state.Validators[80].ValidatorStatus) + }) + + t.Run("Test8: Consolidation from banned to not subscribed", func(t *testing.T) { + oracle := NewOracle(&Config{Network: "mainnet"}) + oracle.state.Validators[80] = &ValidatorInfo{PendingRewardsWei: big.NewInt(999), ValidatorStatus: Banned} + oracle.state.Validators[81] = &ValidatorInfo{PendingRewardsWei: big.NewInt(0), ValidatorStatus: NotSubscribed} + + oracle.SetGetSetOfValidatorsFunc(func(_ []phase0.ValidatorIndex, _ string, _ ...retry.Option) (map[phase0.ValidatorIndex]*v1.Validator, error) { + return map[phase0.ValidatorIndex]*v1.Validator{ + 50: {Index: 80, Status: v1.ValidatorStateExitedUnslashed}, + 60: {Index: 81, Status: v1.ValidatorStateExitedUnslashed}, + }, nil + }) + oracle.GetPendingConsolidationsFunc(func(stateID string, opts ...retry.Option) (*PendingConsolidationsResponse, error) { + return &PendingConsolidationsResponse{Data: []PendingConsolidation{ + {SourceIndex: 80, TargetIndex: 81}, + }}, nil + }) + + err := oracle.ValidatorCleanup(mainnetElectra + 1) + require.NoError(t, err) + require.Equal(t, big.NewInt(0), oracle.state.Validators[80].PendingRewardsWei) + require.Equal(t, big.NewInt(999), oracle.state.PoolAccumulatedFees) + require.Equal(t, Banned, oracle.state.Validators[80].ValidatorStatus) + }) + + t.Run("Test9: Consolidation from notsubscribed to banned", func(t *testing.T) { + oracle := NewOracle(&Config{Network: "mainnet"}) + oracle.state.Validators[80] = &ValidatorInfo{PendingRewardsWei: big.NewInt(999), ValidatorStatus: Banned} + oracle.state.Validators[85] = &ValidatorInfo{PendingRewardsWei: big.NewInt(0), ValidatorStatus: NotSubscribed} + + oracle.SetGetSetOfValidatorsFunc(func(_ []phase0.ValidatorIndex, _ string, _ ...retry.Option) (map[phase0.ValidatorIndex]*v1.Validator, error) { + return map[phase0.ValidatorIndex]*v1.Validator{ + 50: {Index: 80, Status: v1.ValidatorStateExitedUnslashed}, + 60: {Index: 85, Status: v1.ValidatorStateExitedUnslashed}, + }, nil + }) + oracle.GetPendingConsolidationsFunc(func(stateID string, opts ...retry.Option) (*PendingConsolidationsResponse, error) { + return &PendingConsolidationsResponse{Data: []PendingConsolidation{ + {SourceIndex: 80, TargetIndex: 85}, + }}, nil + }) + + err := oracle.ValidatorCleanup(mainnetElectra + 1) + require.NoError(t, err) + require.Equal(t, big.NewInt(0), oracle.state.Validators[80].PendingRewardsWei) + require.Equal(t, big.NewInt(0), oracle.state.Validators[85].PendingRewardsWei) + require.Equal(t, big.NewInt(999), oracle.state.PoolAccumulatedFees) + require.Equal(t, Banned, oracle.state.Validators[80].ValidatorStatus) + require.Equal(t, NotSubscribed, oracle.state.Validators[85].ValidatorStatus) + }) + + t.Run("Test10: Consolidation from Banned to Banned", func(t *testing.T) { + oracle := NewOracle(&Config{Network: "mainnet"}) + oracle.state.Validators[80] = &ValidatorInfo{PendingRewardsWei: big.NewInt(999), ValidatorStatus: Banned} + oracle.state.Validators[85] = &ValidatorInfo{PendingRewardsWei: big.NewInt(0), ValidatorStatus: Banned} + + oracle.SetGetSetOfValidatorsFunc(func(_ []phase0.ValidatorIndex, _ string, _ ...retry.Option) (map[phase0.ValidatorIndex]*v1.Validator, error) { + return map[phase0.ValidatorIndex]*v1.Validator{ + 50: {Index: 80, Status: v1.ValidatorStateExitedUnslashed}, + 60: {Index: 85, Status: v1.ValidatorStateActiveExiting}, + }, nil + }) + oracle.GetPendingConsolidationsFunc(func(stateID string, opts ...retry.Option) (*PendingConsolidationsResponse, error) { + return &PendingConsolidationsResponse{Data: []PendingConsolidation{ + {SourceIndex: 80, TargetIndex: 85}, + }}, nil + }) + + err := oracle.ValidatorCleanup(mainnetElectra + 1) + require.NoError(t, err) + require.Equal(t, big.NewInt(0), oracle.state.Validators[80].PendingRewardsWei) + require.Equal(t, big.NewInt(0), oracle.state.Validators[85].PendingRewardsWei) + require.Equal(t, big.NewInt(999), oracle.state.PoolAccumulatedFees) + require.Equal(t, Banned, oracle.state.Validators[80].ValidatorStatus) + require.Equal(t, Banned, oracle.state.Validators[85].ValidatorStatus) + }) + } func Test_increaseValidatorPendingRewards(t *testing.T) {