validator client: adding in get duties v2 (#15380)

* adding in get duties v2

* gaz

* missed definition

* removing comment

* updating description
This commit is contained in:
james-prysm
2025-06-05 10:49:57 -05:00
committed by GitHub
parent 265d84569c
commit 8c324cc491
6 changed files with 138 additions and 8 deletions

View File

@@ -0,0 +1,3 @@
### Added
- Added feature flag for validator client to use get duties v2.

View File

@@ -50,6 +50,7 @@ type Flags struct {
EnableHistoricalSpaceRepresentation bool // EnableHistoricalSpaceRepresentation enables the saving of registry validators in separate buckets to save space
EnableBeaconRESTApi bool // EnableBeaconRESTApi enables experimental usage of the beacon REST API by the validator when querying a beacon node
EnableExperimentalAttestationPool bool // EnableExperimentalAttestationPool enables an experimental attestation pool design.
EnableDutiesV2 bool // EnableDutiesV2 sets validator client to use the get Duties V2 endpoint
// Logging related toggles.
DisableGRPCConnectionLogs bool // Disables logging when a new grpc client has connected.
EnableFullSSZDataLogging bool // Enables logging for full ssz data on rejected gossip messages
@@ -334,6 +335,10 @@ func ConfigureValidator(ctx *cli.Context) error {
logEnabled(EnableBeaconRESTApi)
cfg.EnableBeaconRESTApi = true
}
if ctx.Bool(EnableDutiesV2.Name) {
logEnabled(EnableDutiesV2)
cfg.EnableDutiesV2 = true
}
cfg.KeystoreImportDebounceInterval = ctx.Duration(dynamicKeyReloadDebounceInterval.Name)
Init(cfg)
return nil

View File

@@ -188,6 +188,12 @@ var (
Name: "blacklist-roots",
Usage: "A comma-separatted list of 0x-prefixed hexstrings. Declares blocks with the given blockroots to be invalid. It downscores peers that send these blocks.",
}
// EnableDutiesV2 sets the validator client to use the get duties v2 grpc endpoint
EnableDutiesV2 = &cli.BoolFlag{
Name: "enable-duties-v2",
Usage: "Forces use of get duties v2 endpoint.",
}
)
// devModeFlags holds list of flags that are set when development mode is on.
@@ -208,6 +214,7 @@ var ValidatorFlags = append(deprecatedFlags, []cli.Flag{
EnableMinimalSlashingProtection,
enableDoppelGangerProtection,
EnableBeaconRESTApi,
EnableDutiesV2,
}...)
// E2EValidatorFlags contains a list of the validator feature flags to be tested in E2E.

View File

@@ -17,6 +17,7 @@ go_library(
"//api/server/structs:go_default_library",
"//beacon-chain/rpc/eth/helpers:go_default_library",
"//beacon-chain/state/state-native:go_default_library",
"//config/features:go_default_library",
"//consensus-types/primitives:go_default_library",
"//consensus-types/validator:go_default_library",
"//encoding/bytesutil:go_default_library",
@@ -29,6 +30,8 @@ go_library(
"@com_github_pkg_errors//:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
"@org_golang_google_grpc//:go_default_library",
"@org_golang_google_grpc//codes:go_default_library",
"@org_golang_google_grpc//status:go_default_library",
],
)

View File

@@ -8,6 +8,7 @@ import (
"github.com/OffchainLabs/prysm/v6/api/client"
eventClient "github.com/OffchainLabs/prysm/v6/api/client/event"
"github.com/OffchainLabs/prysm/v6/api/server/structs"
"github.com/OffchainLabs/prysm/v6/config/features"
"github.com/OffchainLabs/prysm/v6/consensus-types/primitives"
"github.com/OffchainLabs/prysm/v6/encoding/bytesutil"
"github.com/OffchainLabs/prysm/v6/monitoring/tracing/trace"
@@ -18,6 +19,8 @@ import (
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type grpcValidatorClient struct {
@@ -26,16 +29,33 @@ type grpcValidatorClient struct {
}
func (c *grpcValidatorClient) Duties(ctx context.Context, in *ethpb.DutiesRequest) (*ethpb.ValidatorDutiesContainer, error) {
dutiesResponse, err := c.beaconNodeValidatorClient.GetDuties(ctx, in)
if features.Get().EnableDutiesV2 {
dutiesResponse, err := c.beaconNodeValidatorClient.GetDutiesV2(ctx, in)
if err != nil {
return nil, err
if status.Code(err) == codes.Unimplemented {
log.Warn("beaconNodeValidatorClient.GetDutiesV2() returned status code unavailable, falling back to GetDuties")
return c.getDuties(ctx, in)
}
return toValidatorDutiesContainer(dutiesResponse)
return nil, errors.Wrap(
client.ErrConnectionIssue,
errors.Wrap(err, "getDutiesV2").Error(),
)
}
return toValidatorDutiesContainerV2(dutiesResponse)
}
return c.getDuties(ctx, in)
}
func (c *grpcValidatorClient) DutiesV2(ctx context.Context, in *ethpb.DutiesRequest) (*ethpb.ValidatorDutiesContainer, error) {
// TODO: update to v2 get duties in separate PR, used to satisfy interface
return nil, errors.New("not implemented")
// getDuties is calling the v1 of get duties
func (c *grpcValidatorClient) getDuties(ctx context.Context, in *ethpb.DutiesRequest) (*ethpb.ValidatorDutiesContainer, error) {
dutiesResponse, err := c.beaconNodeValidatorClient.GetDuties(ctx, in)
if err != nil {
return nil, errors.Wrap(
client.ErrConnectionIssue,
errors.Wrap(err, "getDuties").Error(),
)
}
return toValidatorDutiesContainer(dutiesResponse)
}
func toValidatorDutiesContainer(dutiesResponse *ethpb.DutiesResponse) (*ethpb.ValidatorDutiesContainer, error) {
@@ -87,6 +107,46 @@ func toValidatorDuty(duty *ethpb.DutiesResponse_Duty) (*ethpb.ValidatorDuty, err
}, nil
}
func toValidatorDutiesContainerV2(dutiesResponse *ethpb.DutiesV2Response) (*ethpb.ValidatorDutiesContainer, error) {
currentDuties := make([]*ethpb.ValidatorDuty, len(dutiesResponse.CurrentEpochDuties))
for i, cd := range dutiesResponse.CurrentEpochDuties {
duty, err := toValidatorDutyV2(cd)
if err != nil {
return nil, err
}
currentDuties[i] = duty
}
nextDuties := make([]*ethpb.ValidatorDuty, len(dutiesResponse.NextEpochDuties))
for i, nd := range dutiesResponse.NextEpochDuties {
duty, err := toValidatorDutyV2(nd)
if err != nil {
return nil, err
}
nextDuties[i] = duty
}
return &ethpb.ValidatorDutiesContainer{
PrevDependentRoot: dutiesResponse.PreviousDutyDependentRoot,
CurrDependentRoot: dutiesResponse.CurrentDutyDependentRoot,
CurrentEpochDuties: currentDuties,
NextEpochDuties: nextDuties,
}, nil
}
func toValidatorDutyV2(duty *ethpb.DutiesV2Response_Duty) (*ethpb.ValidatorDuty, error) {
return &ethpb.ValidatorDuty{
CommitteeLength: duty.CommitteeLength,
CommitteeIndex: duty.CommitteeIndex,
CommitteesAtSlot: duty.CommitteesAtSlot, // GRPC doesn't use this value though
ValidatorCommitteeIndex: duty.ValidatorCommitteeIndex,
AttesterSlot: duty.AttesterSlot,
ProposerSlots: duty.ProposerSlots,
PublicKey: bytesutil.SafeCopyBytes(duty.PublicKey),
Status: duty.Status,
ValidatorIndex: duty.ValidatorIndex,
IsSyncCommittee: duty.IsSyncCommittee,
}, nil
}
func (c *grpcValidatorClient) CheckDoppelGanger(ctx context.Context, in *ethpb.DoppelGangerRequest) (*ethpb.DoppelGangerResponse, error) {
return c.beaconNodeValidatorClient.CheckDoppelGanger(ctx, in)
}

View File

@@ -19,7 +19,6 @@ import (
"google.golang.org/protobuf/types/known/emptypb"
)
// toValidatorDutiesContainer is assumed to be available from your package, returning a *v1alpha1.ValidatorDutiesContainer.
func TestToValidatorDutiesContainer_HappyPath(t *testing.T) {
// Create a mock DutiesResponse with current and next duties.
dutiesResp := &eth.DutiesResponse{
@@ -71,6 +70,59 @@ func TestToValidatorDutiesContainer_HappyPath(t *testing.T) {
assert.DeepEqual(t, firstNextDuty.ProposerSlots, expectedNextDuty.ProposerSlots)
}
func TestToValidatorDutiesContainerV2_HappyPath(t *testing.T) {
// Create a mock DutiesResponse with current and next duties.
dutiesResp := &eth.DutiesV2Response{
CurrentEpochDuties: []*eth.DutiesV2Response_Duty{
{
CommitteeLength: 2,
CommitteeIndex: 4,
ValidatorCommitteeIndex: 1,
AttesterSlot: 200,
ProposerSlots: []primitives.Slot{400},
PublicKey: []byte{0xAA, 0xBB},
Status: eth.ValidatorStatus_ACTIVE,
ValidatorIndex: 101,
IsSyncCommittee: false,
CommitteesAtSlot: 2,
},
},
NextEpochDuties: []*eth.DutiesV2Response_Duty{
{
CommitteeLength: 2,
CommitteeIndex: 8,
ValidatorCommitteeIndex: 1,
AttesterSlot: 600,
ProposerSlots: []primitives.Slot{700, 701},
PublicKey: []byte{0xCC, 0xDD},
Status: eth.ValidatorStatus_ACTIVE,
ValidatorIndex: 301,
IsSyncCommittee: true,
CommitteesAtSlot: 3,
},
},
}
gotContainer, err := toValidatorDutiesContainerV2(dutiesResp)
require.NoError(t, err)
// Validate we have the correct number of duties in current and next epochs.
assert.Equal(t, len(gotContainer.CurrentEpochDuties), len(dutiesResp.CurrentEpochDuties))
assert.Equal(t, len(gotContainer.NextEpochDuties), len(dutiesResp.NextEpochDuties))
firstCurrentDuty := gotContainer.CurrentEpochDuties[0]
expectedCurrentDuty := dutiesResp.CurrentEpochDuties[0]
assert.DeepEqual(t, firstCurrentDuty.PublicKey, expectedCurrentDuty.PublicKey)
assert.Equal(t, firstCurrentDuty.ValidatorIndex, expectedCurrentDuty.ValidatorIndex)
assert.DeepEqual(t, firstCurrentDuty.ProposerSlots, expectedCurrentDuty.ProposerSlots)
firstNextDuty := gotContainer.NextEpochDuties[0]
expectedNextDuty := dutiesResp.NextEpochDuties[0]
assert.DeepEqual(t, firstNextDuty.PublicKey, expectedNextDuty.PublicKey)
assert.Equal(t, firstNextDuty.ValidatorIndex, expectedNextDuty.ValidatorIndex)
assert.DeepEqual(t, firstNextDuty.ProposerSlots, expectedNextDuty.ProposerSlots)
}
func TestWaitForChainStart_StreamSetupFails(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()