Files
prysm/validator/client/beacon-api/beacon_api_node_client_test.go
james-prysm cf94ccbf72 node fallback cleanup (#16316)
**What type of PR is this?**

 Other

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

Follow up to https://github.com/OffchainLabs/prysm/pull/16215 this pr
improves logging, fixes stuttering in package naming, adds additional
unit tests, and deduplicates fallback node code.

**Which issues(s) does this PR fix?**

fixes a potential race if reconnecting to the same host very quickly
which has a stale connection still.

**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-02-04 15:59:42 +00:00

348 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{}
handler := mock.NewMockJsonRestHandler(ctrl)
if testCase.queriesDepositContract {
handler.EXPECT().Get(
gomock.Any(),
"/eth/v1/config/deposit_contract",
&depositContractJson,
).Return(
testCase.depositContractError,
).SetArg(
2,
testCase.depositContractResponse,
)
}
nodeClient := &beaconApiNodeClient{
genesisProvider: genesisProvider,
handler: handler,
}
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{}
handler := mock.NewMockJsonRestHandler(ctrl)
handler.EXPECT().Get(
gomock.Any(),
syncingEndpoint,
&syncingResponse,
).Return(
testCase.restEndpointError,
).SetArg(
2,
testCase.restEndpointResponse,
)
nodeClient := &beaconApiNodeClient{handler: handler}
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
handler := mock.NewMockJsonRestHandler(ctrl)
handler.EXPECT().Get(
gomock.Any(),
versionEndpoint,
&versionResponse,
).Return(
testCase.restEndpointError,
).SetArg(
2,
testCase.restEndpointResponse,
)
nodeClient := &beaconApiNodeClient{handler: handler}
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()
handler := mock.NewMockJsonRestHandler(ctrl)
handler.EXPECT().GetStatusCode(
gomock.Any(),
healthEndpoint,
).Return(tc.statusCode, tc.err)
handler.EXPECT().Host().Return("http://localhost:3500").AnyTimes()
nodeClient := &beaconApiNodeClient{handler: handler}
result := nodeClient.IsReady(ctx)
assert.Equal(t, tc.expectedResult, result)
})
}
}