diff --git a/validator/client/beacon-api/beacon_api_node_client.go b/validator/client/beacon-api/beacon_api_node_client.go index f995806e2..6dd726b1a 100644 --- a/validator/client/beacon-api/beacon_api_node_client.go +++ b/validator/client/beacon-api/beacon_api_node_client.go @@ -115,7 +115,7 @@ func NewNodeClientWithFallback(jsonRestHandler JsonRestHandler, fallbackClient i b := &beaconApiNodeClient{ jsonRestHandler: jsonRestHandler, fallbackClient: fallbackClient, - genesisProvider: beaconApiGenesisProvider{jsonRestHandler: jsonRestHandler}, + genesisProvider: &beaconApiGenesisProvider{jsonRestHandler: jsonRestHandler}, } b.healthTracker = beacon.NewNodeHealthTracker(b) return b diff --git a/validator/client/beacon-api/beacon_api_validator_client.go b/validator/client/beacon-api/beacon_api_validator_client.go index 115053c56..7e59bd0ab 100644 --- a/validator/client/beacon-api/beacon_api_validator_client.go +++ b/validator/client/beacon-api/beacon_api_validator_client.go @@ -29,7 +29,7 @@ type beaconApiValidatorClient struct { func NewBeaconApiValidatorClient(jsonRestHandler JsonRestHandler, opts ...ValidatorClientOpt) iface.ValidatorClient { c := &beaconApiValidatorClient{ - genesisProvider: beaconApiGenesisProvider{jsonRestHandler: jsonRestHandler}, + genesisProvider: &beaconApiGenesisProvider{jsonRestHandler: jsonRestHandler}, dutiesProvider: beaconApiDutiesProvider{jsonRestHandler: jsonRestHandler}, stateValidatorsProvider: beaconApiStateValidatorsProvider{jsonRestHandler: jsonRestHandler}, jsonRestHandler: jsonRestHandler, diff --git a/validator/client/beacon-api/genesis.go b/validator/client/beacon-api/genesis.go index c235e2f9f..411953660 100644 --- a/validator/client/beacon-api/genesis.go +++ b/validator/client/beacon-api/genesis.go @@ -4,6 +4,7 @@ import ( "context" "net/http" "strconv" + "sync" "time" "github.com/ethereum/go-ethereum/common/hexutil" @@ -19,6 +20,8 @@ type GenesisProvider interface { type beaconApiGenesisProvider struct { jsonRestHandler JsonRestHandler + genesis *structs.Genesis + once sync.Once } func (c beaconApiValidatorClient) waitForChainStart(ctx context.Context) (*ethpb.ChainStartResponse, error) { @@ -64,15 +67,23 @@ func (c beaconApiValidatorClient) waitForChainStart(ctx context.Context) (*ethpb } // GetGenesis gets the genesis information from the beacon node via the /eth/v1/beacon/genesis endpoint -func (c beaconApiGenesisProvider) GetGenesis(ctx context.Context) (*structs.Genesis, error) { +func (c *beaconApiGenesisProvider) GetGenesis(ctx context.Context) (*structs.Genesis, error) { genesisJson := &structs.GetGenesisResponse{} - if err := c.jsonRestHandler.Get(ctx, "/eth/v1/beacon/genesis", genesisJson); err != nil { - return nil, err + var doErr error + c.once.Do(func() { + if err := c.jsonRestHandler.Get(ctx, "/eth/v1/beacon/genesis", genesisJson); err != nil { + doErr = err + return + } + if genesisJson.Data == nil { + doErr = errors.New("genesis data is nil") + return + } + c.genesis = genesisJson.Data + }) + if doErr != nil { + // Allow another call because the current one returned an error + c.once = sync.Once{} } - - if genesisJson.Data == nil { - return nil, errors.New("genesis data is nil") - } - - return genesisJson.Data, nil + return c.genesis, doErr } diff --git a/validator/client/beacon-api/genesis_test.go b/validator/client/beacon-api/genesis_test.go index 4eca109f4..1835068dd 100644 --- a/validator/client/beacon-api/genesis_test.go +++ b/validator/client/beacon-api/genesis_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/pkg/errors" "github.com/prysmaticlabs/prysm/v5/api/server/structs" "github.com/prysmaticlabs/prysm/v5/testing/assert" "github.com/prysmaticlabs/prysm/v5/testing/require" @@ -66,3 +67,78 @@ func TestGetGenesis_NilData(t *testing.T) { _, err := genesisProvider.GetGenesis(ctx) assert.ErrorContains(t, "genesis data is nil", err) } + +func TestGetGenesis_EndpointCalledOnlyOnce(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + + genesisResponseJson := structs.GetGenesisResponse{} + jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) + jsonRestHandler.EXPECT().Get( + ctx, + "/eth/v1/beacon/genesis", + &genesisResponseJson, + ).Return( + nil, + ).SetArg( + 2, + structs.GetGenesisResponse{ + Data: &structs.Genesis{ + GenesisTime: "1234", + GenesisValidatorsRoot: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + }, + }, + ).Times(1) + + genesisProvider := &beaconApiGenesisProvider{jsonRestHandler: jsonRestHandler} + _, err := genesisProvider.GetGenesis(ctx) + assert.NoError(t, err) + resp, err := genesisProvider.GetGenesis(ctx) + assert.NoError(t, err) + require.NotNil(t, resp) + assert.Equal(t, "1234", resp.GenesisTime) + assert.Equal(t, "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", resp.GenesisValidatorsRoot) +} + +func TestGetGenesis_EndpointCanBeCalledAgainAfterError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + + genesisResponseJson := structs.GetGenesisResponse{} + jsonRestHandler := mock.NewMockJsonRestHandler(ctrl) + jsonRestHandler.EXPECT().Get( + ctx, + "/eth/v1/beacon/genesis", + &genesisResponseJson, + ).Return( + errors.New("foo"), + ).Times(1) + jsonRestHandler.EXPECT().Get( + ctx, + "/eth/v1/beacon/genesis", + &genesisResponseJson, + ).Return( + nil, + ).SetArg( + 2, + structs.GetGenesisResponse{ + Data: &structs.Genesis{ + GenesisTime: "1234", + GenesisValidatorsRoot: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + }, + }, + ).Times(1) + + genesisProvider := &beaconApiGenesisProvider{jsonRestHandler: jsonRestHandler} + _, err := genesisProvider.GetGenesis(ctx) + require.ErrorContains(t, "foo", err) + resp, err := genesisProvider.GetGenesis(ctx) + assert.NoError(t, err) + require.NotNil(t, resp) + assert.Equal(t, "1234", resp.GenesisTime) + assert.Equal(t, "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", resp.GenesisValidatorsRoot) +} diff --git a/validator/client/beacon-api/wait_for_chain_start_test.go b/validator/client/beacon-api/wait_for_chain_start_test.go index 539554677..6b29babd9 100644 --- a/validator/client/beacon-api/wait_for_chain_start_test.go +++ b/validator/client/beacon-api/wait_for_chain_start_test.go @@ -40,7 +40,7 @@ func TestWaitForChainStart_ValidGenesis(t *testing.T) { ).Times(1) genesisProvider := beaconApiGenesisProvider{jsonRestHandler: jsonRestHandler} - validatorClient := beaconApiValidatorClient{genesisProvider: genesisProvider} + validatorClient := beaconApiValidatorClient{genesisProvider: &genesisProvider} resp, err := validatorClient.WaitForChainStart(ctx, &emptypb.Empty{}) assert.NoError(t, err) @@ -104,7 +104,7 @@ func TestWaitForChainStart_BadGenesis(t *testing.T) { ).Times(1) genesisProvider := beaconApiGenesisProvider{jsonRestHandler: jsonRestHandler} - validatorClient := beaconApiValidatorClient{genesisProvider: genesisProvider} + validatorClient := beaconApiValidatorClient{genesisProvider: &genesisProvider} _, err := validatorClient.WaitForChainStart(ctx, &emptypb.Empty{}) assert.ErrorContains(t, testCase.errorMessage, err) }) @@ -127,7 +127,7 @@ func TestWaitForChainStart_JsonResponseError(t *testing.T) { ).Times(1) genesisProvider := beaconApiGenesisProvider{jsonRestHandler: jsonRestHandler} - validatorClient := beaconApiValidatorClient{genesisProvider: genesisProvider} + validatorClient := beaconApiValidatorClient{genesisProvider: &genesisProvider} _, err := validatorClient.WaitForChainStart(ctx, &emptypb.Empty{}) assert.ErrorContains(t, "failed to get genesis data", err) assert.ErrorContains(t, "some specific json error", err) @@ -172,7 +172,7 @@ func TestWaitForChainStart_JsonResponseError404(t *testing.T) { ).Times(1) genesisProvider := beaconApiGenesisProvider{jsonRestHandler: jsonRestHandler} - validatorClient := beaconApiValidatorClient{genesisProvider: genesisProvider} + validatorClient := beaconApiValidatorClient{genesisProvider: &genesisProvider} resp, err := validatorClient.WaitForChainStart(ctx, &emptypb.Empty{}) assert.NoError(t, err)