Skip to content

Commit

Permalink
Add REST implementation for Validator's WaitForChainStart (#11654)
Browse files Browse the repository at this point in the history
* Add REST implementation for Validator's WaitForChainStart

* Add missing error mapping

* Add missing bazel dependency

* Add missing tests

* Address PR comments

* Replace EventErrorJson with DefaultErrorJson

* Add tests for WaitForChainStart

* Refactor tests

* Address PR comments

* Add gazelle:build_tag use_beacon_api comment in BUILD.bazel

* Address PR comments

* Address PR comments

Co-authored-by: Radosław Kapka <rkapka@wp.pl>
  • Loading branch information
PatriceVignola and rkapka authored Nov 22, 2022
1 parent fe1281d commit 0d4b98c
Show file tree
Hide file tree
Showing 15 changed files with 690 additions and 203 deletions.
208 changes: 104 additions & 104 deletions testing/mock/validator_client_mock.go

Large diffs are not rendered by default.

36 changes: 32 additions & 4 deletions validator/client/beacon-api/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,15 +1,43 @@
load("@prysm//tools/go:def.bzl", "go_library")
load("@prysm//tools/go:def.bzl", "go_library", "go_test")

# Gazelle will flag this for removal since it's only being used with the use_beacon_api flag, but it should be kept
# keep
# gazelle:build_tags use_beacon_api
go_library(
name = "go_default_library",
srcs = ["beacon_api_validator_client.go"],
srcs = [
"beacon_api_helpers.go",
"beacon_api_validator_client.go",
"genesis.go",
],
importpath = "github.com/prysmaticlabs/prysm/v3/validator/client/beacon-api",
visibility = ["//validator:__subpackages__"],
deps = [
"//api/gateway/apimiddleware:go_default_library",
"//beacon-chain/rpc/apimiddleware:go_default_library",
"//proto/prysm/v1alpha1:go_default_library",
"//validator/client/iface:go_default_library",
"@com_github_ethereum_go_ethereum//common/hexutil:go_default_library",
"@com_github_pkg_errors//:go_default_library",
"@io_bazel_rules_go//proto/wkt:empty_go_proto",
],
)

# gazelle:build_tags use_beacon_api
go_test(
name = "go_default_test",
size = "small",
srcs = [
"beacon_api_helpers_test.go",
"genesis_test.go",
"handlers_test.go",
"wait_for_chain_start_test.go",
],
embed = [":go_default_library"],
deps = [
"//api/gateway/apimiddleware:go_default_library",
"//beacon-chain/rpc/apimiddleware:go_default_library",
"//testing/assert:go_default_library",
"//testing/require:go_default_library",
"@com_github_ethereum_go_ethereum//common/hexutil:go_default_library",
"@org_golang_google_protobuf//types/known/emptypb:go_default_library",
],
)
16 changes: 16 additions & 0 deletions validator/client/beacon-api/beacon_api_helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//go:build use_beacon_api
// +build use_beacon_api

package beacon_api

import (
"regexp"
)

func validRoot(root string) bool {
matchesRegex, err := regexp.MatchString("^0x[a-fA-F0-9]{64}$", root)
if err != nil {
return false
}
return matchesRegex
}
55 changes: 55 additions & 0 deletions validator/client/beacon-api/beacon_api_helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//go:build use_beacon_api
// +build use_beacon_api

package beacon_api

import (
"testing"

"github.com/prysmaticlabs/prysm/v3/testing/assert"
)

func TestBeaconApiHelpers(t *testing.T) {
tests := []struct {
name string
input string
valid bool
}{
{
name: "correct format",
input: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
valid: true,
},
{
name: "root too small",
input: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f",
valid: false,
},
{
name: "root too big",
input: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f22",
valid: false,
},
{
name: "empty root",
input: "",
valid: false,
},
{
name: "no 0x prefix",
input: "cf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
valid: false,
},
{
name: "invalid characters",
input: "0xzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
valid: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.valid, validRoot(tt.input))
})
}
}
4 changes: 2 additions & 2 deletions validator/client/beacon-api/beacon_api_validator_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@ func (*beaconApiValidatorClient) WaitForActivation(_ context.Context, _ *ethpb.V
}

