HTTP validator API: beacon and account endpoints (#13191)

* fixing squashing changes, migrates beacon , account, and auth endpoints on validator client

* adding accounts endpoints

* fixing tests and query endpoints

* adding auth endpoint and fixing unit tests

* removing unused files and updating node file to skip gRPC

* ineffectual assignment fix

* rolling back a change to fix e2e

* fixing issues with ui

* updating with webui version 2.0.5

* updating package name flag in readme

* removing restore assets functions

* adding nomemcopy flag to see if vulenerability scan passes

* making data non compressed to avoid copy vulnerability

* Update beacon-chain/rpc/eth/shared/structs_validator.go

Co-authored-by: Raul Jordan <raul@prysmaticlabs.com>

* updating site_data, and skipping static analysis on file

* adding back deprecation comment notice

* updating workflows to ignore generated

* addressing radek comments

* missed a conversion

---------

Co-authored-by: Raul Jordan <raul@prysmaticlabs.com>
This commit is contained in:
james-prysm
2023-12-01 14:40:09 -06:00
committed by GitHub
parent 461af4baa6
commit 394bd1786a
38 changed files with 2052 additions and 4623 deletions

View File

@@ -794,10 +794,7 @@ func (c *ValidatorClient) registerRPCGatewayService(router *mux.Router) error {
maxCallSize := c.cliCtx.Uint64(cmd.GrpcMaxCallRecvMsgSizeFlag.Name)
registrations := []gateway.PbHandlerRegistration{
validatorpb.RegisterAuthHandler,
pb.RegisterHealthHandler,
validatorpb.RegisterAccountsHandler,
validatorpb.RegisterBeaconHandler,
}
gwmux := gwruntime.NewServeMux(
gwruntime.WithMarshalerOption(gwruntime.MIMEWildcard, &gwruntime.HTTPBodyMarshaler{
@@ -812,7 +809,7 @@ func (c *ValidatorClient) registerRPCGatewayService(router *mux.Router) error {
},
}),
gwruntime.WithMarshalerOption(
"text/event-stream", &gwruntime.EventSourceJSONPb{},
"text/event-stream", &gwruntime.EventSourceJSONPb{}, // TODO: remove this
),
gwruntime.WithForwardResponseOption(gateway.HttpResponseModifier),
)
@@ -825,19 +822,13 @@ func (c *ValidatorClient) registerRPCGatewayService(router *mux.Router) error {
h(w, req)
} else {
// Finally, we handle with the web server.
// DEPRECATED: Prysm Web UI and associated endpoints will be fully removed in a future hard fork.
web.Handler(w, req)
}
}
// remove "/accounts/", "/v2/" after WebUI DEPRECATED
pbHandler := &gateway.PbMux{
Registrations: registrations,
Patterns: []string{
"/accounts/",
"/v2/",
},
Mux: gwmux,
Mux: gwmux,
}
opts := []gateway.Option{
gateway.WithMuxHandler(muxHandler),

View File

@@ -3,10 +3,12 @@ load("@prysm//tools/go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"accounts.go",
"auth_token.go",
"beacon.go",
"handler_wallet.go",
"handlers_accounts.go",
"handlers_auth.go",
"handlers_beacon.go",
"handlers_health.go",
"handlers_keymanager.go",
"handlers_slashing.go",
@@ -20,6 +22,7 @@ go_library(
"//visibility:public",
],
deps = [
"//api:go_default_library",
"//api/grpc:go_default_library",
"//api/pagination:go_default_library",
"//async/event:go_default_library",
@@ -40,7 +43,6 @@ go_library(
"//monitoring/tracing:go_default_library",
"//network/http:go_default_library",
"//proto/prysm/v1alpha1:go_default_library",
"//proto/prysm/v1alpha1/validator-client:go_default_library",
"//runtime/version:go_default_library",
"//validator/accounts:go_default_library",
"//validator/accounts/petnames:go_default_library",
@@ -61,7 +63,6 @@ go_library(
"@com_github_ethereum_go_ethereum//common/hexutil:go_default_library",
"@com_github_fsnotify_fsnotify//:go_default_library",
"@com_github_golang_jwt_jwt_v4//:go_default_library",
"@com_github_golang_protobuf//ptypes/empty",
"@com_github_gorilla_mux//:go_default_library",
"@com_github_grpc_ecosystem_go_grpc_middleware//:go_default_library",
"@com_github_grpc_ecosystem_go_grpc_middleware//recovery:go_default_library",
@@ -88,10 +89,12 @@ go_library(
go_test(
name = "go_default_test",
srcs = [
"accounts_test.go",
"auth_token_test.go",
"beacon_test.go",
"handler_wallet_test.go",
"handlers_accounts_test.go",
"handlers_auth_test.go",
"handlers_beacon_test.go",
"handlers_health_test.go",
"handlers_keymanager_test.go",
"handlers_slashing_test.go",
@@ -100,7 +103,9 @@ go_test(
],
embed = [":go_default_library"],
deps = [
"//api:go_default_library",
"//async/event:go_default_library",
"//beacon-chain/rpc/eth/shared:go_default_library",
"//cmd/validator/flags:go_default_library",
"//config/features:go_default_library",
"//config/fieldparams:go_default_library",
@@ -114,7 +119,6 @@ go_test(
"//io/file:go_default_library",
"//io/logs/mock:go_default_library",
"//proto/prysm/v1alpha1:go_default_library",
"//proto/prysm/v1alpha1/validator-client:go_default_library",
"//testing/assert:go_default_library",
"//testing/require:go_default_library",
"//testing/validator-mock:go_default_library",

View File

@@ -1,197 +0,0 @@
package rpc
import (
"archive/zip"
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/prysmaticlabs/prysm/v4/api/pagination"
"github.com/prysmaticlabs/prysm/v4/cmd"
"github.com/prysmaticlabs/prysm/v4/crypto/bls"
pb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1/validator-client"
"github.com/prysmaticlabs/prysm/v4/validator/accounts"
"github.com/prysmaticlabs/prysm/v4/validator/accounts/petnames"
"github.com/prysmaticlabs/prysm/v4/validator/keymanager"
"github.com/prysmaticlabs/prysm/v4/validator/keymanager/derived"
"github.com/prysmaticlabs/prysm/v4/validator/keymanager/local"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// ListAccounts allows retrieval of validating keys and their petnames
// for a user's wallet via RPC.
// DEPRECATED: Prysm Web UI and associated endpoints will be fully removed in a future hard fork.
func (s *Server) ListAccounts(ctx context.Context, req *pb.ListAccountsRequest) (*pb.ListAccountsResponse, error) {
if s.validatorService == nil {
return nil, status.Error(codes.FailedPrecondition, "Validator service not yet initialized")
}
if !s.walletInitialized {
return nil, status.Error(codes.FailedPrecondition, "Wallet not yet initialized")
}
if int(req.PageSize) > cmd.Get().MaxRPCPageSize {
return nil, status.Errorf(codes.InvalidArgument, "Requested page size %d can not be greater than max size %d",
req.PageSize, cmd.Get().MaxRPCPageSize)
}
km, err := s.validatorService.Keymanager()
if err != nil {
return nil, err
}
keys, err := km.FetchValidatingPublicKeys(ctx)
if err != nil {
return nil, err
}
accs := make([]*pb.Account, len(keys))
for i := 0; i < len(keys); i++ {
accs[i] = &pb.Account{
ValidatingPublicKey: keys[i][:],
AccountName: petnames.DeterministicName(keys[i][:], "-"),
}
if s.wallet.KeymanagerKind() == keymanager.Derived {
accs[i].DerivationPath = fmt.Sprintf(derived.ValidatingKeyDerivationPathTemplate, i)
}
}
if req.All {
return &pb.ListAccountsResponse{
Accounts: accs,
TotalSize: int32(len(keys)),
NextPageToken: "",
}, nil
}
start, end, nextPageToken, err := pagination.StartAndEndPage(req.PageToken, int(req.PageSize), len(keys))
if err != nil {
return nil, status.Errorf(
codes.Internal,
"Could not paginate results: %v",
err,
)
}
return &pb.ListAccountsResponse{
Accounts: accs[start:end],
TotalSize: int32(len(keys)),
NextPageToken: nextPageToken,
}, nil
}
// BackupAccounts creates a zip file containing EIP-2335 keystores for the user's
// specified public keys by encrypting them with the specified password.
// DEPRECATED: Prysm Web UI and associated endpoints will be fully removed in a future hard fork.
func (s *Server) BackupAccounts(
ctx context.Context, req *pb.BackupAccountsRequest,
) (*pb.BackupAccountsResponse, error) {
if s.validatorService == nil {
return nil, status.Error(codes.FailedPrecondition, "Validator service not yet initialized")
}
if req.PublicKeys == nil || len(req.PublicKeys) < 1 {
return nil, status.Error(codes.InvalidArgument, "No public keys specified to backup")
}
if req.BackupPassword == "" {
return nil, status.Error(codes.InvalidArgument, "Backup password cannot be empty")
}
if s.wallet == nil {
return nil, status.Error(codes.FailedPrecondition, "No wallet found")
}
var err error
km, err := s.validatorService.Keymanager()
if err != nil {
return nil, err
}
pubKeys := make([]bls.PublicKey, len(req.PublicKeys))
for i, key := range req.PublicKeys {
pubKey, err := bls.PublicKeyFromBytes(key)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "%#x Not a valid BLS public key: %v", key, err)
}
pubKeys[i] = pubKey
}
var keystoresToBackup []*keymanager.Keystore
switch km := km.(type) {
case *local.Keymanager:
keystoresToBackup, err = km.ExtractKeystores(ctx, pubKeys, req.BackupPassword)
if err != nil {
return nil, status.Errorf(codes.Internal, "Could not backup accounts for local keymanager: %v", err)
}
case *derived.Keymanager:
keystoresToBackup, err = km.ExtractKeystores(ctx, pubKeys, req.BackupPassword)
if err != nil {
return nil, status.Errorf(codes.Internal, "Could not backup accounts for derived keymanager: %v", err)
}
default:
return nil, status.Error(codes.FailedPrecondition, "Only HD or IMPORTED wallets can backup accounts")
}
if len(keystoresToBackup) == 0 {
return nil, status.Error(codes.InvalidArgument, "No keystores to backup")
}
buf := new(bytes.Buffer)
writer := zip.NewWriter(buf)
for i, k := range keystoresToBackup {
encodedFile, err := json.MarshalIndent(k, "", "\t")
if err != nil {
if err := writer.Close(); err != nil {
log.WithError(err).Error("Could not close zip file after writing")
}
return nil, status.Errorf(codes.Internal, "could not marshal keystore to JSON file: %v", err)
}
f, err := writer.Create(fmt.Sprintf("keystore-%d.json", i))
if err != nil {
if err := writer.Close(); err != nil {
log.WithError(err).Error("Could not close zip file after writing")
}
return nil, status.Errorf(codes.Internal, "Could not write keystore file to zip: %v", err)
}
if _, err = f.Write(encodedFile); err != nil {
if err := writer.Close(); err != nil {
log.WithError(err).Error("Could not close zip file after writing")
}
return nil, status.Errorf(codes.Internal, "Could not write keystore file contents")
}
}
if err := writer.Close(); err != nil {
log.WithError(err).Error("Could not close zip file after writing")
}
return &pb.BackupAccountsResponse{
ZipFile: buf.Bytes(),
}, nil
}
// VoluntaryExit performs a voluntary exit for the validator keys specified in a request.
// DEPRECATE: Prysm Web UI and associated endpoints will be fully removed in a future hard fork. There is a similar endpoint that is still used /eth/v1alpha1/validator/exit.
func (s *Server) VoluntaryExit(
ctx context.Context, req *pb.VoluntaryExitRequest,
) (*pb.VoluntaryExitResponse, error) {
if s.validatorService == nil {
return nil, status.Error(codes.FailedPrecondition, "Validator service not yet initialized")
}
if len(req.PublicKeys) == 0 {
return nil, status.Error(codes.InvalidArgument, "No public keys specified to delete")
}
if s.wallet == nil {
return nil, status.Error(codes.FailedPrecondition, "No wallet found")
}
km, err := s.validatorService.Keymanager()
if err != nil {
return nil, err
}
formattedKeys := make([]string, len(req.PublicKeys))
for i, key := range req.PublicKeys {
formattedKeys[i] = fmt.Sprintf("%#x", key)
}
cfg := accounts.PerformExitCfg{
ValidatorClient: s.beaconNodeValidatorClient,
NodeClient: s.beaconNodeClient,
Keymanager: km,
RawPubKeys: req.PublicKeys,
FormattedPubKeys: formattedKeys,
}
rawExitedKeys, _, err := accounts.PerformVoluntaryExit(ctx, cfg)
if err != nil {
return nil, status.Errorf(codes.Internal, "Could not perform voluntary exit: %v", err)
}
return &pb.VoluntaryExitResponse{
ExitedKeys: rawExitedKeys,
}, nil
}

View File

@@ -17,36 +17,15 @@ import (
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/v4/crypto/rand"
"github.com/prysmaticlabs/prysm/v4/io/file"
pb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1/validator-client"
"github.com/prysmaticlabs/prysm/v4/validator/accounts/wallet"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
const (
authTokenFileName = "auth-token"
AuthTokenFileName = "auth-token"
)
// Initialize returns metadata regarding whether the caller has authenticated and has a wallet.
// DEPRECATED: Prysm Web UI and associated endpoints will be fully removed in a future hard fork. Web APIs won't require JWT in the future.
// Still used by Keymanager API until web ui is fully removed.
func (s *Server) Initialize(_ context.Context, _ *emptypb.Empty) (*pb.InitializeAuthResponse, error) {
walletExists, err := wallet.Exists(s.walletDir)
if err != nil {
return nil, status.Error(codes.Internal, "Could not check if wallet exists")
}
authTokenPath := filepath.Join(s.walletDir, authTokenFileName)
return &pb.InitializeAuthResponse{
HasSignedUp: file.Exists(authTokenPath),
HasWallet: walletExists,
}, nil
}
// CreateAuthToken generates a new jwt key, token and writes them
// to a file in the specified directory. Also, it logs out a prepared URL
// for the user to navigate to and authenticate with the Prysm web interface.
// DEPRECATED: associated to Initialize Web UI API
func CreateAuthToken(walletDirPath, validatorWebAddr string) error {
jwtKey, err := createRandomJWTSecret()
if err != nil {
@@ -56,7 +35,7 @@ func CreateAuthToken(walletDirPath, validatorWebAddr string) error {
if err != nil {
return err
}
authTokenPath := filepath.Join(walletDirPath, authTokenFileName)
authTokenPath := filepath.Join(walletDirPath, AuthTokenFileName)
log.Infof("Generating auth token and saving it to %s", authTokenPath)
if err := saveAuthToken(walletDirPath, jwtKey, token); err != nil {
return err
@@ -70,9 +49,8 @@ func CreateAuthToken(walletDirPath, validatorWebAddr string) error {
// user via stdout and the validator client should then attempt to open the default
// browser. The web interface authenticates by looking for this token in the query parameters
// of the URL. This token is then used as the bearer token for jwt auth.
// DEPRECATED: associated to Initialize Web UI API
func (s *Server) initializeAuthToken(walletDir string) (string, error) {
authTokenFile := filepath.Join(walletDir, authTokenFileName)
authTokenFile := filepath.Join(walletDir, AuthTokenFileName)
if file.Exists(authTokenFile) {
// #nosec G304
f, err := os.Open(authTokenFile)
@@ -106,7 +84,6 @@ func (s *Server) initializeAuthToken(walletDir string) (string, error) {
return token, nil
}
// DEPRECATED: associated to Initialize Web UI API
func (s *Server) refreshAuthTokenFromFileChanges(ctx context.Context, authTokenPath string) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
@@ -142,7 +119,6 @@ func (s *Server) refreshAuthTokenFromFileChanges(ctx context.Context, authTokenP
}
}
// DEPRECATED: associated to Initialize Web UI API
func logValidatorWebAuth(validatorWebAddr, token string, tokenPath string) {
webAuthURLTemplate := "http://%s/initialize?token=%s"
webAuthURL := fmt.Sprintf(
@@ -158,9 +134,8 @@ func logValidatorWebAuth(validatorWebAddr, token string, tokenPath string) {
log.Infof("Validator CLient JWT for RPC and REST authentication set at:%s", tokenPath)
}
// DEPRECATED: associated to Initialize Web UI API
func saveAuthToken(walletDirPath string, jwtKey []byte, token string) error {
hashFilePath := filepath.Join(walletDirPath, authTokenFileName)
hashFilePath := filepath.Join(walletDirPath, AuthTokenFileName)
bytesBuf := new(bytes.Buffer)
if _, err := bytesBuf.Write([]byte(fmt.Sprintf("%x", jwtKey))); err != nil {
return err
@@ -177,7 +152,6 @@ func saveAuthToken(walletDirPath string, jwtKey []byte, token string) error {
return file.WriteFile(hashFilePath, bytesBuf.Bytes())
}
// DEPRECATED: associated to Initialize Web UI API
func readAuthTokenFile(r io.Reader) (secret []byte, token string, err error) {
br := bufio.NewReader(r)
var jwtKeyHex string
@@ -198,7 +172,6 @@ func readAuthTokenFile(r io.Reader) (secret []byte, token string, err error) {
}
// Creates a JWT token string using the JWT key.
// DEPRECATED: associated to Initialize Web UI API
func createTokenString(jwtKey []byte) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{})
// Sign and get the complete encoded token as a string using the secret

View File

@@ -64,7 +64,7 @@ func TestServer_RefreshJWTSecretOnFileChange(t *testing.T) {
currentSecret := srv.jwtSecret
require.Equal(t, true, len(currentSecret) > 0)
authTokenPath := filepath.Join(walletDir, authTokenFileName)
authTokenPath := filepath.Join(walletDir, AuthTokenFileName)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -81,7 +81,7 @@ func TestServer_RefreshJWTSecretOnFileChange(t *testing.T) {
newSecret := srv.jwtSecret
require.Equal(t, true, len(newSecret) > 0)
require.Equal(t, true, !bytes.Equal(currentSecret, newSecret))
err = os.Remove(authTokenFileName)
err = os.Remove(AuthTokenFileName)
require.NoError(t, err)
}

View File

@@ -1,10 +1,6 @@
package rpc
import (
"context"
"time"
"github.com/golang/protobuf/ptypes/empty"
middleware "github.com/grpc-ecosystem/go-grpc-middleware"
grpcretry "github.com/grpc-ecosystem/go-grpc-middleware/retry"
grpcopentracing "github.com/grpc-ecosystem/go-grpc-middleware/tracing/opentracing"
@@ -12,14 +8,12 @@ import (
"github.com/pkg/errors"
grpcutil "github.com/prysmaticlabs/prysm/v4/api/grpc"
ethpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1"
validatorpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1/validator-client"
"github.com/prysmaticlabs/prysm/v4/validator/client"
beaconChainClientFactory "github.com/prysmaticlabs/prysm/v4/validator/client/beacon-chain-client-factory"
nodeClientFactory "github.com/prysmaticlabs/prysm/v4/validator/client/node-client-factory"
validatorClientFactory "github.com/prysmaticlabs/prysm/v4/validator/client/validator-client-factory"
validatorHelpers "github.com/prysmaticlabs/prysm/v4/validator/helpers"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/emptypb"
)
// Initialize a client connect to a beacon node gRPC endpoint.
@@ -62,85 +56,3 @@ func (s *Server) registerBeaconClient() error {
s.beaconNodeValidatorClient = validatorClientFactory.NewValidatorClient(conn)
return nil
}
// GetBeaconStatus retrieves information about the beacon node gRPC connection
// and certain chain metadata, such as the genesis time, the chain head, and the
// deposit contract address.
// DEPRECATED: Prysm Web UI and associated endpoints will be fully removed in a future hard fork.
func (s *Server) GetBeaconStatus(ctx context.Context, _ *empty.Empty) (*validatorpb.BeaconStatusResponse, error) {
syncStatus, err := s.beaconNodeClient.GetSyncStatus(ctx, &emptypb.Empty{})
if err != nil {
//nolint:nilerr
return &validatorpb.BeaconStatusResponse{
BeaconNodeEndpoint: s.nodeGatewayEndpoint,
Connected: false,
Syncing: false,
}, nil
}
genesis, err := s.beaconNodeClient.GetGenesis(ctx, &emptypb.Empty{})
if err != nil {
return nil, err
}
genesisTime := uint64(time.Unix(genesis.GenesisTime.Seconds, 0).Unix())
address := genesis.DepositContractAddress
chainHead, err := s.beaconChainClient.GetChainHead(ctx, &emptypb.Empty{})
if err != nil {
return nil, err
}
return &validatorpb.BeaconStatusResponse{
BeaconNodeEndpoint: s.beaconClientEndpoint,
Connected: true,
Syncing: syncStatus.Syncing,
GenesisTime: genesisTime,
DepositContractAddress: address,
ChainHead: chainHead,
}, nil
}
// GetValidatorParticipation is a wrapper around the /eth/v1alpha1 endpoint of the same name.
// DEPRECATED: Prysm Web UI and associated endpoints will be fully removed in a future hard fork.
func (s *Server) GetValidatorParticipation(
ctx context.Context, req *ethpb.GetValidatorParticipationRequest,
) (*ethpb.ValidatorParticipationResponse, error) {
return s.beaconChainClient.GetValidatorParticipation(ctx, req)
}
// GetValidatorPerformance is a wrapper around the /eth/v1alpha1 endpoint of the same name.
// DEPRECATED: Prysm Web UI and associated endpoints will be fully removed in a future hard fork.
func (s *Server) GetValidatorPerformance(
ctx context.Context, req *ethpb.ValidatorPerformanceRequest,
) (*ethpb.ValidatorPerformanceResponse, error) {
return s.beaconChainClient.GetValidatorPerformance(ctx, req)
}
// GetValidatorBalances is a wrapper around the /eth/v1alpha1 endpoint of the same name.
// DEPRECATED: Prysm Web UI and associated endpoints will be fully removed in a future hard fork.
func (s *Server) GetValidatorBalances(
ctx context.Context, req *ethpb.ListValidatorBalancesRequest,
) (*ethpb.ValidatorBalances, error) {
return s.beaconChainClient.ListValidatorBalances(ctx, req)
}
// GetValidators is a wrapper around the /eth/v1alpha1 endpoint of the same name.
// DEPRECATED: Prysm Web UI and associated endpoints will be fully removed in a future hard fork.
func (s *Server) GetValidators(
ctx context.Context, req *ethpb.ListValidatorsRequest,
) (*ethpb.Validators, error) {
return s.beaconChainClient.ListValidators(ctx, req)
}
// GetValidatorQueue is a wrapper around the /eth/v1alpha1 endpoint of the same name.
// DEPRECATED: Prysm Web UI and associated endpoints will be fully removed in a future hard fork.
func (s *Server) GetValidatorQueue(
ctx context.Context, _ *empty.Empty,
) (*ethpb.ValidatorQueue, error) {
return s.beaconChainClient.GetValidatorQueue(ctx, &emptypb.Empty{})
}
// GetPeers is a wrapper around the /eth/v1alpha1 endpoint of the same name.
// DEPRECATED: Prysm Web UI and associated endpoints will be fully removed in a future hard fork.
func (s *Server) GetPeers(
ctx context.Context, _ *empty.Empty,
) (*ethpb.Peers, error) {
return s.beaconNodeClient.ListPeers(ctx, &emptypb.Empty{})
}

View File

@@ -3,83 +3,12 @@ package rpc
import (
"context"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/golang/protobuf/ptypes/empty"
"github.com/pkg/errors"
ethpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1"
pb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1/validator-client"
"github.com/prysmaticlabs/prysm/v4/testing/assert"
"github.com/prysmaticlabs/prysm/v4/testing/require"
validatormock "github.com/prysmaticlabs/prysm/v4/testing/validator-mock"
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/types/known/timestamppb"
)
func TestGetBeaconStatus_NotConnected(t *testing.T) {
ctrl := gomock.NewController(t)
nodeClient := validatormock.NewMockNodeClient(ctrl)
nodeClient.EXPECT().GetSyncStatus(
gomock.Any(), // ctx
gomock.Any(),
).Return(nil /*response*/, errors.New("uh oh"))
srv := &Server{
beaconNodeClient: nodeClient,
}
ctx := context.Background()
resp, err := srv.GetBeaconStatus(ctx, &empty.Empty{})
require.NoError(t, err)
want := &pb.BeaconStatusResponse{
BeaconNodeEndpoint: "",
Connected: false,
Syncing: false,
}
assert.DeepEqual(t, want, resp)
}
func TestGetBeaconStatus_OK(t *testing.T) {
ctrl := gomock.NewController(t)
nodeClient := validatormock.NewMockNodeClient(ctrl)
beaconChainClient := validatormock.NewMockBeaconChainClient(ctrl)
nodeClient.EXPECT().GetSyncStatus(
gomock.Any(), // ctx
gomock.Any(),
).Return(&ethpb.SyncStatus{Syncing: true}, nil)
timeStamp := timestamppb.New(time.Unix(0, 0))
nodeClient.EXPECT().GetGenesis(
gomock.Any(), // ctx
gomock.Any(),
).Return(&ethpb.Genesis{
GenesisTime: timeStamp,
DepositContractAddress: []byte("hello"),
}, nil)
beaconChainClient.EXPECT().GetChainHead(
gomock.Any(), // ctx
gomock.Any(),
).Return(&ethpb.ChainHead{
HeadEpoch: 1,
}, nil)
srv := &Server{
beaconNodeClient: nodeClient,
beaconChainClient: beaconChainClient,
}
ctx := context.Background()
resp, err := srv.GetBeaconStatus(ctx, &empty.Empty{})
require.NoError(t, err)
want := &pb.BeaconStatusResponse{
BeaconNodeEndpoint: "",
Connected: true,
Syncing: true,
GenesisTime: uint64(time.Unix(0, 0).Unix()),
DepositContractAddress: []byte("hello"),
ChainHead: &ethpb.ChainHead{
HeadEpoch: 1,
},
}
assert.DeepEqual(t, want, resp)
}
func TestGrpcHeaders(t *testing.T) {
s := &Server{
ctx: context.Background(),

View File

@@ -0,0 +1,274 @@
package rpc
import (
"archive/zip"
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/v4/api/pagination"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/eth/shared"
"github.com/prysmaticlabs/prysm/v4/cmd"
fieldparams "github.com/prysmaticlabs/prysm/v4/config/fieldparams"
"github.com/prysmaticlabs/prysm/v4/crypto/bls"
"github.com/prysmaticlabs/prysm/v4/encoding/bytesutil"
httputil "github.com/prysmaticlabs/prysm/v4/network/http"
"github.com/prysmaticlabs/prysm/v4/validator/accounts"
"github.com/prysmaticlabs/prysm/v4/validator/accounts/petnames"
"github.com/prysmaticlabs/prysm/v4/validator/keymanager"
"github.com/prysmaticlabs/prysm/v4/validator/keymanager/derived"
"github.com/prysmaticlabs/prysm/v4/validator/keymanager/local"
"go.opencensus.io/trace"
)
// ListAccounts allows retrieval of validating keys and their petnames
// for a user's wallet via RPC.
func (s *Server) ListAccounts(w http.ResponseWriter, r *http.Request) {
ctx, span := trace.StartSpan(r.Context(), "validator.web.accounts.ListAccounts")
defer span.End()
if s.validatorService == nil {
httputil.HandleError(w, "Validator service not ready.", http.StatusServiceUnavailable)
return
}
if !s.walletInitialized {
httputil.HandleError(w, "Prysm Wallet not initialized. Please create a new wallet.", http.StatusServiceUnavailable)
return
}
pageSize := r.URL.Query().Get("page_size")
var ps int64
if pageSize != "" {
psi, err := strconv.ParseInt(pageSize, 10, 32)
if err != nil {
httputil.HandleError(w, errors.Wrap(err, "Failed to parse page_size").Error(), http.StatusBadRequest)
return
}
ps = psi
}
pageToken := r.URL.Query().Get("page_token")
publicKeys := r.URL.Query()["public_keys"]
pubkeys := make([][]byte, len(publicKeys))
for i, key := range publicKeys {
k, ok := shared.ValidateHex(w, fmt.Sprintf("PublicKeys[%d]", i), key, fieldparams.BLSPubkeyLength)
if !ok {
return
}
pubkeys[i] = bytesutil.SafeCopyBytes(k)
}
if int(ps) > cmd.Get().MaxRPCPageSize {
httputil.HandleError(w, fmt.Sprintf("Requested page size %d can not be greater than max size %d",
ps, cmd.Get().MaxRPCPageSize), http.StatusBadRequest)
return
}
km, err := s.validatorService.Keymanager()
if err != nil {
httputil.HandleError(w, err.Error(), http.StatusInternalServerError)
return
}
keys, err := km.FetchValidatingPublicKeys(ctx)
if err != nil {
httputil.HandleError(w, errors.Errorf("Could not retrieve public keys: %v", err).Error(), http.StatusInternalServerError)
return
}
accs := make([]*Account, len(keys))
for i := 0; i < len(keys); i++ {
accs[i] = &Account{
ValidatingPublicKey: hexutil.Encode(keys[i][:]),
AccountName: petnames.DeterministicName(keys[i][:], "-"),
}
if s.wallet.KeymanagerKind() == keymanager.Derived {
accs[i].DerivationPath = fmt.Sprintf(derived.ValidatingKeyDerivationPathTemplate, i)
}
}
if r.URL.Query().Get("all") == "true" {
httputil.WriteJson(w, &ListAccountsResponse{
Accounts: accs,
TotalSize: int32(len(keys)),
NextPageToken: "",
})
return
}
start, end, nextPageToken, err := pagination.StartAndEndPage(pageToken, int(ps), len(keys))
if err != nil {
httputil.HandleError(w, fmt.Errorf("Could not paginate results: %v",
err).Error(), http.StatusInternalServerError)
return
}
httputil.WriteJson(w, &ListAccountsResponse{
Accounts: accs[start:end],
TotalSize: int32(len(keys)),
NextPageToken: nextPageToken,
})
}
// BackupAccounts creates a zip file containing EIP-2335 keystores for the user's
// specified public keys by encrypting them with the specified password.
func (s *Server) BackupAccounts(w http.ResponseWriter, r *http.Request) {
ctx, span := trace.StartSpan(r.Context(), "validator.web.accounts.ListAccounts")
defer span.End()
if s.validatorService == nil {
httputil.HandleError(w, "Validator service not ready.", http.StatusServiceUnavailable)
return
}
if !s.walletInitialized {
httputil.HandleError(w, "Prysm Wallet not initialized. Please create a new wallet.", http.StatusServiceUnavailable)
return
}
var req BackupAccountsRequest
err := json.NewDecoder(r.Body).Decode(&req)
switch {
case err == io.EOF:
httputil.HandleError(w, "No data submitted", http.StatusBadRequest)
return
case err != nil:
httputil.HandleError(w, "Could not decode request body: "+err.Error(), http.StatusBadRequest)
return
}
if req.PublicKeys == nil || len(req.PublicKeys) < 1 {
httputil.HandleError(w, "No public keys specified to backup", http.StatusBadRequest)
return
}
if req.BackupPassword == "" {
httputil.HandleError(w, "Backup password cannot be empty", http.StatusBadRequest)
return
}
km, err := s.validatorService.Keymanager()
if err != nil {
httputil.HandleError(w, err.Error(), http.StatusInternalServerError)
return
}
pubKeys := make([]bls.PublicKey, len(req.PublicKeys))
for i, key := range req.PublicKeys {
byteskey, ok := shared.ValidateHex(w, "pubkey", key, fieldparams.BLSPubkeyLength)
if !ok {
return
}
pubKey, err := bls.PublicKeyFromBytes(byteskey)
if err != nil {
httputil.HandleError(w, errors.Wrap(err, fmt.Sprintf("%s Not a valid BLS public key", key)).Error(), http.StatusBadRequest)
return
}
pubKeys[i] = pubKey
}
var keystoresToBackup []*keymanager.Keystore
switch km := km.(type) {
case *local.Keymanager:
keystoresToBackup, err = km.ExtractKeystores(ctx, pubKeys, req.BackupPassword)
if err != nil {
httputil.HandleError(w, errors.Wrap(err, "Could not backup accounts for local keymanager").Error(), http.StatusInternalServerError)
return
}
case *derived.Keymanager:
keystoresToBackup, err = km.ExtractKeystores(ctx, pubKeys, req.BackupPassword)
if err != nil {
httputil.HandleError(w, errors.Wrap(err, "Could not backup accounts for derived keymanager").Error(), http.StatusInternalServerError)
return
}
default:
httputil.HandleError(w, "Only HD or IMPORTED wallets can backup accounts", http.StatusBadRequest)
return
}
if len(keystoresToBackup) == 0 {
httputil.HandleError(w, "No keystores to backup", http.StatusBadRequest)
return
}
buf := new(bytes.Buffer)
writer := zip.NewWriter(buf)
for i, k := range keystoresToBackup {
encodedFile, err := json.MarshalIndent(k, "", "\t")
if err != nil {
if err := writer.Close(); err != nil {
log.WithError(err).Error("Could not close zip file after writing")
}
httputil.HandleError(w, "could not marshal keystore to JSON file", http.StatusInternalServerError)
return
}
f, err := writer.Create(fmt.Sprintf("keystore-%d.json", i))
if err != nil {
if err := writer.Close(); err != nil {
log.WithError(err).Error("Could not close zip file after writing")
}
httputil.HandleError(w, "Could not write keystore file to zip", http.StatusInternalServerError)
return
}
if _, err = f.Write(encodedFile); err != nil {
if err := writer.Close(); err != nil {
log.WithError(err).Error("Could not close zip file after writing")
}
httputil.HandleError(w, "Could not write keystore file contents", http.StatusBadRequest)
return
}
}
if err := writer.Close(); err != nil {
log.WithError(err).Error("Could not close zip file after writing")
}
httputil.WriteJson(w, &BackupAccountsResponse{
ZipFile: base64.StdEncoding.EncodeToString(buf.Bytes()), // convert to base64 string for processing
})
}
// VoluntaryExit performs a voluntary exit for the validator keys specified in a request.
func (s *Server) VoluntaryExit(w http.ResponseWriter, r *http.Request) {
ctx, span := trace.StartSpan(r.Context(), "validator.web.accounts.VoluntaryExit")
defer span.End()
if s.validatorService == nil {
httputil.HandleError(w, "Validator service not ready.", http.StatusServiceUnavailable)
return
}
if !s.walletInitialized {
httputil.HandleError(w, "Prysm Wallet not initialized. Please create a new wallet.", http.StatusServiceUnavailable)
return
}
var req VoluntaryExitRequest
err := json.NewDecoder(r.Body).Decode(&req)
switch {
case err == io.EOF:
httputil.HandleError(w, "No data submitted", http.StatusBadRequest)
return
case err != nil:
httputil.HandleError(w, "Could not decode request body: "+err.Error(), http.StatusBadRequest)
return
}
if len(req.PublicKeys) == 0 {
httputil.HandleError(w, "No public keys specified to delete", http.StatusBadRequest)
return
}
km, err := s.validatorService.Keymanager()
if err != nil {
httputil.HandleError(w, err.Error(), http.StatusInternalServerError)
return
}
pubKeys := make([][]byte, len(req.PublicKeys))
for i, key := range req.PublicKeys {
byteskey, ok := shared.ValidateHex(w, "pubkey", key, fieldparams.BLSPubkeyLength)
if !ok {
return
}
pubKeys[i] = byteskey
}
cfg := accounts.PerformExitCfg{
ValidatorClient: s.beaconNodeValidatorClient,
NodeClient: s.beaconNodeClient,
Keymanager: km,
RawPubKeys: pubKeys,
FormattedPubKeys: req.PublicKeys,
}
rawExitedKeys, _, err := accounts.PerformVoluntaryExit(ctx, cfg)
if err != nil {
httputil.HandleError(w, errors.Wrap(err, "Could not perform voluntary exit").Error(), http.StatusInternalServerError)
return
}
httputil.WriteJson(w, &VoluntaryExitResponse{
ExitedKeys: rawExitedKeys,
})
}

View File

@@ -4,17 +4,21 @@ import (
"archive/zip"
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"time"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/golang/mock/gomock"
"github.com/prysmaticlabs/prysm/v4/api"
"github.com/prysmaticlabs/prysm/v4/cmd/validator/flags"
ethpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1"
pb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1/validator-client"
"github.com/prysmaticlabs/prysm/v4/testing/assert"
"github.com/prysmaticlabs/prysm/v4/testing/require"
validatormock "github.com/prysmaticlabs/prysm/v4/testing/validator-mock"
@@ -66,42 +70,74 @@ func TestServer_ListAccounts(t *testing.T) {
require.Equal(t, true, ok)
err = dr.RecoverAccountsFromMnemonic(ctx, constant.TestMnemonic, derived.DefaultMnemonicLanguage, "", numAccounts)
require.NoError(t, err)
resp, err := s.ListAccounts(ctx, &pb.ListAccountsRequest{
PageSize: int32(numAccounts),
})
require.NoError(t, err)
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf(api.WebUrlPrefix+"accounts?page_size=%d", int32(numAccounts)), nil)
wr := httptest.NewRecorder()
wr.Body = &bytes.Buffer{}
s.ListAccounts(wr, req)
require.Equal(t, http.StatusOK, wr.Code)
resp := &ListAccountsResponse{}
require.NoError(t, json.Unmarshal(wr.Body.Bytes(), resp))
require.Equal(t, len(resp.Accounts), numAccounts)
tests := []struct {
req *pb.ListAccountsRequest
res *pb.ListAccountsResponse
PageSize int
PageToken string
All bool
res *ListAccountsResponse
}{
{
req: &pb.ListAccountsRequest{
PageSize: 5,
},
res: &pb.ListAccountsResponse{
PageSize: 5,
res: &ListAccountsResponse{
Accounts: resp.Accounts[0:5],
NextPageToken: "1",
TotalSize: int32(numAccounts),
},
},
{
req: &pb.ListAccountsRequest{
PageSize: 5,
PageToken: "1",
},
res: &pb.ListAccountsResponse{
PageSize: 5,
PageToken: "1",
res: &ListAccountsResponse{
Accounts: resp.Accounts[5:10],
NextPageToken: "2",
TotalSize: int32(numAccounts),
},
},
{
All: true,
res: &ListAccountsResponse{
Accounts: resp.Accounts,
NextPageToken: "",
TotalSize: int32(numAccounts),
},
},
}
for _, test := range tests {
res, err := s.ListAccounts(context.Background(), test.req)
require.NoError(t, err)
assert.DeepEqual(t, res, test.res)
url := api.WebUrlPrefix + "accounts"
if test.PageSize != 0 || test.PageToken != "" || test.All {
url = url + "?"
}
if test.All {
url = url + "all=true"
} else {
if test.PageSize != 0 {
url = url + fmt.Sprintf("page_size=%d", test.PageSize)
}
if test.PageToken != "" {
url = url + fmt.Sprintf("&page_token=%s", test.PageToken)
}
}
req = httptest.NewRequest(http.MethodGet, url, nil)
wr = httptest.NewRecorder()
wr.Body = &bytes.Buffer{}
s.ListAccounts(wr, req)
require.Equal(t, http.StatusOK, wr.Code)
resp = &ListAccountsResponse{}
require.NoError(t, json.Unmarshal(wr.Body.Bytes(), resp))
assert.DeepEqual(t, resp, test.res)
}
}
@@ -139,32 +175,45 @@ func TestServer_BackupAccounts(t *testing.T) {
require.Equal(t, true, ok)
err = dr.RecoverAccountsFromMnemonic(ctx, constant.TestMnemonic, derived.DefaultMnemonicLanguage, "", numAccounts)
require.NoError(t, err)
resp, err := s.ListAccounts(ctx, &pb.ListAccountsRequest{
PageSize: int32(numAccounts),
})
require.NoError(t, err)
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/v2/validator/accounts?page_size=%d", int32(numAccounts)), nil)
wr := httptest.NewRecorder()
wr.Body = &bytes.Buffer{}
s.ListAccounts(wr, req)
require.Equal(t, http.StatusOK, wr.Code)
resp := &ListAccountsResponse{}
require.NoError(t, json.Unmarshal(wr.Body.Bytes(), resp))
require.Equal(t, len(resp.Accounts), numAccounts)
pubKeys := make([][]byte, numAccounts)
pubKeys := make([]string, numAccounts)
for i, aa := range resp.Accounts {
pubKeys[i] = aa.ValidatingPublicKey
}
// We now attempt to backup all public keys from the wallet.
res, err := s.BackupAccounts(context.Background(), &pb.BackupAccountsRequest{
request := &BackupAccountsRequest{
PublicKeys: pubKeys,
BackupPassword: s.wallet.Password(),
})
}
var buf bytes.Buffer
err = json.NewEncoder(&buf).Encode(request)
require.NoError(t, err)
req = httptest.NewRequest(http.MethodPost, api.WebUrlPrefix+"accounts/backup", &buf)
wr = httptest.NewRecorder()
wr.Body = &bytes.Buffer{}
// We now attempt to backup all public keys from the wallet.
s.BackupAccounts(wr, req)
require.Equal(t, http.StatusOK, wr.Code)
res := &BackupAccountsResponse{}
require.NoError(t, json.Unmarshal(wr.Body.Bytes(), res))
// decode the base64 string
decodedBytes, err := base64.StdEncoding.DecodeString(res.ZipFile)
require.NoError(t, err)
require.NotNil(t, res.ZipFile)
// Open a zip archive for reading.
buf := bytes.NewReader(res.ZipFile)
r, err := zip.NewReader(buf, int64(len(res.ZipFile)))
bu := bytes.NewReader(decodedBytes)
r, err := zip.NewReader(bu, int64(len(decodedBytes)))
require.NoError(t, err)
require.Equal(t, len(pubKeys), len(r.File))
// Iterate through the files in the archive, checking they
// match the keystores we wanted to backup.
// match the keystores we wanted to back up.
for i, f := range r.File {
keystoreFile, err := f.Open()
require.NoError(t, err)
@@ -178,7 +227,7 @@ func TestServer_BackupAccounts(t *testing.T) {
require.NoError(t, keystoreFile.Close())
t.Fatal(err)
}
assert.Equal(t, keystore.Pubkey, fmt.Sprintf("%x", pubKeys[i]))
assert.Equal(t, "0x"+keystore.Pubkey, pubKeys[i])
require.NoError(t, keystoreFile.Close())
}
}
@@ -255,13 +304,25 @@ func TestServer_VoluntaryExit(t *testing.T) {
pubKeys, err := dr.FetchValidatingPublicKeys(ctx)
require.NoError(t, err)
rawPubKeys := make([][]byte, len(pubKeys))
rawPubKeys := make([]string, len(pubKeys))
for i, key := range pubKeys {
rawPubKeys[i] = key[:]
rawPubKeys[i] = hexutil.Encode(key[:])
}
res, err := s.VoluntaryExit(ctx, &pb.VoluntaryExitRequest{
request := &VoluntaryExitRequest{
PublicKeys: rawPubKeys,
})
}
var buf bytes.Buffer
err = json.NewEncoder(&buf).Encode(request)
require.NoError(t, err)
require.DeepEqual(t, rawPubKeys, res.ExitedKeys)
req := httptest.NewRequest(http.MethodPost, api.WebUrlPrefix+"accounts/voluntary-exit", &buf)
wr := httptest.NewRecorder()
wr.Body = &bytes.Buffer{}
s.VoluntaryExit(wr, req)
require.Equal(t, http.StatusOK, wr.Code)
res := &VoluntaryExitResponse{}
require.NoError(t, json.Unmarshal(wr.Body.Bytes(), res))
for i := range res.ExitedKeys {
require.Equal(t, rawPubKeys[i], hexutil.Encode(res.ExitedKeys[i]))
}
}

View File

@@ -0,0 +1,28 @@
package rpc
import (
"net/http"
"path/filepath"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/v4/io/file"
httputil "github.com/prysmaticlabs/prysm/v4/network/http"
"github.com/prysmaticlabs/prysm/v4/validator/accounts/wallet"
"go.opencensus.io/trace"
)
// Initialize returns metadata regarding whether the caller has authenticated and has a wallet.
func (s *Server) Initialize(w http.ResponseWriter, r *http.Request) {
_, span := trace.StartSpan(r.Context(), "validator.web.Initialize")
defer span.End()
walletExists, err := wallet.Exists(s.walletDir)
if err != nil {
httputil.HandleError(w, errors.Wrap(err, "Could not check if wallet exists").Error(), http.StatusInternalServerError)
return
}
authTokenPath := filepath.Join(s.walletDir, AuthTokenFileName)
httputil.WriteJson(w, &InitializeAuthResponse{
HasSignedUp: file.Exists(authTokenPath),
HasWallet: walletExists,
})
}

View File

@@ -0,0 +1,60 @@
package rpc
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/prysmaticlabs/prysm/v4/testing/require"
"github.com/prysmaticlabs/prysm/v4/validator/accounts"
"github.com/prysmaticlabs/prysm/v4/validator/keymanager"
)
func TestInitialize(t *testing.T) {
// Step 1: Create a temporary directory
localWalletDir := setupWalletDir(t)
// Step 2: Optionally create a temporary 'auth-token' file
authTokenPath := filepath.Join(localWalletDir, AuthTokenFileName)
_, err := os.Create(authTokenPath)
require.NoError(t, err)
// Create an instance of the Server with the temporary directory
opts := []accounts.Option{
accounts.WithWalletDir(localWalletDir),
accounts.WithKeymanagerType(keymanager.Derived),
accounts.WithWalletPassword(strongPass),
accounts.WithSkipMnemonicConfirm(true),
}
acc, err := accounts.NewCLIManager(opts...)
require.NoError(t, err)
_, err = acc.WalletCreate(context.Background())
require.NoError(t, err)
server := &Server{walletDir: localWalletDir}
// Step 4: Create an HTTP request and response recorder
req := httptest.NewRequest(http.MethodGet, "/initialize", nil)
w := httptest.NewRecorder()
// Step 5: Call the Initialize function
server.Initialize(w, req)
// Step 6: Assert expectations
result := w.Result()
defer func() {
err := result.Body.Close()
require.NoError(t, err)
}()
var response InitializeAuthResponse
err = json.NewDecoder(result.Body).Decode(&response)
require.NoError(t, err)
// Assert the expected response
require.Equal(t, true, response.HasSignedUp)
require.Equal(t, true, response.HasWallet)
}

View File

@@ -0,0 +1,208 @@
package rpc
import (
"encoding/base64"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/eth/shared"
fieldparams "github.com/prysmaticlabs/prysm/v4/config/fieldparams"
"github.com/prysmaticlabs/prysm/v4/encoding/bytesutil"
httputil "github.com/prysmaticlabs/prysm/v4/network/http"
ethpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1"
"go.opencensus.io/trace"
"google.golang.org/protobuf/types/known/emptypb"
)
// GetBeaconStatus retrieves information about the beacon node gRPC connection
// and certain chain metadata, such as the genesis time, the chain head, and the
// deposit contract address.
func (s *Server) GetBeaconStatus(w http.ResponseWriter, r *http.Request) {
ctx, span := trace.StartSpan(r.Context(), "validator.web.beacon.GetBeaconStatus")
defer span.End()
syncStatus, err := s.beaconNodeClient.GetSyncStatus(ctx, &emptypb.Empty{})
if err != nil {
log.WithError(err).Error("beacon node call to get sync status failed")
httputil.WriteJson(w, &BeaconStatusResponse{
BeaconNodeEndpoint: s.nodeGatewayEndpoint,
Connected: false,
Syncing: false,
})
return
}
genesis, err := s.beaconNodeClient.GetGenesis(ctx, &emptypb.Empty{})
if err != nil {
httputil.HandleError(w, errors.Wrap(err, "GetGenesis call failed").Error(), http.StatusInternalServerError)
return
}
genesisTime := uint64(time.Unix(genesis.GenesisTime.Seconds, 0).Unix())
address := genesis.DepositContractAddress
chainHead, err := s.beaconChainClient.GetChainHead(ctx, &emptypb.Empty{})
if err != nil {
httputil.HandleError(w, errors.Wrap(err, "GetChainHead").Error(), http.StatusInternalServerError)
return
}
httputil.WriteJson(w, &BeaconStatusResponse{
BeaconNodeEndpoint: s.beaconClientEndpoint,
Connected: true,
Syncing: syncStatus.Syncing,
GenesisTime: fmt.Sprintf("%d", genesisTime),
DepositContractAddress: hexutil.Encode(address),
ChainHead: shared.ChainHeadResponseFromConsensus(chainHead),
})
}
// GetValidatorPerformance is a wrapper around the /eth/v1alpha1 endpoint of the same name.
func (s *Server) GetValidatorPerformance(w http.ResponseWriter, r *http.Request) {
ctx, span := trace.StartSpan(r.Context(), "validator.web.beacon.GetValidatorPerformance")
defer span.End()
publicKeys := r.URL.Query()["public_keys"]
pubkeys := make([][]byte, len(publicKeys))
for i, key := range publicKeys {
var pk []byte
if strings.HasPrefix(key, "0x") {
k, ok := shared.ValidateHex(w, fmt.Sprintf("PublicKeys[%d]", i), key, fieldparams.BLSPubkeyLength)
if !ok {
return
}
pk = bytesutil.SafeCopyBytes(k)
} else {
data, err := base64.StdEncoding.DecodeString(key)
if err != nil {
httputil.HandleError(w, errors.Wrap(err, "Failed to decode base64").Error(), http.StatusBadRequest)
return
}
pk = bytesutil.SafeCopyBytes(data)
}
pubkeys[i] = pk
}
req := &ethpb.ValidatorPerformanceRequest{
PublicKeys: pubkeys,
}
validatorPerformance, err := s.beaconChainClient.GetValidatorPerformance(ctx, req)
if err != nil {
httputil.HandleError(w, errors.Wrap(err, "GetValidatorPerformance call failed").Error(), http.StatusInternalServerError)
return
}
httputil.WriteJson(w, shared.ValidatorPerformanceResponseFromConsensus(validatorPerformance))
}
// GetValidatorBalances is a wrapper around the /eth/v1alpha1 endpoint of the same name.
func (s *Server) GetValidatorBalances(w http.ResponseWriter, r *http.Request) {
ctx, span := trace.StartSpan(r.Context(), "validator.web.beacon.GetValidatorBalances")
defer span.End()
pageSize := r.URL.Query().Get("page_size")
var ps int64
if pageSize != "" {
psi, err := strconv.ParseInt(pageSize, 10, 32)
if err != nil {
httputil.HandleError(w, errors.Wrap(err, "Failed to parse page_size").Error(), http.StatusBadRequest)
return
}
ps = psi
}
pageToken := r.URL.Query().Get("page_token")
publicKeys := r.URL.Query()["public_keys"]
pubkeys := make([][]byte, len(publicKeys))
for i, key := range publicKeys {
var pk []byte
if strings.HasPrefix(key, "0x") {
k, ok := shared.ValidateHex(w, fmt.Sprintf("PublicKeys[%d]", i), key, fieldparams.BLSPubkeyLength)
if !ok {
return
}
pk = bytesutil.SafeCopyBytes(k)
} else {
data, err := base64.StdEncoding.DecodeString(key)
if err != nil {
httputil.HandleError(w, errors.Wrap(err, "Failed to decode base64").Error(), http.StatusBadRequest)
return
}
pk = bytesutil.SafeCopyBytes(data)
}
pubkeys[i] = pk
}
req := &ethpb.ListValidatorBalancesRequest{
PublicKeys: pubkeys,
PageSize: int32(ps),
PageToken: pageToken,
}
listValidatorBalances, err := s.beaconChainClient.ListValidatorBalances(ctx, req)
if err != nil {
httputil.HandleError(w, errors.Wrap(err, "ListValidatorBalances call failed").Error(), http.StatusInternalServerError)
return
}
response, err := shared.ValidatorBalancesResponseFromConsensus(listValidatorBalances)
if err != nil {
httputil.HandleError(w, errors.Wrap(err, "Failed to convert to json").Error(), http.StatusInternalServerError)
return
}
httputil.WriteJson(w, response)
}
// GetValidators is a wrapper around the /eth/v1alpha1 endpoint of the same name.
func (s *Server) GetValidators(w http.ResponseWriter, r *http.Request) {
ctx, span := trace.StartSpan(r.Context(), "validator.web.beacon.GetValidators")
defer span.End()
pageSize := r.URL.Query().Get("page_size")
ps, err := strconv.ParseInt(pageSize, 10, 32)
if err != nil {
httputil.HandleError(w, errors.Wrap(err, "Failed to parse page_size").Error(), http.StatusBadRequest)
return
}
pageToken := r.URL.Query().Get("page_token")
publicKeys := r.URL.Query()["public_keys"]
pubkeys := make([][]byte, len(publicKeys))
for i, key := range publicKeys {
var pk []byte
if strings.HasPrefix(key, "0x") {
k, ok := shared.ValidateHex(w, fmt.Sprintf("PublicKeys[%d]", i), key, fieldparams.BLSPubkeyLength)
if !ok {
return
}
pk = bytesutil.SafeCopyBytes(k)
} else {
data, err := base64.StdEncoding.DecodeString(key)
if err != nil {
httputil.HandleError(w, errors.Wrap(err, "Failed to decode base64").Error(), http.StatusBadRequest)
return
}
pk = bytesutil.SafeCopyBytes(data)
}
pubkeys[i] = pk
}
req := &ethpb.ListValidatorsRequest{
PublicKeys: pubkeys,
PageSize: int32(ps),
PageToken: pageToken,
}
validators, err := s.beaconChainClient.ListValidators(ctx, req)
if err != nil {
httputil.HandleError(w, errors.Wrap(err, "ListValidators call failed").Error(), http.StatusInternalServerError)
return
}
response, err := shared.ValidatorsResponseFromConsensus(validators)
if err != nil {
httputil.HandleError(w, errors.Wrap(err, "Failed to convert to json").Error(), http.StatusInternalServerError)
return
}
httputil.WriteJson(w, response)
}
// GetPeers is a wrapper around the /eth/v1alpha1 endpoint of the same name.
func (s *Server) GetPeers(w http.ResponseWriter, r *http.Request) {
ctx, span := trace.StartSpan(r.Context(), "validator.web.beacon.GetPeers")
defer span.End()
peers, err := s.beaconNodeClient.ListPeers(ctx, &emptypb.Empty{})
if err != nil {
httputil.HandleError(w, errors.Wrap(err, "ListPeers call failed").Error(), http.StatusInternalServerError)
return
}
httputil.WriteJson(w, peers)
}

View File

@@ -0,0 +1,105 @@
package rpc
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/eth/shared"
ethpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1"
"github.com/prysmaticlabs/prysm/v4/testing/assert"
"github.com/prysmaticlabs/prysm/v4/testing/require"
validatormock "github.com/prysmaticlabs/prysm/v4/testing/validator-mock"
"google.golang.org/protobuf/types/known/timestamppb"
)
func TestGetBeaconStatus_NotConnected(t *testing.T) {
ctrl := gomock.NewController(t)
nodeClient := validatormock.NewMockNodeClient(ctrl)
nodeClient.EXPECT().GetSyncStatus(
gomock.Any(), // ctx
gomock.Any(),
).Return(nil /*response*/, errors.New("uh oh"))
srv := &Server{
beaconNodeClient: nodeClient,
}
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/v2/validator/beacon/status"), nil)
wr := httptest.NewRecorder()
wr.Body = &bytes.Buffer{}
srv.GetBeaconStatus(wr, req)
require.Equal(t, http.StatusOK, wr.Code)
resp := &BeaconStatusResponse{}
require.NoError(t, json.Unmarshal(wr.Body.Bytes(), resp))
want := &BeaconStatusResponse{
BeaconNodeEndpoint: "",
Connected: false,
Syncing: false,
}
assert.DeepEqual(t, want, resp)
}
func TestGetBeaconStatus_OK(t *testing.T) {
ctrl := gomock.NewController(t)
nodeClient := validatormock.NewMockNodeClient(ctrl)
beaconChainClient := validatormock.NewMockBeaconChainClient(ctrl)
nodeClient.EXPECT().GetSyncStatus(
gomock.Any(), // ctx
gomock.Any(),
).Return(&ethpb.SyncStatus{Syncing: true}, nil)
timeStamp := timestamppb.New(time.Unix(0, 0))
nodeClient.EXPECT().GetGenesis(
gomock.Any(), // ctx
gomock.Any(),
).Return(&ethpb.Genesis{
GenesisTime: timeStamp,
DepositContractAddress: []byte("hello"),
}, nil)
beaconChainClient.EXPECT().GetChainHead(
gomock.Any(), // ctx
gomock.Any(),
).Return(&ethpb.ChainHead{
HeadEpoch: 1,
}, nil)
srv := &Server{
beaconNodeClient: nodeClient,
beaconChainClient: beaconChainClient,
}
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/v2/validator/beacon/status"), nil)
wr := httptest.NewRecorder()
wr.Body = &bytes.Buffer{}
srv.GetBeaconStatus(wr, req)
require.Equal(t, http.StatusOK, wr.Code)
resp := &BeaconStatusResponse{}
require.NoError(t, json.Unmarshal(wr.Body.Bytes(), resp))
want := &BeaconStatusResponse{
BeaconNodeEndpoint: "",
Connected: true,
Syncing: true,
GenesisTime: fmt.Sprintf("%d", time.Unix(0, 0).Unix()),
DepositContractAddress: "0x68656c6c6f",
ChainHead: &shared.ChainHead{
HeadSlot: "0",
HeadEpoch: "1",
HeadBlockRoot: "0x",
FinalizedSlot: "0",
FinalizedEpoch: "0",
FinalizedBlockRoot: "0x",
JustifiedSlot: "0",
JustifiedEpoch: "0",
JustifiedBlockRoot: "0x",
PreviousJustifiedSlot: "0",
PreviousJustifiedEpoch: "0",
PreviousJustifiedBlockRoot: "0x",
OptimisticStatus: false,
},
}
assert.DeepEqual(t, want, resp)
}

View File

@@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/ethereum/go-ethereum/common"
@@ -90,7 +91,12 @@ func (s *Server) ImportKeystores(w http.ResponseWriter, r *http.Request) {
}
var req ImportKeystoresRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
err = json.NewDecoder(r.Body).Decode(&req)
switch {
case err == io.EOF:
httputil.HandleError(w, "No data submitted", http.StatusBadRequest)
return
case err != nil:
httputil.HandleError(w, "Could not decode request body: "+err.Error(), http.StatusBadRequest)
return
}
@@ -182,10 +188,16 @@ func (s *Server) DeleteKeystores(w http.ResponseWriter, r *http.Request) {
return
}
var req DeleteKeystoresRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
err = json.NewDecoder(r.Body).Decode(&req)
switch {
case err == io.EOF:
httputil.HandleError(w, "No data submitted", http.StatusBadRequest)
return
case err != nil:
httputil.HandleError(w, "Could not decode request body: "+err.Error(), http.StatusBadRequest)
return
}
if len(req.Pubkeys) == 0 {
httputil.WriteJson(w, &DeleteKeystoresResponse{Data: make([]*keymanager.KeyStatus, 0)})
return
@@ -447,10 +459,16 @@ func (s *Server) ImportRemoteKeys(w http.ResponseWriter, r *http.Request) {
}
var req ImportRemoteKeysRequest
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
err = json.NewDecoder(r.Body).Decode(&req)
switch {
case err == io.EOF:
httputil.HandleError(w, "No data submitted", http.StatusBadRequest)
return
case err != nil:
httputil.HandleError(w, "Could not decode request body: "+err.Error(), http.StatusBadRequest)
return
}
adder, ok := km.(keymanager.PublicKeyAdder)
if !ok {
statuses := make([]*keymanager.KeyStatus, len(req.RemoteKeys))
@@ -501,8 +519,14 @@ func (s *Server) DeleteRemoteKeys(w http.ResponseWriter, r *http.Request) {
httputil.HandleError(w, "Prysm Wallet is not of type Web3Signer. Please execute validator client with web3signer flags.", http.StatusInternalServerError)
return
}
var req DeleteRemoteKeysRequest
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
err = json.NewDecoder(r.Body).Decode(&req)
switch {
case err == io.EOF:
httputil.HandleError(w, "No data submitted", http.StatusBadRequest)
return
case err != nil:
httputil.HandleError(w, "Could not decode request body: "+err.Error(), http.StatusBadRequest)
return
}
@@ -592,11 +616,18 @@ func (s *Server) SetFeeRecipientByPubkey(w http.ResponseWriter, r *http.Request)
if !valid {
return
}
var req SetFeeRecipientByPubkeyRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
err := json.NewDecoder(r.Body).Decode(&req)
switch {
case err == io.EOF:
httputil.HandleError(w, "No data submitted", http.StatusBadRequest)
return
case err != nil:
httputil.HandleError(w, "Could not decode request body: "+err.Error(), http.StatusBadRequest)
return
}
ethAddress, valid := shared.ValidateHex(w, "Ethereum Address", req.Ethaddress, fieldparams.FeeRecipientLength)
if !valid {
return
@@ -757,7 +788,12 @@ func (s *Server) SetGasLimit(w http.ResponseWriter, r *http.Request) {
}
var req SetGasLimitRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
err := json.NewDecoder(r.Body).Decode(&req)
switch {
case err == io.EOF:
httputil.HandleError(w, "No data submitted", http.StatusBadRequest)
return
case err != nil:
httputil.HandleError(w, "Could not decode request body: "+err.Error(), http.StatusBadRequest)
return
}

View File

@@ -3,9 +3,11 @@ package rpc
import (
"context"
"fmt"
"net/http"
"strings"
"github.com/golang-jwt/jwt/v4"
"github.com/prysmaticlabs/prysm/v4/api"
"github.com/sirupsen/logrus"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
@@ -33,6 +35,29 @@ func (s *Server) JWTInterceptor() grpc.UnaryServerInterceptor {
}
}
// JwtHttpInterceptor is an HTTP handler to authorize a route.
func (s *Server) JwtHttpInterceptor(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// if it's not initialize or has a web prefix
if !strings.Contains(r.URL.Path, api.WebUrlPrefix+"initialize") && // ignore some routes
!strings.Contains(r.URL.Path, api.WebUrlPrefix+"health/logs") &&
strings.Contains(r.URL.Path, api.WebUrlPrefix) {
reqToken := r.Header.Get("Authorization")
if reqToken == "" {
http.Error(w, "unauthorized: no Authorization header passed. Please use an Authorization header with the jwt created in the prysm wallet", http.StatusUnauthorized)
return
}
token := strings.Split(reqToken, "Bearer ")[1]
_, err := jwt.Parse(token, s.validateJWT)
if err != nil {
http.Error(w, fmt.Errorf("unauthorized:could not parse JWT token: %v", err).Error(), http.StatusUnauthorized)
return
}
}
next.ServeHTTP(w, r)
})
}
// Authorize the token received is valid.
func (s *Server) authorize(ctx context.Context) error {
md, ok := metadata.FromIncomingContext(ctx)

View File

@@ -14,11 +14,11 @@ import (
grpcopentracing "github.com/grpc-ecosystem/go-grpc-middleware/tracing/opentracing"
grpcprometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/v4/api"
"github.com/prysmaticlabs/prysm/v4/async/event"
"github.com/prysmaticlabs/prysm/v4/io/logs"
"github.com/prysmaticlabs/prysm/v4/monitoring/tracing"
ethpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1"
validatorpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1/validator-client"
"github.com/prysmaticlabs/prysm/v4/validator/accounts/wallet"
"github.com/prysmaticlabs/prysm/v4/validator/client"
iface "github.com/prysmaticlabs/prysm/v4/validator/client/iface"
@@ -183,9 +183,6 @@ func (s *Server) Start() {
// Register services available for the gRPC server.
reflection.Register(s.grpcServer)
validatorpb.RegisterAuthServer(s.grpcServer, s)
validatorpb.RegisterBeaconServer(s.grpcServer, s)
validatorpb.RegisterAccountsServer(s.grpcServer, s)
// routes needs to be set before the server calls the server function
go func() {
@@ -204,7 +201,7 @@ func (s *Server) Start() {
return
}
validatorWebAddr := fmt.Sprintf("%s:%d", s.validatorGatewayHost, s.validatorGatewayPort)
authTokenPath := filepath.Join(s.walletDir, authTokenFileName)
authTokenPath := filepath.Join(s.walletDir, AuthTokenFileName)
logValidatorWebAuth(validatorWebAddr, token, authTokenPath)
go s.refreshAuthTokenFromFileChanges(s.ctx, authTokenPath)
}
@@ -216,6 +213,8 @@ func (s *Server) InitializeRoutes() error {
if s.router == nil {
return errors.New("no router found on server")
}
// Adding Auth Interceptor for the routes below
s.router.Use(s.JwtHttpInterceptor)
// Register all services, HandleFunc calls, etc.
// ...
s.router.HandleFunc("/eth/v1/keystores", s.ListKeystores).Methods(http.MethodGet)
@@ -231,18 +230,30 @@ func (s *Server) InitializeRoutes() error {
s.router.HandleFunc("/eth/v1/validator/{pubkey}/feerecipient", s.SetFeeRecipientByPubkey).Methods(http.MethodPost)
s.router.HandleFunc("/eth/v1/validator/{pubkey}/feerecipient", s.DeleteFeeRecipientByPubkey).Methods(http.MethodDelete)
s.router.HandleFunc("/eth/v1/validator/{pubkey}/voluntary_exit", s.SetVoluntaryExit).Methods(http.MethodPost)
// auth endpoint
s.router.HandleFunc(api.WebUrlPrefix+"initialize", s.Initialize).Methods(http.MethodGet)
// accounts endpoints
s.router.HandleFunc(api.WebUrlPrefix+"accounts", s.ListAccounts).Methods(http.MethodGet)
s.router.HandleFunc(api.WebUrlPrefix+"accounts/backup", s.BackupAccounts).Methods(http.MethodPost)
s.router.HandleFunc(api.WebUrlPrefix+"accounts/voluntary-exit", s.VoluntaryExit).Methods(http.MethodPost)
// web health endpoints
s.router.HandleFunc("/v2/validator/health/version", s.GetVersion).Methods(http.MethodGet)
s.router.HandleFunc("/v2/validator/health/logs/validator/stream", s.StreamValidatorLogs).Methods(http.MethodGet)
s.router.HandleFunc("/v2/validator/health/logs/beacon/stream", s.StreamBeaconLogs).Methods(http.MethodGet)
s.router.HandleFunc(api.WebUrlPrefix+"health/version", s.GetVersion).Methods(http.MethodGet)
s.router.HandleFunc(api.WebUrlPrefix+"health/logs/validator/stream", s.StreamValidatorLogs).Methods(http.MethodGet)
s.router.HandleFunc(api.WebUrlPrefix+"health/logs/beacon/stream", s.StreamBeaconLogs).Methods(http.MethodGet)
// Beacon calls
s.router.HandleFunc(api.WebUrlPrefix+"beacon/status", s.GetBeaconStatus).Methods(http.MethodGet)
s.router.HandleFunc(api.WebUrlPrefix+"beacon/summary", s.GetValidatorPerformance).Methods(http.MethodGet)
s.router.HandleFunc(api.WebUrlPrefix+"beacon/validators", s.GetValidators).Methods(http.MethodGet)
s.router.HandleFunc(api.WebUrlPrefix+"beacon/balances", s.GetValidatorBalances).Methods(http.MethodGet)
s.router.HandleFunc(api.WebUrlPrefix+"beacon/peers", s.GetPeers).Methods(http.MethodGet)
// web wallet endpoints
s.router.HandleFunc("/v2/validator/wallet", s.WalletConfig).Methods(http.MethodGet)
s.router.HandleFunc("/v2/validator/wallet/create", s.CreateWallet).Methods(http.MethodPost)
s.router.HandleFunc("/v2/validator/wallet/keystores/validate", s.ValidateKeystores).Methods(http.MethodPost)
s.router.HandleFunc("/v2/validator/wallet/recover", s.RecoverWallet).Methods(http.MethodPost)
s.router.HandleFunc(api.WebUrlPrefix+"wallet", s.WalletConfig).Methods(http.MethodGet)
s.router.HandleFunc(api.WebUrlPrefix+"wallet/create", s.CreateWallet).Methods(http.MethodPost)
s.router.HandleFunc(api.WebUrlPrefix+"wallet/keystores/validate", s.ValidateKeystores).Methods(http.MethodPost)
s.router.HandleFunc(api.WebUrlPrefix+"wallet/recover", s.RecoverWallet).Methods(http.MethodPost)
// slashing protection endpoints
s.router.HandleFunc("/v2/validator/slashing-protection/export", s.ExportSlashingProtection).Methods(http.MethodGet)
s.router.HandleFunc("/v2/validator/slashing-protection/import", s.ImportSlashingProtection).Methods(http.MethodPost)
s.router.HandleFunc(api.WebUrlPrefix+"slashing-protection/export", s.ExportSlashingProtection).Methods(http.MethodGet)
s.router.HandleFunc(api.WebUrlPrefix+"slashing-protection/import", s.ImportSlashingProtection).Methods(http.MethodPost)
log.Info("Initialized REST API routes")
return nil
}

View File

@@ -5,12 +5,9 @@ import (
"testing"
"github.com/gorilla/mux"
pb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1/validator-client"
"github.com/prysmaticlabs/prysm/v4/testing/require"
)
var _ pb.AuthServer = (*Server)(nil)
func TestServer_InitializeRoutes(t *testing.T) {
s := Server{
router: mux.NewRouter(),
@@ -33,6 +30,15 @@ func TestServer_InitializeRoutes(t *testing.T) {
"/v2/validator/wallet/recover": {http.MethodPost},
"/v2/validator/slashing-protection/export": {http.MethodGet},
"/v2/validator/slashing-protection/import": {http.MethodPost},
"/v2/validator/accounts": {http.MethodGet},
"/v2/validator/accounts/backup": {http.MethodPost},
"/v2/validator/accounts/voluntary-exit": {http.MethodPost},
"/v2/validator/beacon/balances": {http.MethodGet},
"/v2/validator/beacon/peers": {http.MethodGet},
"/v2/validator/beacon/status": {http.MethodGet},
"/v2/validator/beacon/summary": {http.MethodGet},
"/v2/validator/beacon/validators": {http.MethodGet},
"/v2/validator/initialize": {http.MethodGet},
}
gotRouteList := make(map[string][]string)
err = s.router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {

View File

@@ -90,6 +90,15 @@ type SetFeeRecipientByPubkeyRequest struct {
Ethaddress string `json:"ethaddress"`
}
type BeaconStatusResponse struct {
BeaconNodeEndpoint string `json:"beacon_node_endpoint"`
Connected bool `json:"connected"`
Syncing bool `json:"syncing"`
GenesisTime string `json:"genesis_time"`
DepositContractAddress string `json:"deposit_contract_address"`
ChainHead *shared.ChainHead `json:"chain_head"`
}
// KeymanagerKind is a type of key manager for the wallet
type KeymanagerKind string
@@ -140,3 +149,38 @@ type ImportSlashingProtectionRequest struct {
type ExportSlashingProtectionResponse struct {
File string `json:"file"`
}
type BackupAccountsRequest struct {
PublicKeys []string `json:"public_keys"`
BackupPassword string `json:"backup_password"`
}
type VoluntaryExitRequest struct {
PublicKeys []string `json:"public_keys"`
}
type BackupAccountsResponse struct {
ZipFile string `json:"zip_file"`
}
type ListAccountsResponse struct {
Accounts []*Account `json:"accounts"`
NextPageToken string `json:"next_page_token"`
TotalSize int32 `json:"total_size"`
}
type Account struct {
ValidatingPublicKey string `json:"validating_public_key"`
AccountName string `json:"account_name"`
DepositTxData string `json:"deposit_tx_data"`
DerivationPath string `json:"derivation_path"`
}
type VoluntaryExitResponse struct {
ExitedKeys [][]byte `protobuf:"bytes,1,rep,name=exited_keys,json=exitedKeys,proto3" json:"exited_keys,omitempty"`
}
type InitializeAuthResponse struct {
HasSignedUp bool `json:"has_signed_up"`
HasWallet bool `json:"has_wallet"`
}

11
validator/web/README.md Normal file
View File

@@ -0,0 +1,11 @@
## Note on future releases
** There is no longer an automated PR workflow for releasing the web ui due to its frozen state **
This is due to this PR removal of build content:https://github.com/prysmaticlabs/prysm/pull/12719
in order to update the `site_data.go` follow the following steps to update the specific release of https://github.com/prysmaticlabs/prysm-web-ui/releases
1. download and install https://github.com/kevinburke/go-bindata. (working as of version `4.0.2`) This tool will be used to generate the site_data.go file.
2. download the specific release from https://github.com/prysmaticlabs/prysm-web-ui/releases
3. run `go-bindata -pkg web -nometadata -modtime 0 -o site_data.go prysm-web-ui/` . `prysm-web-ui/` represents the extracted folder from the release.
4. copy and replace the site_data.go in this package.
5. Open a PR

View File

@@ -7,7 +7,7 @@ import (
"path"
)
const prefix = "external/prysm_web_ui/prysm-web-ui"
const prefix = "prysm-web-ui"
// Handler serves web requests from the bundled site data.
// DEPRECATED: Prysm Web UI and associated endpoints will be fully removed in a future hard fork.
@@ -24,20 +24,28 @@ var Handler = func(res http.ResponseWriter, req *http.Request) {
}
p = path.Join(prefix, p)
if d, ok := site[p]; ok {
if d, ok := _bindata[p]; ok {
m := mime.TypeByExtension(path.Ext(p))
res.Header().Add("Content-Type", m)
res.WriteHeader(200)
if _, err := res.Write(d); err != nil {
asset, err := d()
if err != nil {
log.WithError(err).Error("Failed to unwrap asset data for http response")
}
if _, err := res.Write(asset.bytes); err != nil {
log.WithError(err).Error("Failed to write http response")
}
} else if d, ok := site[path.Join(prefix, "index.html")]; ok {
} else if d, ok := _bindata[path.Join(prefix, "index.html")]; ok {
// Angular routing expects that routes are rewritten to serve index.html. For example, if
// requesting /login, this should serve the single page app index.html.
m := mime.TypeByExtension(".html")
res.Header().Add("Content-Type", m)
res.WriteHeader(200)
if _, err := res.Write(d); err != nil {
asset, err := d()
if err != nil {
log.WithError(err).Error("Failed to unwrap asset data for http response")
}
if _, err := res.Write(asset.bytes); err != nil {
log.WithError(err).Error("Failed to write http response")
}
} else { // If index.html is not present, serve 404. This should never happen.

File diff suppressed because one or more lines are too long