package beacon_api import ( "context" "strings" "testing" "time" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/golang/mock/gomock" "github.com/pkg/errors" rpcmiddleware "github.com/prysmaticlabs/prysm/v3/beacon-chain/rpc/apimiddleware" "github.com/prysmaticlabs/prysm/v3/config/params" ethpb "github.com/prysmaticlabs/prysm/v3/proto/prysm/v1alpha1" "github.com/prysmaticlabs/prysm/v3/testing/assert" "github.com/prysmaticlabs/prysm/v3/testing/require" "github.com/prysmaticlabs/prysm/v3/validator/client/beacon-api/mock" ) func TestComputeWaitElements_LastRecvTimeZero(t *testing.T) { now := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC) lastRecvTime := time.Time{} waitDuration, nextRecvTime := computeWaitElements(now, lastRecvTime) assert.Equal(t, time.Duration(0), waitDuration) assert.Equal(t, now, nextRecvTime) } func TestComputeWaitElements_LastRecvTimeNotZero(t *testing.T) { delay := 10 now := time.Date(2022, 1, 1, 0, 0, delay, 0, time.UTC) lastRecvTime := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC) secondsPerSlot := params.BeaconConfig().SecondsPerSlot waitDuration, nextRecvTime := computeWaitElements(now, lastRecvTime) assert.Equal(t, time.Duration(secondsPerSlot-uint64(delay))*time.Second, waitDuration) assert.Equal(t, time.Date(2022, 1, 1, 0, 0, int(secondsPerSlot), 0, time.UTC), nextRecvTime) } func TestComputeWaitElements_Longest(t *testing.T) { now := time.Date(2022, 1, 1, 0, 0, 20, 0, time.UTC) lastRecvTime := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC) waitDuration, nextRecvTime := computeWaitElements(now, lastRecvTime) assert.Equal(t, 0*time.Second, waitDuration) assert.Equal(t, now, nextRecvTime) } func TestActivation_Nominal(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() stringPubKeys := []string{ "0x8000091c2ae64ee414a54c1cc1fc67dec663408bc636cb86756e0200e41a75c8f86603f104f02c856983d2783116be13", // active_ongoing "0x80000e851c0f53c3246ff726d7ff7766661ca5e12a07c45c114d208d54f0f8233d4380b2e9aff759d69795d1df905526", // active_exiting "0x424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242", // does not exist "0x800015473bdc3a7f45ef8eb8abc598bc20021e55ad6e6ad1d745aaef9730dd2c28ec08bf42df18451de94dd4a6d24ec5", // exited_slashed } pubKeys := make([][]byte, len(stringPubKeys)) url := strings.Join([]string{ "/eth/v1/beacon/states/head/validators?", "id=0x8000091c2ae64ee414a54c1cc1fc67dec663408bc636cb86756e0200e41a75c8f86603f104f02c856983d2783116be13&", "id=0x80000e851c0f53c3246ff726d7ff7766661ca5e12a07c45c114d208d54f0f8233d4380b2e9aff759d69795d1df905526&", "id=0x424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242&", "id=0x800015473bdc3a7f45ef8eb8abc598bc20021e55ad6e6ad1d745aaef9730dd2c28ec08bf42df18451de94dd4a6d24ec5", }, "") for i, stringPubKey := range stringPubKeys { pubKey, err := hexutil.Decode(stringPubKey) require.NoError(t, err) pubKeys[i] = pubKey } wantedStatuses := []*ethpb.ValidatorActivationResponse_Status{ { PublicKey: pubKeys[0], Index: 55293, Status: ðpb.ValidatorStatusResponse{ Status: ethpb.ValidatorStatus_ACTIVE, }, }, { PublicKey: pubKeys[1], Index: 11877, Status: ðpb.ValidatorStatusResponse{ Status: ethpb.ValidatorStatus_EXITING, }, }, { PublicKey: pubKeys[3], Index: 210439, Status: ðpb.ValidatorStatusResponse{ Status: ethpb.ValidatorStatus_EXITED, }, }, { PublicKey: pubKeys[2], Index: 18446744073709551615, Status: ðpb.ValidatorStatusResponse{ Status: ethpb.ValidatorStatus_UNKNOWN_STATUS, }, }, } stateValidatorsResponseJson := rpcmiddleware.StateValidatorsResponseJson{} // Instantiate a cancellable context. ctx, cancel := context.WithCancel(context.Background()) jsonRestHandler := mock.NewMockjsonRestHandler(ctrl) // GetRestJsonResponse does not return any result for non existing key jsonRestHandler.EXPECT().GetRestJsonResponse( ctx, url, &stateValidatorsResponseJson, ).Return( nil, nil, ).SetArg( 2, rpcmiddleware.StateValidatorsResponseJson{ Data: []*rpcmiddleware.ValidatorContainerJson{ { Index: "55293", Status: "active_ongoing", Validator: &rpcmiddleware.ValidatorJson{ PublicKey: stringPubKeys[0], }, }, { Index: "11877", Status: "active_exiting", Validator: &rpcmiddleware.ValidatorJson{ PublicKey: stringPubKeys[1], }, }, { Index: "210439", Status: "exited_slashed", Validator: &rpcmiddleware.ValidatorJson{ PublicKey: stringPubKeys[3], }, }, }, }, ).Times(1) validatorClient := beaconApiValidatorClient{ stateValidatorsProvider: beaconApiStateValidatorsProvider{ jsonRestHandler: jsonRestHandler, }, } waitForActivation, err := validatorClient.WaitForActivation( ctx, ðpb.ValidatorActivationRequest{ PublicKeys: pubKeys, }, ) assert.NoError(t, err) // This first call to `Recv` should return immediately resp, err := waitForActivation.Recv() require.NoError(t, err) assert.DeepEqual(t, wantedStatuses, resp.Statuses) // Cancel the context after 1 second go func(ctx context.Context) { time.Sleep(time.Second) cancel() }(ctx) // This second call to `Recv` should return after ~12 seconds, but is interrupted by the cancel _, err = waitForActivation.Recv() assert.ErrorContains(t, "context canceled", err) } func TestActivation_InvalidData(t *testing.T) { testCases := []struct { name string data []*rpcmiddleware.ValidatorContainerJson expectedErrorMessage string }{ { name: "bad validator public key", data: []*rpcmiddleware.ValidatorContainerJson{ { Index: "55293", Status: "active_ongoing", Validator: &rpcmiddleware.ValidatorJson{ PublicKey: "NotAPubKey", }, }, }, expectedErrorMessage: "failed to parse validator public key", }, { name: "bad validator index", data: []*rpcmiddleware.ValidatorContainerJson{ { Index: "NotAnIndex", Status: "active_ongoing", Validator: &rpcmiddleware.ValidatorJson{ PublicKey: stringPubKey, }, }, }, expectedErrorMessage: "failed to parse validator index", }, { name: "invalid validator status", data: []*rpcmiddleware.ValidatorContainerJson{ { Index: "12345", Status: "NotAStatus", Validator: &rpcmiddleware.ValidatorJson{ PublicKey: stringPubKey, }, }, }, expectedErrorMessage: "invalid validator status: NotAStatus", }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() ctx := context.Background() jsonRestHandler := mock.NewMockjsonRestHandler(ctrl) jsonRestHandler.EXPECT().GetRestJsonResponse( ctx, gomock.Any(), gomock.Any(), ).Return( nil, nil, ).SetArg( 2, rpcmiddleware.StateValidatorsResponseJson{ Data: testCase.data, }, ).Times(1) validatorClient := beaconApiValidatorClient{ stateValidatorsProvider: beaconApiStateValidatorsProvider{ jsonRestHandler: jsonRestHandler, }, } waitForActivation, err := validatorClient.WaitForActivation( ctx, ðpb.ValidatorActivationRequest{}, ) assert.NoError(t, err) _, err = waitForActivation.Recv() assert.ErrorContains(t, testCase.expectedErrorMessage, err) }, ) } } func TestActivation_JsonResponseError(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() ctx := context.Background() jsonRestHandler := mock.NewMockjsonRestHandler(ctrl) jsonRestHandler.EXPECT().GetRestJsonResponse( ctx, gomock.Any(), gomock.Any(), ).Return( nil, errors.New("some specific json error"), ).Times(1) validatorClient := beaconApiValidatorClient{ stateValidatorsProvider: beaconApiStateValidatorsProvider{ jsonRestHandler: jsonRestHandler, }, } waitForActivation, err := validatorClient.WaitForActivation( ctx, ðpb.ValidatorActivationRequest{}, ) assert.NoError(t, err) _, err = waitForActivation.Recv() assert.ErrorContains(t, "failed to get state validators", err) }