// Deprecated: Do not use.
func (*beaconApiValidatorClient) WaitForChainStart(_ context.Context, _ *empty.Empty) (ethpb.BeaconNodeValidator_WaitForChainStartClient, error) {
panic("beaconApiValidatorClient.WaitForChainStart is not implemented")
func (c *beaconApiValidatorClient) WaitForChainStart(_ context.Context, _ *empty.Empty) (*ethpb.ChainStartResponse, error) {
return c.waitForChainStart()
}

func NewBeaconApiValidatorClient(url string, timeout time.Duration) iface.ValidatorClient {
Expand Down
76 changes: 76 additions & 0 deletions validator/client/beacon-api/genesis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//go:build use_beacon_api
// +build use_beacon_api

package beacon_api

import (
"encoding/json"
"net/http"
"strconv"

"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/v3/api/gateway/apimiddleware"
rpcmiddleware "github.com/prysmaticlabs/prysm/v3/beacon-chain/rpc/apimiddleware"
ethpb "github.com/prysmaticlabs/prysm/v3/proto/prysm/v1alpha1"
)

func (c beaconApiValidatorClient) waitForChainStart() (*ethpb.ChainStartResponse, error) {
genesis, err := c.getGenesis()
if err != nil {
return nil, errors.Wrap(err, "failed to get genesis data")
}

genesisTime, err := strconv.ParseUint(genesis.Data.GenesisTime, 10, 64)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse genesis time: %s", genesis.Data.GenesisTime)
}

chainStartResponse := &ethpb.ChainStartResponse{}
chainStartResponse.Started = true
chainStartResponse.GenesisTime = genesisTime

if !validRoot(genesis.Data.GenesisValidatorsRoot) {
return nil, errors.Errorf("invalid genesis validators root: %s", genesis.Data.GenesisValidatorsRoot)
}

genesisValidatorRoot, err := hexutil.Decode(genesis.Data.GenesisValidatorsRoot)
if err != nil {
return nil, errors.Wrap(err, "failed to decode genesis validators root")
}
chainStartResponse.GenesisValidatorsRoot = genesisValidatorRoot

return chainStartResponse, nil
}

func (c beaconApiValidatorClient) getGenesis() (*rpcmiddleware.GenesisResponseJson, error) {
resp, err := c.httpClient.Get(c.url + "/eth/v1/beacon/genesis")
if err != nil {
return nil, errors.Wrap(err, "failed to query REST API genesis endpoint")
}
defer func() {
if err = resp.Body.Close(); err != nil {
return
}
}()

if resp.StatusCode != http.StatusOK {
errorJson := apimiddleware.DefaultErrorJson{}
if err := json.NewDecoder(resp.Body).Decode(&errorJson); err != nil {
return nil, errors.Wrap(err, "failed to decode response body genesis error json")
}

return nil, errors.Errorf("error %d: %s", errorJson.Code, errorJson.Message)
}

genesisJson := &rpcmiddleware.GenesisResponseJson{}
if err := json.NewDecoder(resp.Body).Decode(&genesisJson); err != nil {
return nil, errors.Wrap(err, "failed to decode response body genesis json")
}

if genesisJson.Data == nil {
return nil, errors.New("GenesisResponseJson.Data is nil")
}

return genesisJson, nil
}
73 changes: 73 additions & 0 deletions validator/client/beacon-api/genesis_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//go:build use_beacon_api
// +build use_beacon_api

package beacon_api

import (
"net/http"
"net/http/httptest"
"testing"
"time"

rpcmiddleware "github.com/prysmaticlabs/prysm/v3/beacon-chain/rpc/apimiddleware"
"github.com/prysmaticlabs/prysm/v3/testing/assert"
"github.com/prysmaticlabs/prysm/v3/testing/require"
)

func TestGetGenesis_ValidGenesis(t *testing.T) {
server := httptest.NewServer(createGenesisHandler(&rpcmiddleware.GenesisResponse_GenesisJson{
GenesisTime: "1234",
GenesisValidatorsRoot: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
}))
defer server.Close()

validatorClient := &beaconApiValidatorClient{url: server.URL, httpClient: http.Client{Timeout: time.Second * 5}}
resp, err := validatorClient.getGenesis()
assert.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, resp.Data)
assert.Equal(t, "1234", resp.Data.GenesisTime)
assert.Equal(t, "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", resp.Data.GenesisValidatorsRoot)
}

