Files
prysm/validator/client/beacon-api/beacon_api_node_client_test.go
james-prysm 1a6252ade4 changing isHealthy to isReady (#16167)
<!-- Thanks for sending a PR! Before submitting:

1. If this is your first PR, check out our contribution guide here
https://docs.prylabs.network/docs/contribute/contribution-guidelines
You will then need to sign our Contributor License Agreement (CLA),
which will show up as a comment from a bot in this pull request after
you open it. We cannot review code without a signed CLA.
2. Please file an associated tracking issue if this pull request is
non-trivial and requires context for our team to understand. All
features and most bug fixes should have
an associated issue with a design discussed and decided upon. Small bug
   fixes and documentation improvements don't need issues.
3. New features and bug fixes must have tests. Documentation may need to
be updated. If you're unsure what to update, send the PR, and we'll
discuss
   in review.
4. Note that PRs updating dependencies and new Go versions are not
accepted.
   Please file an issue instead.
5. A changelog entry is required for user facing issues.
-->

**What type of PR is this?**

 Bug fix

**What does this PR do? Why is it needed?**

validator fallbacks shouldn't work on nodes that are syncing as many of
the tasks validators perform require the node to be fully synced.

- 206 or any other code is  interpreted as "not ready"
- 200 interpreted as "ready"

**Which issues(s) does this PR fix?**
 
continuation of https://github.com/OffchainLabs/prysm/pull/15401

**Other notes for review**

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).
2026-01-06 18:58:12 +00:00

347 lines
9.0 KiB
Go

