ETH2 APIs: Implement Validator Status support (#8940)

* Update protos for new changes

* Begin implementing validator status

* Add support for validator status to state/validators

* Fix nil pointer
This commit is contained in:
Ivan Martinez
2021-05-26 00:24:29 -04:00
committed by GitHub
parent 2e322bdf6d
commit d5ad15ed80
2 changed files with 539 additions and 4 deletions

View File

@@ -30,11 +30,13 @@ func (bs *Server) GetValidator(ctx context.Context, req *ethpb.StateValidatorReq
if err != nil {
return nil, status.Errorf(codes.Internal, "Could not get validator container: %v", err)
}
if len(valContainer) == 0 {
return nil, status.Error(codes.NotFound, "Could not find validator")
}
return &ethpb.StateValidatorResponse{Data: valContainer[0]}, nil
}
// ListValidators returns filterable list of validators with their balance, status and index.
// TODO(#8901): missing status support.
func (bs *Server) ListValidators(ctx context.Context, req *ethpb.StateValidatorsRequest) (*ethpb.StateValidatorsResponse, error) {
state, err := bs.StateFetcher.State(ctx, req.StateId)
if err != nil {
@@ -46,7 +48,24 @@ func (bs *Server) ListValidators(ctx context.Context, req *ethpb.StateValidators
return nil, status.Errorf(codes.Internal, "Could not get validator container: %v", err)
}
return &ethpb.StateValidatorsResponse{Data: valContainers}, nil
if len(req.Status) == 0 {
return &ethpb.StateValidatorsResponse{Data: valContainers}, nil
}
filterStatus := make(map[ethpb.ValidatorStatus]bool, len(req.Status))
for _, ss := range req.Status {
filterStatus[ss] = true
}
epoch := helpers.SlotToEpoch(state.Slot())
filteredVals := make([]*ethpb.ValidatorContainer, 0, len(valContainers))
for _, vc := range valContainers {
valStatus := validatorStatus(vc.Validator, epoch)
valSubStatus := validatorSubStatus(vc.Validator, epoch)
if filterStatus[valStatus] || filterStatus[valSubStatus] {
filteredVals = append(filteredVals, vc)
}
}
return &ethpb.StateValidatorsResponse{Data: filteredVals}, nil
}
// ListValidatorBalances returns a filterable list of validator balances.
@@ -123,16 +142,19 @@ func (bs *Server) ListCommittees(ctx context.Context, req *ethpb.StateCommittees
// This function returns the validator object based on the passed in ID. The validator ID could be its public key,
// or its index.
func valContainersByRequestIds(state iface.BeaconState, validatorIds [][]byte) ([]*ethpb.ValidatorContainer, error) {
epoch := helpers.SlotToEpoch(state.Slot())
allValidators := state.Validators()
allBalances := state.Balances()
var valContainers []*ethpb.ValidatorContainer
if len(validatorIds) == 0 {
valContainers = make([]*ethpb.ValidatorContainer, len(allValidators))
for i, validator := range allValidators {
v1Validator := migration.V1Alpha1ValidatorToV1(validator)
valContainers[i] = &ethpb.ValidatorContainer{
Index: types.ValidatorIndex(i),
Balance: allBalances[i],
Validator: migration.V1Alpha1ValidatorToV1(validator),
Status: validatorSubStatus(v1Validator, epoch),
Validator: v1Validator,
}
}
} else {
@@ -152,12 +174,71 @@ func valContainersByRequestIds(state iface.BeaconState, validatorIds [][]byte) (
}
valIndex = types.ValidatorIndex(index)
}
v1Validator := migration.V1Alpha1ValidatorToV1(allValidators[valIndex])
valContainers[i] = &ethpb.ValidatorContainer{
Index: valIndex,
Balance: allBalances[valIndex],
Validator: migration.V1Alpha1ValidatorToV1(allValidators[valIndex]),
Status: validatorSubStatus(v1Validator, epoch),
Validator: v1Validator,
}
}
}
return valContainers, nil
}
func validatorStatus(validator *ethpb.Validator, epoch types.Epoch) ethpb.ValidatorStatus {
switch validatorSubStatus(validator, epoch) {
case ethpb.ValidatorStatus_PENDING_INITIALIZED, ethpb.ValidatorStatus_PENDING_QUEUED:
return ethpb.ValidatorStatus_PENDING
case ethpb.ValidatorStatus_ACTIVE_ONGOING, ethpb.ValidatorStatus_ACTIVE_SLASHED, ethpb.ValidatorStatus_ACTIVE_EXITING:
return ethpb.ValidatorStatus_ACTIVE
case ethpb.ValidatorStatus_EXITED_UNSLASHED, ethpb.ValidatorStatus_EXITED_SLASHED:
return ethpb.ValidatorStatus_EXITED
case ethpb.ValidatorStatus_WITHDRAWAL_POSSIBLE, ethpb.ValidatorStatus_WITHDRAWAL_DONE:
return ethpb.ValidatorStatus_WITHDRAWAL
}
return 0
}
func validatorSubStatus(validator *ethpb.Validator, epoch types.Epoch) ethpb.ValidatorStatus {
farFutureEpoch := params.BeaconConfig().FarFutureEpoch
// Pending.
if validator.ActivationEpoch > epoch {
if validator.ActivationEligibilityEpoch == farFutureEpoch {
return ethpb.ValidatorStatus_PENDING_INITIALIZED
} else if validator.ActivationEligibilityEpoch < farFutureEpoch && validator.ActivationEpoch > epoch {
return ethpb.ValidatorStatus_PENDING_QUEUED
}
}
// Active.
if validator.ActivationEpoch <= epoch && epoch < validator.ExitEpoch {
if validator.ExitEpoch == farFutureEpoch {
return ethpb.ValidatorStatus_ACTIVE_ONGOING
} else if validator.ExitEpoch < farFutureEpoch {
if validator.Slashed {
return ethpb.ValidatorStatus_ACTIVE_SLASHED
}
return ethpb.ValidatorStatus_ACTIVE_EXITING
}
}
// Exited.
if validator.ExitEpoch <= epoch && epoch < validator.WithdrawableEpoch {
if validator.Slashed {
return ethpb.ValidatorStatus_EXITED_SLASHED
}
return ethpb.ValidatorStatus_EXITED_UNSLASHED
}
if validator.WithdrawableEpoch <= epoch {
if validator.EffectiveBalance != 0 {
return ethpb.ValidatorStatus_WITHDRAWAL_POSSIBLE
} else {
return ethpb.ValidatorStatus_WITHDRAWAL_DONE
}
}
return 0
}

View File

@@ -6,6 +6,8 @@ import (
"strings"
"testing"
ethpb_alpha "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1"
"github.com/ethereum/go-ethereum/common/hexutil"
types "github.com/prysmaticlabs/eth2-types"
ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1"
@@ -112,6 +114,9 @@ func TestListValidators(t *testing.T) {
})
require.NoError(t, err)
assert.Equal(t, len(resp.Data), 8192)
for _, val := range resp.Data {
assert.Equal(t, ethpb.ValidatorStatus_ACTIVE_ONGOING, val.Status)
}
})
t.Run("Head List Validators by index", func(t *testing.T) {
@@ -130,6 +135,7 @@ func TestListValidators(t *testing.T) {
require.NoError(t, err)
for i, val := range resp.Data {
assert.Equal(t, idNums[i], val.Index)
assert.Equal(t, ethpb.ValidatorStatus_ACTIVE_ONGOING, val.Status)
}
})
@@ -153,6 +159,7 @@ func TestListValidators(t *testing.T) {
for i, val := range resp.Data {
assert.Equal(t, idNums[i], val.Index)
assert.Equal(t, true, bytes.Equal(pubKeys[i], val.Validator.Pubkey))
assert.Equal(t, ethpb.ValidatorStatus_ACTIVE_ONGOING, val.Status)
}
})
@@ -178,6 +185,7 @@ func TestListValidators(t *testing.T) {
for i, val := range resp.Data {
assert.Equal(t, idNums[i], val.Index)
assert.Equal(t, true, bytes.Equal(pubkeys[i], val.Validator.Pubkey))
assert.Equal(t, ethpb.ValidatorStatus_ACTIVE_ONGOING, val.Status)
}
})
@@ -204,6 +212,208 @@ func TestListValidators(t *testing.T) {
})
}
func TestListValidators_Status(t *testing.T) {
ctx := context.Background()
var state iface.BeaconState
state, _ = testutil.DeterministicGenesisState(t, 8192)
farFutureEpoch := params.BeaconConfig().FarFutureEpoch
validators := []*ethpb_alpha.Validator{
{
ActivationEpoch: farFutureEpoch,
ActivationEligibilityEpoch: farFutureEpoch,
},
{
ActivationEpoch: 0,
ExitEpoch: farFutureEpoch,
},
{
ActivationEpoch: 0,
ExitEpoch: 30,
Slashed: true,
},
{
ActivationEpoch: 3,
ExitEpoch: 30,
Slashed: false,
},
{
ActivationEpoch: 3,
ExitEpoch: 30,
WithdrawableEpoch: 40,
Slashed: true,
},
{
ActivationEpoch: 3,
ExitEpoch: 30,
WithdrawableEpoch: 40,
Slashed: false,
},
{
ActivationEpoch: 3,
ExitEpoch: 30,
WithdrawableEpoch: 40,
EffectiveBalance: params.BeaconConfig().MaxEffectiveBalance,
Slashed: false,
},
{
ActivationEpoch: 3,
ExitEpoch: 30,
WithdrawableEpoch: 40,
EffectiveBalance: 0,
Slashed: false,
},
}
for _, validator := range validators {
require.NoError(t, state.AppendValidator(validator))
require.NoError(t, state.AppendBalance(params.BeaconConfig().MaxEffectiveBalance))
}
t.Run("Head List All ACTIVE Validators", func(t *testing.T) {
s := Server{
StateFetcher: statefetcher.StateProvider{
ChainInfoFetcher: &chainMock.ChainService{State: state},
},
}
resp, err := s.ListValidators(ctx, &ethpb.StateValidatorsRequest{
StateId: []byte("head"),
Status: []ethpb.ValidatorStatus{ethpb.ValidatorStatus_ACTIVE},
})
require.NoError(t, err)
assert.Equal(t, len(resp.Data), 8192+2 /* 2 active */)
for _, datum := range resp.Data {
status := validatorStatus(datum.Validator, 0)
require.Equal(
t,
true,
status == ethpb.ValidatorStatus_ACTIVE,
)
require.Equal(
t,
true,
datum.Status == ethpb.ValidatorStatus_ACTIVE_ONGOING ||
datum.Status == ethpb.ValidatorStatus_ACTIVE_EXITING ||
datum.Status == ethpb.ValidatorStatus_ACTIVE_SLASHED,
)
}
})
t.Run("Head List All ACTIVE_ONGOING Validators", func(t *testing.T) {
s := Server{
StateFetcher: statefetcher.StateProvider{
ChainInfoFetcher: &chainMock.ChainService{State: state},
},
}
resp, err := s.ListValidators(ctx, &ethpb.StateValidatorsRequest{
StateId: []byte("head"),
Status: []ethpb.ValidatorStatus{ethpb.ValidatorStatus_ACTIVE_ONGOING},
})
require.NoError(t, err)
assert.Equal(t, len(resp.Data), 8192+1 /* 1 active_ongoing */)
for _, datum := range resp.Data {
status := validatorSubStatus(datum.Validator, 0)
require.Equal(
t,
true,
status == ethpb.ValidatorStatus_ACTIVE_ONGOING,
)
require.Equal(
t,
true,
datum.Status == ethpb.ValidatorStatus_ACTIVE_ONGOING,
)
}
})
require.NoError(t, state.SetSlot(params.BeaconConfig().SlotsPerEpoch*35))
t.Run("Head List All EXITED Validators", func(t *testing.T) {
s := Server{
StateFetcher: statefetcher.StateProvider{
ChainInfoFetcher: &chainMock.ChainService{State: state},
},
}
resp, err := s.ListValidators(ctx, &ethpb.StateValidatorsRequest{
StateId: []byte("head"),
Status: []ethpb.ValidatorStatus{ethpb.ValidatorStatus_EXITED},
})
require.NoError(t, err)
assert.Equal(t, 4 /* 4 exited */, len(resp.Data))
for _, datum := range resp.Data {
status := validatorStatus(datum.Validator, 35)
require.Equal(
t,
true,
status == ethpb.ValidatorStatus_EXITED,
)
require.Equal(
t,
true,
datum.Status == ethpb.ValidatorStatus_EXITED_UNSLASHED || datum.Status == ethpb.ValidatorStatus_EXITED_SLASHED,
)
}
})
t.Run("Head List All PENDING_INITIALIZED and EXITED_UNSLASHED Validators", func(t *testing.T) {
s := Server{
StateFetcher: statefetcher.StateProvider{
ChainInfoFetcher: &chainMock.ChainService{State: state},
},
}
resp, err := s.ListValidators(ctx, &ethpb.StateValidatorsRequest{
StateId: []byte("head"),
Status: []ethpb.ValidatorStatus{ethpb.ValidatorStatus_PENDING_INITIALIZED, ethpb.ValidatorStatus_EXITED_UNSLASHED},
})
require.NoError(t, err)
assert.Equal(t, 4 /* 4 exited */, len(resp.Data))
for _, datum := range resp.Data {
status := validatorSubStatus(datum.Validator, 35)
require.Equal(
t,
true,
status == ethpb.ValidatorStatus_PENDING_INITIALIZED || status == ethpb.ValidatorStatus_EXITED_UNSLASHED,
)
require.Equal(
t,
true,
datum.Status == ethpb.ValidatorStatus_PENDING_INITIALIZED || datum.Status == ethpb.ValidatorStatus_EXITED_UNSLASHED,
)
}
})
t.Run("Head List All PENDING and EXITED Validators", func(t *testing.T) {
s := Server{
StateFetcher: statefetcher.StateProvider{
ChainInfoFetcher: &chainMock.ChainService{State: state},
},
}
resp, err := s.ListValidators(ctx, &ethpb.StateValidatorsRequest{
StateId: []byte("head"),
Status: []ethpb.ValidatorStatus{ethpb.ValidatorStatus_PENDING, ethpb.ValidatorStatus_EXITED_SLASHED},
})
require.NoError(t, err)
assert.Equal(t, 2 /* 1 pending, 1 exited */, len(resp.Data))
for _, datum := range resp.Data {
status := validatorStatus(datum.Validator, 35)
subStatus := validatorSubStatus(datum.Validator, 35)
require.Equal(
t,
true,
status == ethpb.ValidatorStatus_PENDING || subStatus == ethpb.ValidatorStatus_EXITED_SLASHED,
)
require.Equal(
t,
true,
datum.Status == ethpb.ValidatorStatus_PENDING_INITIALIZED || datum.Status == ethpb.ValidatorStatus_EXITED_SLASHED,
)
}
})
}
func TestListValidatorBalances(t *testing.T) {
ctx := context.Background()
@@ -431,3 +641,247 @@ func TestListCommittees(t *testing.T) {
require.ErrorContains(t, "invalid state ID: foo", err)
})
}
func Test_validatorStatus(t *testing.T) {
farFutureEpoch := params.BeaconConfig().FarFutureEpoch
type args struct {
validator *ethpb.Validator
epoch types.Epoch
}
tests := []struct {
name string
args args
want ethpb.ValidatorStatus
}{
{
name: "pending initialized",
args: args{
validator: &ethpb.Validator{
ActivationEpoch: farFutureEpoch,
ActivationEligibilityEpoch: farFutureEpoch,
},
epoch: types.Epoch(5),
},
want: ethpb.ValidatorStatus_PENDING,
},
{
name: "active ongoing",
args: args{
validator: &ethpb.Validator{
ActivationEpoch: 3,
ExitEpoch: farFutureEpoch,
},
epoch: types.Epoch(5),
},
want: ethpb.ValidatorStatus_ACTIVE,
},
{
name: "active slashed",
args: args{
validator: &ethpb.Validator{
ActivationEpoch: 3,
ExitEpoch: 30,
Slashed: true,
},
epoch: types.Epoch(5),
},
want: ethpb.ValidatorStatus_ACTIVE,
},
{
name: "active exiting",
args: args{
validator: &ethpb.Validator{
ActivationEpoch: 3,
ExitEpoch: 30,
Slashed: false,
},
epoch: types.Epoch(5),
},
want: ethpb.ValidatorStatus_ACTIVE,
},
{
name: "exited slashed",
args: args{
validator: &ethpb.Validator{
ActivationEpoch: 3,
ExitEpoch: 30,
WithdrawableEpoch: 40,
Slashed: true,
},
epoch: types.Epoch(35),
},
want: ethpb.ValidatorStatus_EXITED,
},
{
name: "exited unslashed",
args: args{
validator: &ethpb.Validator{
ActivationEpoch: 3,
ExitEpoch: 30,
WithdrawableEpoch: 40,
Slashed: false,
},
epoch: types.Epoch(35),
},
want: ethpb.ValidatorStatus_EXITED,
},
{
name: "withdrawal possible",
args: args{
validator: &ethpb.Validator{
ActivationEpoch: 3,
ExitEpoch: 30,
WithdrawableEpoch: 40,
EffectiveBalance: params.BeaconConfig().MaxEffectiveBalance,
Slashed: false,
},
epoch: types.Epoch(45),
},
want: ethpb.ValidatorStatus_WITHDRAWAL,
},
{
name: "withdrawal done",
args: args{
validator: &ethpb.Validator{
ActivationEpoch: 3,
ExitEpoch: 30,
WithdrawableEpoch: 40,
EffectiveBalance: 0,
Slashed: false,
},
epoch: types.Epoch(45),
},
want: ethpb.ValidatorStatus_WITHDRAWAL,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := validatorStatus(tt.args.validator, tt.args.epoch); got != tt.want {
t.Errorf("validatorStatus() = %v, want %v", got, tt.want)
}
})
}
}
func Test_validatorSubStatus(t *testing.T) {
farFutureEpoch := params.BeaconConfig().FarFutureEpoch
type args struct {
validator *ethpb.Validator
epoch types.Epoch
}
tests := []struct {
name string
args args
want ethpb.ValidatorStatus
}{
{
name: "pending initialized",
args: args{
validator: &ethpb.Validator{
ActivationEpoch: farFutureEpoch,
ActivationEligibilityEpoch: farFutureEpoch,
},
epoch: types.Epoch(5),
},
want: ethpb.ValidatorStatus_PENDING_INITIALIZED,
},
{
name: "active ongoing",
args: args{
validator: &ethpb.Validator{
ActivationEpoch: 3,
ExitEpoch: farFutureEpoch,
},
epoch: types.Epoch(5),
},
want: ethpb.ValidatorStatus_ACTIVE_ONGOING,
},
{
name: "active slashed",
args: args{
validator: &ethpb.Validator{
ActivationEpoch: 3,
ExitEpoch: 30,
Slashed: true,
},
epoch: types.Epoch(5),
},
want: ethpb.ValidatorStatus_ACTIVE_SLASHED,
},
{
name: "active exiting",
args: args{
validator: &ethpb.Validator{
ActivationEpoch: 3,
ExitEpoch: 30,
Slashed: false,
},
epoch: types.Epoch(5),
},
want: ethpb.ValidatorStatus_ACTIVE_EXITING,
},
{
name: "exited slashed",
args: args{
validator: &ethpb.Validator{
ActivationEpoch: 3,
ExitEpoch: 30,
WithdrawableEpoch: 40,
Slashed: true,
},
epoch: types.Epoch(35),
},
want: ethpb.ValidatorStatus_EXITED_SLASHED,
},
{
name: "exited unslashed",
args: args{
validator: &ethpb.Validator{
ActivationEpoch: 3,
ExitEpoch: 30,
WithdrawableEpoch: 40,
Slashed: false,
},
epoch: types.Epoch(35),
},
want: ethpb.ValidatorStatus_EXITED_UNSLASHED,
},
{
name: "withdrawal possible",
args: args{
validator: &ethpb.Validator{
ActivationEpoch: 3,
ExitEpoch: 30,
WithdrawableEpoch: 40,
EffectiveBalance: params.BeaconConfig().MaxEffectiveBalance,
Slashed: false,
},
epoch: types.Epoch(45),
},
want: ethpb.ValidatorStatus_WITHDRAWAL_POSSIBLE,
},
{
name: "withdrawal done",
args: args{
validator: &ethpb.Validator{
ActivationEpoch: 3,
ExitEpoch: 30,
WithdrawableEpoch: 40,
EffectiveBalance: 0,
Slashed: false,
},
epoch: types.Epoch(45),
},
want: ethpb.ValidatorStatus_WITHDRAWAL_DONE,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := validatorSubStatus(tt.args.validator, tt.args.epoch); got != tt.want {
t.Errorf("validatorSubStatus() = %v, want %v", got, tt.want)
}
})
}
}