func TestGetGenesis_NilData(t *testing.T) {
server := httptest.NewServer(createGenesisHandler(nil))
defer server.Close()

validatorClient := &beaconApiValidatorClient{url: server.URL, httpClient: http.Client{Timeout: time.Second * 5}}
_, err := validatorClient.getGenesis()
assert.ErrorContains(t, "GenesisResponseJson.Data is nil", err)
}

func TestGetGenesis_InvalidJsonGenesis(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte("foo"))
require.NoError(t, err)
}))
defer server.Close()

validatorClient := &beaconApiValidatorClient{url: server.URL, httpClient: http.Client{Timeout: time.Second * 5}}
_, err := validatorClient.getGenesis()
assert.ErrorContains(t, "failed to decode response body genesis json", err)
}

func TestGetGenesis_InvalidJsonError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(invalidJsonErrHandler))
defer server.Close()

validatorClient := &beaconApiValidatorClient{url: server.URL, httpClient: http.Client{Timeout: time.Second * 5}}
_, err := validatorClient.getGenesis()
assert.ErrorContains(t, "failed to decode response body genesis error json", err)
}

func TestGetGenesis_Timeout(t *testing.T) {
server := httptest.NewServer(createGenesisHandler(&rpcmiddleware.GenesisResponse_GenesisJson{
GenesisTime: "1234",
GenesisValidatorsRoot: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
}))
defer server.Close()

validatorClient := &beaconApiValidatorClient{url: server.URL, httpClient: http.Client{Timeout: 1}}
_, err := validatorClient.getGenesis()
assert.ErrorContains(t, "failed to query REST API genesis endpoint", err)
}
89 changes: 89 additions & 0 deletions validator/client/beacon-api/handlers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//go:build use_beacon_api
// +build use_beacon_api

package beacon_api

import (
"encoding/json"
"net/http"

"github.com/prysmaticlabs/prysm/v3/api/gateway/apimiddleware"
rpcmiddleware "github.com/prysmaticlabs/prysm/v3/beacon-chain/rpc/apimiddleware"
)

func internalServerErrHandler(w http.ResponseWriter, r *http.Request) {
internalErrorJson := &apimiddleware.DefaultErrorJson{
Code: http.StatusInternalServerError,
Message: "Internal server error",
}

marshalledError, err := json.Marshal(internalErrorJson)
if err != nil {
panic(err)
}

w.WriteHeader(http.StatusInternalServerError)
_, err = w.Write(marshalledError)
if err != nil {
panic(err)
}
}

func notFoundErrHandler(w http.ResponseWriter, r *http.Request) {
internalErrorJson := &apimiddleware.DefaultErrorJson{
Code: http.StatusNotFound,
Message: "Not found",
}

marshalledError, err := json.Marshal(internalErrorJson)
if err != nil {
panic(err)
}

w.WriteHeader(http.StatusNotFound)
_, err = w.Write(marshalledError)
if err != nil {
panic(err)
}
}

func invalidErr999Handler(w http.ResponseWriter, r *http.Request) {
internalErrorJson := &apimiddleware.DefaultErrorJson{
Code: 999,
Message: "Invalid error",
}

marshalledError, err := json.Marshal(internalErrorJson)
if err != nil {
panic(err)
}

w.WriteHeader(999)
_, err = w.Write(marshalledError)
if err != nil {
panic(err)
}
}

func invalidJsonErrHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
_, err := w.Write([]byte("foo"))
if err != nil {
panic(err)
}
}

func createGenesisHandler(data *rpcmiddleware.GenesisResponse_GenesisJson) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
genesisResponseJson := &rpcmiddleware.GenesisResponseJson{Data: data}
marshalledResponse, err := json.Marshal(genesisResponseJson)
if err != nil {
panic(err)
}

_, err = w.Write(marshalledResponse)
if err != nil {
panic(err)
}
})
}
Loading

0 comments on commit 0d4b98c

Please sign in to comment.