package beacon_api
import (
"errors"
"net/http"
"testing"
"github.com/OffchainLabs/prysm/v7/api/server/structs"
ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
"github.com/OffchainLabs/prysm/v7/testing/assert"
"github.com/OffchainLabs/prysm/v7/validator/client/beacon-api/mock"
"github.com/ethereum/go-ethereum/common/hexutil"
"go.uber.org/mock/gomock"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/timestamppb"
)
func TestGetGenesis(t *testing.T) {
testCases := []struct {
name string
genesisResponse *structs.Genesis
genesisError error
depositContractResponse structs.GetDepositContractResponse
depositContractError error
queriesDepositContract bool
expectedResponse *ethpb.Genesis
expectedError string
}{
{
name: "fails to get genesis",
genesisError: errors.New("foo error"),
expectedError: "failed to get genesis: foo error",
},
{
name: "fails to decode genesis validator root",
genesisResponse: &structs.Genesis{
GenesisTime: "1",
GenesisValidatorsRoot: "foo",
},
expectedError: "failed to decode genesis validator root `foo`",
},
{
name: "fails to parse genesis time",
genesisResponse: &structs.Genesis{
GenesisTime: "foo",
GenesisValidatorsRoot: hexutil.Encode([]byte{1}),
},
expectedError: "failed to parse genesis time `foo`",
},
{
name: "fails to query contract information",
genesisResponse: &structs.Genesis{
GenesisTime: "1",
GenesisValidatorsRoot: hexutil.Encode([]byte{2}),
},
depositContractError: errors.New("foo error"),
queriesDepositContract: true,
expectedError: "foo error",
},
{
name: "fails to read nil deposit contract data",
genesisResponse: &structs.Genesis{
GenesisTime: "1",
GenesisValidatorsRoot: hexutil.Encode([]byte{2}),
},
queriesDepositContract: true,
depositContractResponse: structs.GetDepositContractResponse{
Data: nil,
},
expectedError: "deposit contract data is nil",
},
{
name: "fails to decode deposit contract address",
genesisResponse: &structs.Genesis{
GenesisTime: "1",
GenesisValidatorsRoot: hexutil.Encode([]byte{2}),
},
queriesDepositContract: true,
depositContractResponse: structs.GetDepositContractResponse{
Data: &structs.DepositContractData{
Address: "foo",
},
},
expectedError: "failed to decode deposit contract address `foo`",
},
{
name: "successfully retrieves genesis info",
genesisResponse: &structs.Genesis{
GenesisTime: "654812",
GenesisValidatorsRoot: hexutil.Encode([]byte{2}),
},
queriesDepositContract: true,
depositContractResponse: structs.GetDepositContractResponse{
Data: &structs.DepositContractData{
Address: hexutil.Encode([]byte{3}),
},
},
expectedResponse: &ethpb.Genesis{
GenesisTime: &timestamppb.Timestamp{
Seconds: 654812,
},
DepositContractAddress: []byte{3},
GenesisValidatorsRoot: []byte{2},
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := t.Context()
genesisProvider := mock.NewMockGenesisProvider(ctrl)
genesisProvider.EXPECT().Genesis(
gomock.Any(),
).Return(
testCase.genesisResponse,
testCase.genesisError,
)
depositContractJson := structs.GetDepositContractResponse{}
jsonRestHandler := mock.NewMockJsonRestHandler(ctrl)
if testCase.queriesDepositContract {
jsonRestHandler.EXPECT().Get(
gomock.Any(),
"/eth/v1/config/deposit_contract",
&depositContractJson,
).Return(
testCase.depositContractError,
).SetArg(
2,
testCase.depositContractResponse,
)
}
nodeClient := &beaconApiNodeClient{
genesisProvider: genesisProvider,
jsonRestHandler: jsonRestHandler,
}
response, err := nodeClient.Genesis(ctx, &emptypb.Empty{})
if testCase.expectedResponse == nil {
assert.ErrorContains(t, testCase.expectedError, err)
} else {
assert.DeepEqual(t, testCase.expectedResponse, response)
}
})
}
}
func TestGetSyncStatus(t *testing.T) {
const syncingEndpoint = "/eth/v1/node/syncing"
testCases := []struct {
name string
restEndpointResponse structs.SyncStatusResponse
restEndpointError error
expectedResponse *ethpb.SyncStatus
expectedError string
}{
{
name: "fails to query REST endpoint",
restEndpointError: errors.New("foo error"),
expectedError: "foo error",
},
{
name: "returns nil syncing data",
restEndpointResponse: structs.SyncStatusResponse{Data: nil},
expectedError: "syncing data is nil",
},
{
name: "returns false syncing status",
restEndpointResponse: structs.SyncStatusResponse{
Data: &structs.SyncStatusResponseData{
IsSyncing: false,
},
},
expectedResponse: &ethpb.SyncStatus{
Syncing: false,
},
},
{
name: "returns true syncing status",
restEndpointResponse: structs.SyncStatusResponse{
Data: &structs.SyncStatusResponseData{
IsSyncing: true,
},
},
expectedResponse: &ethpb.SyncStatus{
Syncing: true,
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := t.Context()
syncingResponse := structs.SyncStatusResponse{}
jsonRestHandler := mock.NewMockJsonRestHandler(ctrl)
jsonRestHandler.EXPECT().Get(
gomock.Any(),
syncingEndpoint,
&syncingResponse,
).Return(
testCase.restEndpointError,
).SetArg(
2,
testCase.restEndpointResponse,
)
nodeClient := &beaconApiNodeClient{jsonRestHandler: jsonRestHandler}
syncStatus, err := nodeClient.SyncStatus(ctx, &emptypb.Empty{})
if testCase.expectedResponse == nil {
assert.ErrorContains(t, testCase.expectedError, err)
} else {
assert.DeepEqual(t, testCase.expectedResponse, syncStatus)
}
})
}
}
func TestGetVersion(t *testing.T) {
const versionEndpoint = "/eth/v1/node/version"
testCases := []struct {
name string
restEndpointResponse structs.GetVersionResponse
restEndpointError error
expectedResponse *ethpb.Version
expectedError string
}{
{
name: "fails to query REST endpoint",
restEndpointError: errors.New("foo error"),
expectedError: "foo error",
},
{
name: "returns nil version data",
restEndpointResponse: structs.GetVersionResponse{Data: nil},
expectedError: "empty version response",
},
{
name: "returns proper version response",
restEndpointResponse: structs.GetVersionResponse{
Data: &structs.Version{
Version: "prysm/local",
},
},
expectedResponse: &ethpb.Version{
Version: "prysm/local",
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := t.Context()
var versionResponse structs.GetVersionResponse
jsonRestHandler := mock.NewMockJsonRestHandler(ctrl)
jsonRestHandler.EXPECT().Get(
gomock.Any(),
versionEndpoint,
&versionResponse,
).Return(
testCase.restEndpointError,
).SetArg(
2,
testCase.restEndpointResponse,
)
nodeClient := &beaconApiNodeClient{jsonRestHandler: jsonRestHandler}
version, err := nodeClient.Version(ctx, &emptypb.Empty{})
if testCase.expectedResponse == nil {
assert.ErrorContains(t, testCase.expectedError, err)
} else {
assert.DeepEqual(t, testCase.expectedResponse, version)
}
})
}
}
func TestIsReady(t *testing.T) {
const healthEndpoint = "/eth/v1/node/health"
testCases := []struct {
name string
statusCode int
err error
expectedResult bool
}{
{
name: "returns true for 200 OK (fully synced)",
statusCode: http.StatusOK,
expectedResult: true,
},
{
name: "returns false for 206 Partial Content (syncing)",
statusCode: http.StatusPartialContent,
expectedResult: false,
},
{
name: "returns false for 503 Service Unavailable",
statusCode: http.StatusServiceUnavailable,
expectedResult: false,
},
{
name: "returns false for 500 Internal Server Error",
statusCode: http.StatusInternalServerError,
expectedResult: false,
},
{
name: "returns false on error",
err: errors.New("request failed"),
expectedResult: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := t.Context()
jsonRestHandler := mock.NewMockJsonRestHandler(ctrl)
jsonRestHandler.EXPECT().GetStatusCode(
gomock.Any(),
healthEndpoint,
).Return(tc.statusCode, tc.err)
nodeClient := &beaconApiNodeClient{jsonRestHandler: jsonRestHandler}
result := nodeClient.IsReady(ctx)
assert.Equal(t, tc.expectedResult, result)
})
}
}