From 8c324cc491e053642f36f6878c74e4c06902d273 Mon Sep 17 00:00:00 2001 From: james-prysm <90280386+james-prysm@users.noreply.github.com> Date: Thu, 5 Jun 2025 10:49:57 -0500 Subject: [PATCH] validator client: adding in get duties v2 (#15380) * adding in get duties v2 * gaz * missed definition * removing comment * updating description --- changelog/james-prysm_validator-duties-v2.md | 3 + config/features/config.go | 5 ++ config/features/flags.go | 7 ++ validator/client/grpc-api/BUILD.bazel | 3 + .../client/grpc-api/grpc_validator_client.go | 74 +++++++++++++++++-- .../grpc-api/grpc_validator_client_test.go | 54 +++++++++++++- 6 files changed, 138 insertions(+), 8 deletions(-) create mode 100644 changelog/james-prysm_validator-duties-v2.md diff --git a/changelog/james-prysm_validator-duties-v2.md b/changelog/james-prysm_validator-duties-v2.md new file mode 100644 index 0000000000..900fe5b814 --- /dev/null +++ b/changelog/james-prysm_validator-duties-v2.md @@ -0,0 +1,3 @@ +### Added + +- Added feature flag for validator client to use get duties v2. \ No newline at end of file diff --git a/config/features/config.go b/config/features/config.go index 60a9fc7a7f..ec5254c1ce 100644 --- a/config/features/config.go +++ b/config/features/config.go @@ -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 diff --git a/config/features/flags.go b/config/features/flags.go index 721607c639..9c312c5bef 100644 --- a/config/features/flags.go +++ b/config/features/flags.go @@ -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. diff --git a/validator/client/grpc-api/BUILD.bazel b/validator/client/grpc-api/BUILD.bazel index aa40374b63..b0d0f2bbce 100644 --- a/validator/client/grpc-api/BUILD.bazel +++ b/validator/client/grpc-api/BUILD.bazel @@ -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", ], ) diff --git a/validator/client/grpc-api/grpc_validator_client.go b/validator/client/grpc-api/grpc_validator_client.go index cdae52c397..675d08cfe6 100644 --- a/validator/client/grpc-api/grpc_validator_client.go +++ b/validator/client/grpc-api/grpc_validator_client.go @@ -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 err != nil { - return nil, err + if features.Get().EnableDutiesV2 { + dutiesResponse, err := c.beaconNodeValidatorClient.GetDutiesV2(ctx, in) + if err != nil { + if status.Code(err) == codes.Unimplemented { + log.Warn("beaconNodeValidatorClient.GetDutiesV2() returned status code unavailable, falling back to GetDuties") + return c.getDuties(ctx, in) + } + return nil, errors.Wrap( + client.ErrConnectionIssue, + errors.Wrap(err, "getDutiesV2").Error(), + ) + } + return toValidatorDutiesContainerV2(dutiesResponse) } - return toValidatorDutiesContainer(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 ðpb.ValidatorDutiesContainer{ + PrevDependentRoot: dutiesResponse.PreviousDutyDependentRoot, + CurrDependentRoot: dutiesResponse.CurrentDutyDependentRoot, + CurrentEpochDuties: currentDuties, + NextEpochDuties: nextDuties, + }, nil +} + +func toValidatorDutyV2(duty *ethpb.DutiesV2Response_Duty) (*ethpb.ValidatorDuty, error) { + return ðpb.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) } diff --git a/validator/client/grpc-api/grpc_validator_client_test.go b/validator/client/grpc-api/grpc_validator_client_test.go index 898f5c9f41..18d569dacd 100644 --- a/validator/client/grpc-api/grpc_validator_client_test.go +++ b/validator/client/grpc-api/grpc_validator_client_test.go @@ -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 := ð.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 := ð.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()