HTTP validator API: wallet endpoints (#13171)

* converting wallet calls to pure http

* fixing proto and gaz

* adding routes and fixing test

* fixing error handling

* fixing protos after conflict with develop

* adding deprecation notice

* fixing route test

* review feedback

* addressing more comments

* updating comment to be more clear

* fixing web_api proto
This commit is contained in:
james-prysm
2023-11-15 13:40:14 -06:00
committed by GitHub
parent d4726f2866
commit 4c381938e1
16 changed files with 845 additions and 2126 deletions

View File

@@ -794,7 +794,6 @@ func (c *ValidatorClient) registerRPCGatewayService(router *mux.Router) error {
registrations := []gateway.PbHandlerRegistration{
validatorpb.RegisterAuthHandler,
validatorpb.RegisterWalletHandler,
pb.RegisterHealthHandler,
validatorpb.RegisterAccountsHandler,
validatorpb.RegisterBeaconHandler,

View File

@@ -6,6 +6,7 @@ go_library(
"accounts.go",
"auth_token.go",
"beacon.go",
"handler_wallet.go",
"handlers_health.go",
"handlers_keymanager.go",
"handlers_slashing.go",
@@ -13,7 +14,6 @@ go_library(
"log.go",
"server.go",
"structs.go",
"wallet.go",
],
importpath = "github.com/prysmaticlabs/prysm/v4/validator/rpc",
visibility = [
@@ -91,12 +91,12 @@ go_test(
"accounts_test.go",
"auth_token_test.go",
"beacon_test.go",
"handler_wallet_test.go",
"handlers_health_test.go",
"handlers_keymanager_test.go",
"handlers_slashing_test.go",
"intercepter_test.go",
"server_test.go",
"wallet_test.go",
],
embed = [":go_default_library"],
deps = [

View File

@@ -120,7 +120,7 @@ func (s *Server) BackupAccounts(
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")
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")

View File

@@ -4,66 +4,76 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"path/filepath"
"strings"
"github.com/golang/protobuf/ptypes/empty"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/v4/config/features"
"github.com/prysmaticlabs/prysm/v4/io/file"
"github.com/prysmaticlabs/prysm/v4/io/prompt"
pb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1/validator-client"
httputil "github.com/prysmaticlabs/prysm/v4/network/http"
"github.com/prysmaticlabs/prysm/v4/validator/accounts"
"github.com/prysmaticlabs/prysm/v4/validator/accounts/wallet"
"github.com/prysmaticlabs/prysm/v4/validator/keymanager"
"github.com/tyler-smith/go-bip39"
"github.com/tyler-smith/go-bip39/wordlists"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
const (
checkExistsErrMsg = "Could not check if wallet exists"
checkValidityErrMsg = "Could not check if wallet is valid"
invalidWalletMsg = "Directory does not contain a valid wallet"
"go.opencensus.io/trace"
)
// CreateWallet via an API request, allowing a user to save a new
// imported wallet via RPC.
// DEPRECATE: Prysm Web UI and associated endpoints will be fully removed in a future hard fork.
func (s *Server) CreateWallet(ctx context.Context, req *pb.CreateWalletRequest) (*pb.CreateWalletResponse, error) {
func (s *Server) CreateWallet(w http.ResponseWriter, r *http.Request) {
ctx, span := trace.StartSpan(r.Context(), "validator.web.CreateWallet")
defer span.End()
var req CreateWalletRequest
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
}
walletDir := s.walletDir
exists, err := wallet.Exists(walletDir)
if err != nil {
return nil, status.Errorf(codes.Internal, "Could not check for existing wallet: %v", err)
httputil.HandleError(w, "Could not check for existing wallet: "+err.Error(), http.StatusInternalServerError)
return
}
if exists {
if err := s.initializeWallet(ctx, &wallet.Config{
WalletDir: walletDir,
WalletPassword: req.WalletPassword,
}); err != nil {
return nil, err
httputil.HandleError(w, "Could not initialize wallet: "+err.Error(), http.StatusInternalServerError)
return
}
keymanagerKind := pb.KeymanagerKind_IMPORTED
keymanagerKind := importedKeymanagerKind
switch s.wallet.KeymanagerKind() {
case keymanager.Derived:
keymanagerKind = pb.KeymanagerKind_DERIVED
keymanagerKind = derivedKeymanagerKind
case keymanager.Web3Signer:
keymanagerKind = pb.KeymanagerKind_WEB3SIGNER
keymanagerKind = web3signerKeymanagerKind
}
return &pb.CreateWalletResponse{
Wallet: &pb.WalletResponse{
response := &CreateWalletResponse{
Wallet: &WalletResponse{
WalletPath: walletDir,
KeymanagerKind: keymanagerKind,
},
}, nil
}
httputil.WriteJson(w, response)
return
}
if err := prompt.ValidatePasswordInput(req.WalletPassword); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "Password too weak: %v", err)
httputil.HandleError(w, "Password too weak: "+err.Error(), http.StatusBadRequest)
return
}
if req.Keymanager == pb.KeymanagerKind_IMPORTED {
if req.Keymanager == importedKeymanagerKind {
opts := []accounts.Option{
accounts.WithWalletDir(walletDir),
accounts.WithKeymanagerType(keymanager.Local),
@@ -72,84 +82,112 @@ func (s *Server) CreateWallet(ctx context.Context, req *pb.CreateWalletRequest)
}
acc, err := accounts.NewCLIManager(opts...)
if err != nil {
return nil, err
httputil.HandleError(w, "Could not create CLI Manager: "+err.Error(), http.StatusInternalServerError)
return
}
_, err = acc.WalletCreate(ctx)
if err != nil {
return nil, err
httputil.HandleError(w, "Could not create wallet: "+err.Error(), http.StatusInternalServerError)
return
}
if err := s.initializeWallet(ctx, &wallet.Config{
WalletDir: walletDir,
KeymanagerKind: keymanager.Local,
WalletPassword: req.WalletPassword,
}); err != nil {
return nil, err
httputil.HandleError(w, err.Error(), http.StatusInternalServerError)
return
}
if err := writeWalletPasswordToDisk(walletDir, req.WalletPassword); err != nil {
return nil, status.Error(codes.Internal, "Could not write wallet password to disk")
httputil.HandleError(w, "Could not write wallet password to disk: "+err.Error(), http.StatusInternalServerError)
return
}
return &pb.CreateWalletResponse{
Wallet: &pb.WalletResponse{
response := &CreateWalletResponse{
Wallet: &WalletResponse{
WalletPath: walletDir,
KeymanagerKind: pb.KeymanagerKind_IMPORTED,
KeymanagerKind: importedKeymanagerKind,
},
}, nil
}
httputil.WriteJson(w, response)
return
}
return nil, status.Errorf(codes.InvalidArgument, "Keymanager type %T create wallet not supported through web", req.Keymanager)
httputil.HandleError(w, fmt.Sprintf("Keymanager type %s create wallet not supported through web", req.Keymanager), http.StatusBadRequest)
}
// WalletConfig returns the wallet's configuration. If no wallet exists, we return an empty response.
// DEPRECATE: Prysm Web UI and associated endpoints will be fully removed in a future hard fork.
func (s *Server) WalletConfig(_ context.Context, _ *empty.Empty) (*pb.WalletResponse, error) {
func (s *Server) WalletConfig(w http.ResponseWriter, r *http.Request) {
_, span := trace.StartSpan(r.Context(), "validator.web.WalletConfig")
defer span.End()
exists, err := wallet.Exists(s.walletDir)
if err != nil {
return nil, status.Errorf(codes.Internal, checkExistsErrMsg)
httputil.HandleError(w, wallet.CheckExistsErrMsg+": "+err.Error(), http.StatusInternalServerError)
return
}
if !exists {
// If no wallet is found, we simply return an empty response.
return &pb.WalletResponse{}, nil
httputil.WriteJson(w, &WalletResponse{})
return
}
valid, err := wallet.IsValid(s.walletDir)
if errors.Is(err, wallet.ErrNoWalletFound) {
return &pb.WalletResponse{}, nil
httputil.WriteJson(w, &WalletResponse{})
return
}
if err != nil {
return nil, status.Errorf(codes.Internal, checkValidityErrMsg)
httputil.HandleError(w, wallet.CheckValidityErrMsg+": "+err.Error(), http.StatusInternalServerError)
return
}
if !valid {
return nil, status.Errorf(codes.FailedPrecondition, invalidWalletMsg)
httputil.HandleError(w, wallet.InvalidWalletErrMsg, http.StatusInternalServerError)
return
}
if s.wallet == nil || s.validatorService == nil {
// If no wallet is found, we simply return an empty response.
return &pb.WalletResponse{}, nil
httputil.WriteJson(w, &WalletResponse{})
return
}
var keymanagerKind pb.KeymanagerKind
var keymanagerKind KeymanagerKind
switch s.wallet.KeymanagerKind() {
case keymanager.Derived:
keymanagerKind = pb.KeymanagerKind_DERIVED
keymanagerKind = derivedKeymanagerKind
case keymanager.Local:
keymanagerKind = pb.KeymanagerKind_IMPORTED
keymanagerKind = importedKeymanagerKind
case keymanager.Web3Signer:
keymanagerKind = pb.KeymanagerKind_WEB3SIGNER
keymanagerKind = web3signerKeymanagerKind
}
return &pb.WalletResponse{
httputil.WriteJson(w, &WalletResponse{
WalletPath: s.walletDir,
KeymanagerKind: keymanagerKind,
}, nil
})
}
// RecoverWallet via an API request, allowing a user to recover a derived.
// RecoverWallet via an API request, allowing a user to recover a derived wallet.
// Generate the seed from the mnemonic + language + 25th passphrase(optional).
// Create N validator keystores from the seed specified by req.NumAccounts.
// Set the wallet password to req.WalletPassword, then create the wallet from
// the provided Mnemonic and return CreateWalletResponse.
// DEPRECATE: Prysm Web UI and associated endpoints will be fully removed in a future hard fork.
func (s *Server) RecoverWallet(ctx context.Context, req *pb.RecoverWalletRequest) (*pb.CreateWalletResponse, error) {
// DEPRECATED: this endpoint will be removed to improve the safety and security of interacting with wallets
func (s *Server) RecoverWallet(w http.ResponseWriter, r *http.Request) {
ctx, span := trace.StartSpan(r.Context(), "validator.web.RecoverWallet")
defer span.End()
var req RecoverWalletRequest
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
}
numAccounts := int(req.NumAccounts)
if numAccounts == 0 {
return nil, status.Error(codes.InvalidArgument, "Must create at least 1 validator account")
httputil.HandleError(w, "Must create at least 1 validator account", http.StatusBadRequest)
return
}
// Check validate mnemonic with chosen language
@@ -166,17 +204,20 @@ func (s *Server) RecoverWallet(ctx context.Context, req *pb.RecoverWalletRequest
"spanish": wordlists.Spanish,
}
if _, ok := allowedLanguages[language]; !ok {
return nil, status.Error(codes.InvalidArgument, "input not in the list of supported languages")
httputil.HandleError(w, "input not in the list of supported languages", http.StatusBadRequest)
return
}
bip39.SetWordList(allowedLanguages[language])
mnemonic := req.Mnemonic
if err := accounts.ValidateMnemonic(mnemonic); err != nil {
return nil, status.Error(codes.InvalidArgument, "invalid mnemonic in request")
httputil.HandleError(w, "invalid mnemonic in request: "+err.Error(), http.StatusBadRequest)
return
}
// Check it is not null and not an empty string.
// Check it is not whitespace-only (empty is valid)
if req.Mnemonic25ThWord != "" && strings.TrimSpace(req.Mnemonic25ThWord) == "" {
return nil, status.Error(codes.InvalidArgument, "mnemonic 25th word cannot be empty")
httputil.HandleError(w, "mnemonic 25th word cannot be empty", http.StatusBadRequest)
return
}
// Web UI is structured to only write to the default wallet directory
@@ -186,7 +227,8 @@ func (s *Server) RecoverWallet(ctx context.Context, req *pb.RecoverWalletRequest
// Web UI should check the new and confirmed password are equal.
walletPassword := req.WalletPassword
if err := prompt.ValidatePasswordInput(walletPassword); err != nil {
return nil, status.Error(codes.InvalidArgument, "password did not pass validation")
httputil.HandleError(w, "password did not pass validation: "+err.Error(), http.StatusBadRequest)
return
}
opts := []accounts.Option{
@@ -198,50 +240,68 @@ func (s *Server) RecoverWallet(ctx context.Context, req *pb.RecoverWalletRequest
}
acc, err := accounts.NewCLIManager(opts...)
if err != nil {
return nil, err
httputil.HandleError(w, "Could not create CLI Manager: "+err.Error(), http.StatusInternalServerError)
return
}
if _, err := acc.WalletRecover(ctx); err != nil {
return nil, err
httputil.HandleError(w, "Failed to recover wallet: "+err.Error(), http.StatusInternalServerError)
return
}
if err := s.initializeWallet(ctx, &wallet.Config{
WalletDir: walletDir,
KeymanagerKind: keymanager.Derived,
WalletPassword: walletPassword,
}); err != nil {
return nil, err
httputil.HandleError(w, "Failed to initialize wallet: "+err.Error(), http.StatusInternalServerError)
return
}
if err := writeWalletPasswordToDisk(walletDir, walletPassword); err != nil {
return nil, status.Error(codes.Internal, "Could not write wallet password to disk")
httputil.HandleError(w, "Could not write wallet password to disk: "+err.Error(), http.StatusInternalServerError)
return
}
return &pb.CreateWalletResponse{
Wallet: &pb.WalletResponse{
httputil.WriteJson(w, &CreateWalletResponse{
Wallet: &WalletResponse{
WalletPath: walletDir,
KeymanagerKind: pb.KeymanagerKind_DERIVED,
KeymanagerKind: derivedKeymanagerKind,
},
}, nil
})
}
// ValidateKeystores checks whether a set of EIP-2335 keystores in the request
// can indeed be decrypted using a password in the request. If there is no issue,
// we return an empty response with no error. If the password is incorrect for a single keystore,
// we return an appropriate error.
// DEPRECATE: Prysm Web UI and associated endpoints will be fully removed in a future hard fork.
func (*Server) ValidateKeystores(
_ context.Context, req *pb.ValidateKeystoresRequest,
) (*emptypb.Empty, error) {
func (*Server) ValidateKeystores(w http.ResponseWriter, r *http.Request) {
_, span := trace.StartSpan(r.Context(), "validator.web.ValidateKeystores")
defer span.End()
var req ValidateKeystoresRequest
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.KeystoresPassword == "" {
return nil, status.Error(codes.InvalidArgument, "Password required for keystores")
httputil.HandleError(w, "Password required for keystores", http.StatusBadRequest)
return
}
// Needs to unmarshal the keystores from the requests.
if req.Keystores == nil || len(req.Keystores) < 1 {
return nil, status.Error(codes.InvalidArgument, "No keystores included in request")
httputil.HandleError(w, "No keystores included in request", http.StatusBadRequest)
return
}
decryptor := keystorev4.New()
for i := 0; i < len(req.Keystores); i++ {
encoded := req.Keystores[i]
keystore := &keymanager.Keystore{}
if err := json.Unmarshal([]byte(encoded), &keystore); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "Not a valid EIP-2335 keystore JSON file: %v", err)
httputil.HandleError(w, "Not a valid EIP-2335 keystore JSON file: "+err.Error(), http.StatusBadRequest)
return
}
if keystore.Description == "" && keystore.Name != "" {
keystore.Description = keystore.Name
@@ -249,23 +309,19 @@ func (*Server) ValidateKeystores(
if _, err := decryptor.Decrypt(keystore.Crypto, req.KeystoresPassword); err != nil {
doesNotDecrypt := strings.Contains(err.Error(), keymanager.IncorrectPasswordErrMsg)
if doesNotDecrypt {
return nil, status.Errorf(
codes.InvalidArgument,
"Password for keystore with public key %s is incorrect. "+
"Prysm web only supports importing batches of keystores with the same password for all of them",
keystore.Pubkey,
)
httputil.HandleError(w, fmt.Sprintf("Password for keystore with public key %s is incorrect. "+
"Prysm web only supports importing batches of keystores with the same password for all of them",
keystore.Pubkey), http.StatusBadRequest)
return
} else {
return nil, status.Errorf(codes.Internal, "Unexpected error decrypting keystore: %v", err)
httputil.HandleError(w, "Unexpected error decrypting keystore: "+err.Error(), http.StatusInternalServerError)
return
}
}
}
return &emptypb.Empty{}, nil
}
// Initialize a wallet and send it over a global feed.
// DEPRECATE: Prysm Web UI and associated endpoints will be fully removed in a future hard fork.
func (s *Server) initializeWallet(ctx context.Context, cfg *wallet.Config) error {
// We first ensure the user has a wallet.
exists, err := wallet.Exists(cfg.WalletDir)

View File

@@ -10,14 +10,12 @@ import (
"path/filepath"
"testing"
"github.com/golang/protobuf/ptypes/empty"
"github.com/google/uuid"
"github.com/prysmaticlabs/prysm/v4/async/event"
"github.com/prysmaticlabs/prysm/v4/config/features"
"github.com/prysmaticlabs/prysm/v4/crypto/bls"
"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/testing/assert"
"github.com/prysmaticlabs/prysm/v4/testing/require"
"github.com/prysmaticlabs/prysm/v4/validator/accounts"
@@ -60,13 +58,20 @@ func TestServer_CreateWallet_Local(t *testing.T) {
walletDir: defaultWalletPath,
validatorService: vs,
}
_, err = s.CreateWallet(ctx, &pb.CreateWalletRequest{
Keymanager: pb.KeymanagerKind_IMPORTED,
request := &CreateWalletRequest{
Keymanager: importedKeymanagerKind,
WalletPassword: strongPass,
})
}
var buf bytes.Buffer
err = json.NewEncoder(&buf).Encode(request)
require.NoError(t, err)
req := httptest.NewRequest(http.MethodPost, "/v2/validator/wallet/create", &buf)
wr := httptest.NewRecorder()
wr.Body = &bytes.Buffer{}
s.CreateWallet(wr, req)
require.Equal(t, http.StatusOK, wr.Code)
encryptor := keystorev4.New()
keystores := make([]string, 3)
passwords := make([]string, 3)
@@ -98,12 +103,11 @@ func TestServer_CreateWallet_Local(t *testing.T) {
Passwords: passwords,
}
var buf bytes.Buffer
err = json.NewEncoder(&buf).Encode(importReq)
require.NoError(t, err)
req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/eth/v1/keystores"), &buf)
wr := httptest.NewRecorder()
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/eth/v1/keystores"), &buf)
wr = httptest.NewRecorder()
wr.Body = &bytes.Buffer{}
s.ImportKeystores(wr, req)
require.Equal(t, http.StatusOK, wr.Code)
@@ -120,76 +124,128 @@ func TestServer_CreateWallet_Local(t *testing.T) {
func TestServer_CreateWallet_Local_PasswordTooWeak(t *testing.T) {
localWalletDir := setupWalletDir(t)
defaultWalletPath = localWalletDir
ctx := context.Background()
s := &Server{
walletInitializedFeed: new(event.Feed),
walletDir: defaultWalletPath,
}
req := &pb.CreateWalletRequest{
Keymanager: pb.KeymanagerKind_IMPORTED,
request := &CreateWalletRequest{
Keymanager: importedKeymanagerKind,
WalletPassword: "", // Weak password, empty string
}
_, err := s.CreateWallet(ctx, req)
require.ErrorContains(t, "Password too weak", err)
req = &pb.CreateWalletRequest{
Keymanager: pb.KeymanagerKind_IMPORTED,
var buf bytes.Buffer
err := json.NewEncoder(&buf).Encode(request)
require.NoError(t, err)
req := httptest.NewRequest(http.MethodPost, "/v2/validator/wallet/create", &buf)
wr := httptest.NewRecorder()
wr.Body = &bytes.Buffer{}
s.CreateWallet(wr, req)
require.NotEqual(t, http.StatusOK, wr.Code)
require.StringContains(t, "Password too weak", wr.Body.String())
request = &CreateWalletRequest{
Keymanager: importedKeymanagerKind,
WalletPassword: "a", // Weak password, too short
}
_, err = s.CreateWallet(ctx, req)
require.ErrorContains(t, "Password too weak", err)
err = json.NewEncoder(&buf).Encode(request)
require.NoError(t, err)
req = httptest.NewRequest(http.MethodPost, "/v2/validator/wallet/create", &buf)
wr = httptest.NewRecorder()
wr.Body = &bytes.Buffer{}
s.CreateWallet(wr, req)
require.NotEqual(t, http.StatusOK, wr.Code)
require.StringContains(t, "Password too weak", wr.Body.String())
}
func TestServer_RecoverWallet_Derived(t *testing.T) {
localWalletDir := setupWalletDir(t)
ctx := context.Background()
s := &Server{
walletInitializedFeed: new(event.Feed),
walletDir: localWalletDir,
}
req := &pb.RecoverWalletRequest{
request := &RecoverWalletRequest{
WalletPassword: strongPass,
NumAccounts: 0,
}
_, err := s.RecoverWallet(ctx, req)
require.ErrorContains(t, "Must create at least 1 validator account", err)
var buf bytes.Buffer
err := json.NewEncoder(&buf).Encode(request)
require.NoError(t, err)
req := httptest.NewRequest(http.MethodPost, "/v2/validator/wallet/recover", &buf)
wr := httptest.NewRecorder()
wr.Body = &bytes.Buffer{}
s.RecoverWallet(wr, req)
require.NotEqual(t, http.StatusOK, wr.Code)
require.StringContains(t, "Must create at least 1 validator account", wr.Body.String())
req.NumAccounts = 2
req.Language = "Swahili"
_, err = s.RecoverWallet(ctx, req)
require.ErrorContains(t, "input not in the list of supported languages", err)
request.NumAccounts = 2
request.Language = "Swahili"
err = json.NewEncoder(&buf).Encode(request)
require.NoError(t, err)
req = httptest.NewRequest(http.MethodPost, "/v2/validator/wallet/recover", &buf)
wr = httptest.NewRecorder()
wr.Body = &bytes.Buffer{}
s.RecoverWallet(wr, req)
require.NotEqual(t, http.StatusOK, wr.Code)
require.StringContains(t, "input not in the list of supported languages", wr.Body.String())
req.Language = "ENglish"
_, err = s.RecoverWallet(ctx, req)
require.ErrorContains(t, "invalid mnemonic in request", err)
request.Language = "ENglish"
err = json.NewEncoder(&buf).Encode(request)
require.NoError(t, err)
req = httptest.NewRequest(http.MethodPost, "/v2/validator/wallet/recover", &buf)
wr = httptest.NewRecorder()
wr.Body = &bytes.Buffer{}
s.RecoverWallet(wr, req)
require.NotEqual(t, http.StatusOK, wr.Code)
require.StringContains(t, "invalid mnemonic in request", wr.Body.String())
mnemonicRandomness := make([]byte, 32)
_, err = rand.NewGenerator().Read(mnemonicRandomness)
require.NoError(t, err)
mnemonic, err := bip39.NewMnemonic(mnemonicRandomness)
require.NoError(t, err)
req.Mnemonic = mnemonic
request.Mnemonic = mnemonic
request.Mnemonic25ThWord = " "
req.Mnemonic25ThWord = " "
_, err = s.RecoverWallet(ctx, req)
require.ErrorContains(t, "mnemonic 25th word cannot be empty", err)
req.Mnemonic25ThWord = "outer"
err = json.NewEncoder(&buf).Encode(request)
require.NoError(t, err)
req = httptest.NewRequest(http.MethodPost, "/v2/validator/wallet/recover", &buf)
wr = httptest.NewRecorder()
wr.Body = &bytes.Buffer{}
s.RecoverWallet(wr, req)
require.NotEqual(t, http.StatusOK, wr.Code)
require.StringContains(t, "mnemonic 25th word cannot be empty", wr.Body.String())
request.Mnemonic25ThWord = "outer"
// Test weak password.
req.WalletPassword = "123qwe"
_, err = s.RecoverWallet(ctx, req)
require.ErrorContains(t, "password did not pass validation", err)
request.WalletPassword = "123qwe"
req.WalletPassword = strongPass
err = json.NewEncoder(&buf).Encode(request)
require.NoError(t, err)
req = httptest.NewRequest(http.MethodPost, "/v2/validator/wallet/recover", &buf)
wr = httptest.NewRecorder()
wr.Body = &bytes.Buffer{}
s.RecoverWallet(wr, req)
require.NotEqual(t, http.StatusOK, wr.Code)
require.StringContains(t, "password did not pass validation", wr.Body.String())
request.WalletPassword = strongPass
// Create(derived) should fail then test recover.
reqCreate := &pb.CreateWalletRequest{
Keymanager: pb.KeymanagerKind_DERIVED,
reqCreate := &CreateWalletRequest{
Keymanager: derivedKeymanagerKind,
WalletPassword: strongPass,
NumAccounts: 2,
Mnemonic: mnemonic,
}
_, err = s.CreateWallet(ctx, reqCreate)
require.ErrorContains(t, "create wallet not supported through web", err, "Create wallet for DERIVED or REMOTE types not supported through web, either import keystore or recover")
var buff bytes.Buffer
err = json.NewEncoder(&buff).Encode(reqCreate)
require.NoError(t, err)
req = httptest.NewRequest(http.MethodPost, "/v2/validator/wallet/create", &buff)
wr = httptest.NewRecorder()
wr.Body = &bytes.Buffer{}
s.CreateWallet(wr, req)
require.NotEqual(t, http.StatusOK, wr.Code)
require.StringContains(t, "create wallet not supported through web", wr.Body.String())
// This defer will be the last to execute in this func.
resetCfgFalse := features.InitWithReset(&features.Flags{
@@ -203,8 +259,12 @@ func TestServer_RecoverWallet_Derived(t *testing.T) {
defer resetCfgTrue()
// Finally test recover.
_, err = s.RecoverWallet(ctx, req)
err = json.NewEncoder(&buf).Encode(request)
require.NoError(t, err)
req = httptest.NewRequest(http.MethodPost, "/v2/validator/wallet/recover", &buf)
wr = httptest.NewRecorder()
wr.Body = &bytes.Buffer{}
s.RecoverWallet(wr, req)
// Password File should have been written.
passwordFilePath := filepath.Join(localWalletDir, wallet.DefaultWalletPasswordFile)
@@ -217,24 +277,46 @@ func TestServer_RecoverWallet_Derived(t *testing.T) {
}
func TestServer_ValidateKeystores_FailedPreconditions(t *testing.T) {
ctx := context.Background()
strongPass := "29384283xasjasd32%%&*@*#*"
ss := &Server{}
_, err := ss.ValidateKeystores(ctx, &pb.ValidateKeystoresRequest{})
assert.ErrorContains(t, "Password required for keystores", err)
_, err = ss.ValidateKeystores(ctx, &pb.ValidateKeystoresRequest{
request := &ValidateKeystoresRequest{}
var buf bytes.Buffer
err := json.NewEncoder(&buf).Encode(request)
require.NoError(t, err)
req := httptest.NewRequest(http.MethodPost, "/v2/validator/wallet/recover", &buf)
wr := httptest.NewRecorder()
wr.Body = &bytes.Buffer{}
ss.ValidateKeystores(wr, req)
require.NotEqual(t, http.StatusOK, wr.Code)
assert.StringContains(t, "Password required for keystores", wr.Body.String())
request = &ValidateKeystoresRequest{
KeystoresPassword: strongPass,
})
assert.ErrorContains(t, "No keystores included in request", err)
_, err = ss.ValidateKeystores(ctx, &pb.ValidateKeystoresRequest{
}
err = json.NewEncoder(&buf).Encode(request)
require.NoError(t, err)
req = httptest.NewRequest(http.MethodPost, "/v2/validator/wallet/recover", &buf)
wr = httptest.NewRecorder()
wr.Body = &bytes.Buffer{}
ss.ValidateKeystores(wr, req)
require.NotEqual(t, http.StatusOK, wr.Code)
assert.StringContains(t, "No keystores included in request", wr.Body.String())
request = &ValidateKeystoresRequest{
KeystoresPassword: strongPass,
Keystores: []string{"badjson"},
})
assert.ErrorContains(t, "Not a valid EIP-2335 keystore", err)
}
err = json.NewEncoder(&buf).Encode(request)
require.NoError(t, err)
req = httptest.NewRequest(http.MethodPost, "/v2/validator/wallet/recover", &buf)
wr = httptest.NewRecorder()
wr.Body = &bytes.Buffer{}
ss.ValidateKeystores(wr, req)
require.NotEqual(t, http.StatusOK, wr.Code)
assert.StringContains(t, "Not a valid EIP-2335 keystore", wr.Body.String())
}
func TestServer_ValidateKeystores_OK(t *testing.T) {
ctx := context.Background()
strongPass := "29384283xasjasd32%%&*@*#*"
ss := &Server{}
@@ -264,18 +346,32 @@ func TestServer_ValidateKeystores_OK(t *testing.T) {
}
// Validate the keystores and ensure no error.
_, err := ss.ValidateKeystores(ctx, &pb.ValidateKeystoresRequest{
request := &ValidateKeystoresRequest{
KeystoresPassword: strongPass,
Keystores: keystores,
})
}
var buf bytes.Buffer
err := json.NewEncoder(&buf).Encode(request)
require.NoError(t, err)
req := httptest.NewRequest(http.MethodPost, "/v2/validator/wallet/recover", &buf)
wr := httptest.NewRecorder()
wr.Body = &bytes.Buffer{}
ss.ValidateKeystores(wr, req)
require.Equal(t, http.StatusOK, wr.Code)
// Check that using a different password will return an error.
_, err = ss.ValidateKeystores(ctx, &pb.ValidateKeystoresRequest{
request = &ValidateKeystoresRequest{
KeystoresPassword: "badpassword",
Keystores: keystores,
})
require.ErrorContains(t, "is incorrect", err)
}
err = json.NewEncoder(&buf).Encode(request)
require.NoError(t, err)
req = httptest.NewRequest(http.MethodPost, "/v2/validator/wallet/recover", &buf)
wr = httptest.NewRecorder()
wr.Body = &bytes.Buffer{}
ss.ValidateKeystores(wr, req)
require.NotEqual(t, http.StatusOK, wr.Code)
require.StringContains(t, "is incorrect", wr.Body.String())
// Add a new keystore that was encrypted with a different password and expect
// a failure from the function.
@@ -297,18 +393,30 @@ func TestServer_ValidateKeystores_OK(t *testing.T) {
encodedFile, err := json.MarshalIndent(item, "", "\t")
keystores = append(keystores, string(encodedFile))
require.NoError(t, err)
_, err = ss.ValidateKeystores(ctx, &pb.ValidateKeystoresRequest{
request = &ValidateKeystoresRequest{
KeystoresPassword: strongPass,
Keystores: keystores,
})
require.ErrorContains(t, "Password for keystore with public key somepubkey is incorrect", err)
}
err = json.NewEncoder(&buf).Encode(request)
require.NoError(t, err)
req = httptest.NewRequest(http.MethodPost, "/v2/validator/wallet/recover", &buf)
wr = httptest.NewRecorder()
wr.Body = &bytes.Buffer{}
ss.ValidateKeystores(wr, req)
require.NotEqual(t, http.StatusOK, wr.Code)
require.StringContains(t, "Password for keystore with public key somepubkey is incorrect", wr.Body.String())
}
func TestServer_WalletConfig_NoWalletFound(t *testing.T) {
s := &Server{}
resp, err := s.WalletConfig(context.Background(), &empty.Empty{})
require.NoError(t, err)
assert.DeepEqual(t, resp, &pb.WalletResponse{})
req := httptest.NewRequest(http.MethodGet, "/v2/validator/wallet/keystores/validate", nil)
wr := httptest.NewRecorder()
wr.Body = &bytes.Buffer{}
s.WalletConfig(wr, req)
require.Equal(t, http.StatusOK, wr.Code)
var resp WalletResponse
require.NoError(t, json.Unmarshal(wr.Body.Bytes(), &resp))
require.DeepEqual(t, resp, WalletResponse{})
}
func TestServer_WalletConfig(t *testing.T) {
@@ -341,12 +449,17 @@ func TestServer_WalletConfig(t *testing.T) {
})
require.NoError(t, err)
s.validatorService = vs
resp, err := s.WalletConfig(ctx, &empty.Empty{})
require.NoError(t, err)
req := httptest.NewRequest(http.MethodGet, "/v2/validator/wallet/keystores/validate", nil)
wr := httptest.NewRecorder()
wr.Body = &bytes.Buffer{}
s.WalletConfig(wr, req)
require.Equal(t, http.StatusOK, wr.Code)
var resp WalletResponse
require.NoError(t, json.Unmarshal(wr.Body.Bytes(), &resp))
assert.DeepEqual(t, resp, &pb.WalletResponse{
assert.DeepEqual(t, resp, WalletResponse{
WalletPath: localWalletDir,
KeymanagerKind: pb.KeymanagerKind_IMPORTED,
KeymanagerKind: importedKeymanagerKind,
})
}

View File

@@ -18,7 +18,7 @@ import (
"github.com/prysmaticlabs/prysm/v4/consensus-types/primitives"
"github.com/prysmaticlabs/prysm/v4/consensus-types/validator"
"github.com/prysmaticlabs/prysm/v4/encoding/bytesutil"
http2 "github.com/prysmaticlabs/prysm/v4/network/http"
httputil "github.com/prysmaticlabs/prysm/v4/network/http"
"github.com/prysmaticlabs/prysm/v4/validator/client"
"github.com/prysmaticlabs/prysm/v4/validator/keymanager"
"github.com/prysmaticlabs/prysm/v4/validator/keymanager/derived"
@@ -34,25 +34,25 @@ func (s *Server) ListKeystores(w http.ResponseWriter, r *http.Request) {
defer span.End()
if s.validatorService == nil {
http2.HandleError(w, "Validator service not ready.", http.StatusServiceUnavailable)
httputil.HandleError(w, "Validator service not ready.", http.StatusServiceUnavailable)
return
}
if !s.walletInitialized {
http2.HandleError(w, "Prysm Wallet not initialized. Please create a new wallet.", http.StatusServiceUnavailable)
httputil.HandleError(w, "Prysm Wallet not initialized. Please create a new wallet.", http.StatusServiceUnavailable)
return
}
km, err := s.validatorService.Keymanager()
if err != nil {
http2.HandleError(w, err.Error(), http.StatusInternalServerError)
httputil.HandleError(w, err.Error(), http.StatusInternalServerError)
return
}
if s.wallet.KeymanagerKind() != keymanager.Derived && s.wallet.KeymanagerKind() != keymanager.Local {
http2.HandleError(w, errors.Wrap(err, "Prysm validator keys are not stored locally with this keymanager type").Error(), http.StatusInternalServerError)
httputil.HandleError(w, errors.Wrap(err, "Prysm validator keys are not stored locally with this keymanager type").Error(), http.StatusInternalServerError)
return
}
pubKeys, err := km.FetchValidatingPublicKeys(ctx)
if err != nil {
http2.HandleError(w, errors.Wrap(err, "Could not retrieve keystores").Error(), http.StatusInternalServerError)
httputil.HandleError(w, errors.Wrap(err, "Could not retrieve keystores").Error(), http.StatusInternalServerError)
return
}
keystoreResponse := make([]*Keystore, len(pubKeys))
@@ -67,7 +67,7 @@ func (s *Server) ListKeystores(w http.ResponseWriter, r *http.Request) {
response := &ListKeystoresResponse{
Data: keystoreResponse,
}
http2.WriteJson(w, response)
httputil.WriteJson(w, response)
}
// ImportKeystores allows for importing keystores into Prysm with their slashing protection history.
@@ -76,22 +76,22 @@ func (s *Server) ImportKeystores(w http.ResponseWriter, r *http.Request) {
defer span.End()
if s.validatorService == nil {
http2.HandleError(w, "Validator service not ready.", http.StatusServiceUnavailable)
httputil.HandleError(w, "Validator service not ready.", http.StatusServiceUnavailable)
return
}
if !s.walletInitialized {
http2.HandleError(w, "Prysm Wallet not initialized. Please create a new wallet.", http.StatusServiceUnavailable)
httputil.HandleError(w, "Prysm Wallet not initialized. Please create a new wallet.", http.StatusServiceUnavailable)
return
}
km, err := s.validatorService.Keymanager()
if err != nil {
http2.HandleError(w, err.Error(), http.StatusInternalServerError)
httputil.HandleError(w, err.Error(), http.StatusInternalServerError)
return
}
var req ImportKeystoresRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http2.HandleError(w, "Could not decode request body: "+err.Error(), http.StatusBadRequest)
httputil.HandleError(w, "Could not decode request body: "+err.Error(), http.StatusBadRequest)
return
}
@@ -104,11 +104,11 @@ func (s *Server) ImportKeystores(w http.ResponseWriter, r *http.Request) {
Message: fmt.Sprintf("Keymanager kind %T cannot import local keys", km),
}
}
http2.WriteJson(w, &ImportKeystoresResponse{Data: statuses})
httputil.WriteJson(w, &ImportKeystoresResponse{Data: statuses})
return
}
if len(req.Keystores) == 0 {
http2.WriteJson(w, &ImportKeystoresResponse{})
httputil.WriteJson(w, &ImportKeystoresResponse{})
return
}
keystores := make([]*keymanager.Keystore, len(req.Keystores))
@@ -135,7 +135,7 @@ func (s *Server) ImportKeystores(w http.ResponseWriter, r *http.Request) {
Message: fmt.Sprintf("could not import slashing protection: %v", err),
}
}
http2.WriteJson(w, &ImportKeystoresResponse{Data: statuses})
httputil.WriteJson(w, &ImportKeystoresResponse{Data: statuses})
return
}
}
@@ -154,13 +154,13 @@ func (s *Server) ImportKeystores(w http.ResponseWriter, r *http.Request) {
statuses, err := importer.ImportKeystores(ctx, keystores, req.Passwords)
if err != nil {
http2.HandleError(w, errors.Wrap(err, "Could not import keystores").Error(), http.StatusInternalServerError)
httputil.HandleError(w, errors.Wrap(err, "Could not import keystores").Error(), http.StatusInternalServerError)
return
}
// If any of the keys imported had a slashing protection history before, we
// stop marking them as deleted from our validator database.
http2.WriteJson(w, &ImportKeystoresResponse{Data: statuses})
httputil.WriteJson(w, &ImportKeystoresResponse{Data: statuses})
}
// DeleteKeystores allows for deleting specified public keys from Prysm.
@@ -169,25 +169,25 @@ func (s *Server) DeleteKeystores(w http.ResponseWriter, r *http.Request) {
defer span.End()
if s.validatorService == nil {
http2.HandleError(w, "Validator service not ready.", http.StatusServiceUnavailable)
httputil.HandleError(w, "Validator service not ready.", http.StatusServiceUnavailable)
return
}
if !s.walletInitialized {
http2.HandleError(w, "Prysm Wallet not initialized. Please create a new wallet.", http.StatusServiceUnavailable)
httputil.HandleError(w, "Prysm Wallet not initialized. Please create a new wallet.", http.StatusServiceUnavailable)
return
}
km, err := s.validatorService.Keymanager()
if err != nil {
http2.HandleError(w, err.Error(), http.StatusInternalServerError)
httputil.HandleError(w, err.Error(), http.StatusInternalServerError)
return
}
var req DeleteKeystoresRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http2.HandleError(w, "Could not decode request body: "+err.Error(), http.StatusBadRequest)
httputil.HandleError(w, "Could not decode request body: "+err.Error(), http.StatusBadRequest)
return
}
if len(req.Pubkeys) == 0 {
http2.WriteJson(w, &DeleteKeystoresResponse{Data: make([]*keymanager.KeyStatus, 0)})
httputil.WriteJson(w, &DeleteKeystoresResponse{Data: make([]*keymanager.KeyStatus, 0)})
return
}
deleter, ok := km.(keymanager.Deleter)
@@ -199,7 +199,7 @@ func (s *Server) DeleteKeystores(w http.ResponseWriter, r *http.Request) {
Message: fmt.Sprintf("Keymanager kind %T cannot delete local keys", km),
}
}
http2.WriteJson(w, &DeleteKeystoresResponse{Data: sts})
httputil.WriteJson(w, &DeleteKeystoresResponse{Data: sts})
return
}
bytePubKeys := make([][]byte, len(req.Pubkeys))
@@ -212,13 +212,13 @@ func (s *Server) DeleteKeystores(w http.ResponseWriter, r *http.Request) {
}
statuses, err := deleter.DeleteKeystores(ctx, bytePubKeys)
if err != nil {
http2.HandleError(w, errors.Wrap(err, "Could not delete keys").Error(), http.StatusInternalServerError)
httputil.HandleError(w, errors.Wrap(err, "Could not delete keys").Error(), http.StatusInternalServerError)
return
}
statuses, err = s.transformDeletedKeysStatuses(ctx, bytePubKeys, statuses)
if err != nil {
http2.HandleError(w, errors.Wrap(err, "Could not transform deleted keys statuses").Error(), http.StatusInternalServerError)
httputil.HandleError(w, errors.Wrap(err, "Could not transform deleted keys statuses").Error(), http.StatusInternalServerError)
return
}
@@ -232,12 +232,12 @@ func (s *Server) DeleteKeystores(w http.ResponseWriter, r *http.Request) {
Message: "Could not export slashing protection history as existing non duplicate keys were deleted",
}
}
http2.WriteJson(w, &DeleteKeystoresResponse{Data: sts})
httputil.WriteJson(w, &DeleteKeystoresResponse{Data: sts})
return
}
jsonHist, err := json.Marshal(exportedHistory)
if err != nil {
http2.HandleError(w, errors.Wrap(err, "Could not JSON marshal slashing protection history").Error(), http.StatusInternalServerError)
httputil.HandleError(w, errors.Wrap(err, "Could not JSON marshal slashing protection history").Error(), http.StatusInternalServerError)
return
}
@@ -245,7 +245,7 @@ func (s *Server) DeleteKeystores(w http.ResponseWriter, r *http.Request) {
Data: statuses,
SlashingProtection: string(jsonHist),
}
http2.WriteJson(w, response)
httputil.WriteJson(w, response)
}
// For a list of deleted keystore statuses, we check if any NOT_FOUND status actually
@@ -310,24 +310,24 @@ func (s *Server) SetVoluntaryExit(w http.ResponseWriter, r *http.Request) {
defer span.End()
if s.validatorService == nil {
http2.HandleError(w, "Validator service not ready", http.StatusServiceUnavailable)
httputil.HandleError(w, "Validator service not ready", http.StatusServiceUnavailable)
return
}
if !s.walletInitialized {
http2.HandleError(w, "No wallet found", http.StatusServiceUnavailable)
httputil.HandleError(w, "No wallet found", http.StatusServiceUnavailable)
return
}
km, err := s.validatorService.Keymanager()
if err != nil {
http2.HandleError(w, err.Error(), http.StatusInternalServerError)
httputil.HandleError(w, err.Error(), http.StatusInternalServerError)
return
}
rawPubkey := mux.Vars(r)["pubkey"]
if rawPubkey == "" {
http2.HandleError(w, "pubkey is required in URL params", http.StatusBadRequest)
httputil.HandleError(w, "pubkey is required in URL params", http.StatusBadRequest)
return
}
@@ -339,7 +339,7 @@ func (s *Server) SetVoluntaryExit(w http.ResponseWriter, r *http.Request) {
var epoch primitives.Epoch
ok, _, e := shared.UintFromQuery(w, r, "epoch")
if !ok {
http2.HandleError(w, "Invalid epoch", http.StatusBadRequest)
httputil.HandleError(w, "Invalid epoch", http.StatusBadRequest)
return
}
epoch = primitives.Epoch(e)
@@ -347,12 +347,12 @@ func (s *Server) SetVoluntaryExit(w http.ResponseWriter, r *http.Request) {
if epoch == 0 {
genesisResponse, err := s.beaconNodeClient.GetGenesis(ctx, &emptypb.Empty{})
if err != nil {
http2.HandleError(w, errors.Wrap(err, "Failed to get genesis time").Error(), http.StatusInternalServerError)
httputil.HandleError(w, errors.Wrap(err, "Failed to get genesis time").Error(), http.StatusInternalServerError)
return
}
currentEpoch, err := client.CurrentEpoch(genesisResponse.GenesisTime)
if err != nil {
http2.HandleError(w, errors.Wrap(err, "Failed to get current epoch").Error(), http.StatusInternalServerError)
httputil.HandleError(w, errors.Wrap(err, "Failed to get current epoch").Error(), http.StatusInternalServerError)
return
}
epoch = currentEpoch
@@ -365,7 +365,7 @@ func (s *Server) SetVoluntaryExit(w http.ResponseWriter, r *http.Request) {
epoch,
)
if err != nil {
http2.HandleError(w, errors.Wrap(err, "Could not create voluntary exit").Error(), http.StatusInternalServerError)
httputil.HandleError(w, errors.Wrap(err, "Could not create voluntary exit").Error(), http.StatusInternalServerError)
return
}
@@ -378,7 +378,7 @@ func (s *Server) SetVoluntaryExit(w http.ResponseWriter, r *http.Request) {
Signature: hexutil.Encode(sve.Signature),
},
}
http2.WriteJson(w, response)
httputil.WriteJson(w, response)
}
// ListRemoteKeys returns a list of all public keys defined for web3signer keymanager type.
@@ -387,25 +387,25 @@ func (s *Server) ListRemoteKeys(w http.ResponseWriter, r *http.Request) {
defer span.End()
if s.validatorService == nil {
http2.HandleError(w, "Validator service not ready.", http.StatusServiceUnavailable)
httputil.HandleError(w, "Validator service not ready.", http.StatusServiceUnavailable)
return
}
if !s.walletInitialized {
http2.HandleError(w, "Prysm Wallet not initialized. Please create a new wallet.", http.StatusServiceUnavailable)
httputil.HandleError(w, "Prysm Wallet not initialized. Please create a new wallet.", http.StatusServiceUnavailable)
return
}
km, err := s.validatorService.Keymanager()
if err != nil {
http2.HandleError(w, err.Error(), http.StatusInternalServerError)
httputil.HandleError(w, err.Error(), http.StatusInternalServerError)
return
}
if s.wallet.KeymanagerKind() != keymanager.Web3Signer {
http2.HandleError(w, "Prysm Wallet is not of type Web3Signer. Please execute validator client with web3signer flags.", http.StatusInternalServerError)
httputil.HandleError(w, "Prysm Wallet is not of type Web3Signer. Please execute validator client with web3signer flags.", http.StatusInternalServerError)
return
}
pubKeys, err := km.FetchValidatingPublicKeys(ctx)
if err != nil {
http2.HandleError(w, errors.Errorf("Could not retrieve public keys: %v", err).Error(), http.StatusInternalServerError)
httputil.HandleError(w, errors.Errorf("Could not retrieve public keys: %v", err).Error(), http.StatusInternalServerError)
return
}
keystoreResponse := make([]*RemoteKey, len(pubKeys))
@@ -420,7 +420,7 @@ func (s *Server) ListRemoteKeys(w http.ResponseWriter, r *http.Request) {
response := &ListRemoteKeysResponse{
Data: keystoreResponse,
}
http2.WriteJson(w, response)
httputil.WriteJson(w, response)
}
// ImportRemoteKeys imports a list of public keys defined for web3signer keymanager type.
@@ -429,26 +429,26 @@ func (s *Server) ImportRemoteKeys(w http.ResponseWriter, r *http.Request) {
defer span.End()
if s.validatorService == nil {
http2.HandleError(w, "Validator service not ready.", http.StatusServiceUnavailable)
httputil.HandleError(w, "Validator service not ready.", http.StatusServiceUnavailable)
return
}
if !s.walletInitialized {
http2.HandleError(w, "Prysm Wallet not initialized. Please create a new wallet.", http.StatusServiceUnavailable)
httputil.HandleError(w, "Prysm Wallet not initialized. Please create a new wallet.", http.StatusServiceUnavailable)
return
}
km, err := s.validatorService.Keymanager()
if err != nil {
http2.HandleError(w, err.Error(), http.StatusInternalServerError)
httputil.HandleError(w, err.Error(), http.StatusInternalServerError)
return
}
if s.wallet.KeymanagerKind() != keymanager.Web3Signer {
http2.HandleError(w, "Prysm Wallet is not of type Web3Signer. Please execute validator client with web3signer flags.", http.StatusInternalServerError)
httputil.HandleError(w, "Prysm Wallet is not of type Web3Signer. Please execute validator client with web3signer flags.", http.StatusInternalServerError)
return
}
var req ImportRemoteKeysRequest
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
http2.HandleError(w, "Could not decode request body: "+err.Error(), http.StatusBadRequest)
httputil.HandleError(w, "Could not decode request body: "+err.Error(), http.StatusBadRequest)
return
}
adder, ok := km.(keymanager.PublicKeyAdder)
@@ -460,7 +460,7 @@ func (s *Server) ImportRemoteKeys(w http.ResponseWriter, r *http.Request) {
Message: "Keymanager kind cannot import public keys for web3signer keymanager type.",
}
}
http2.WriteJson(w, &RemoteKeysResponse{Data: statuses})
httputil.WriteJson(w, &RemoteKeysResponse{Data: statuses})
return
}
@@ -473,10 +473,10 @@ func (s *Server) ImportRemoteKeys(w http.ResponseWriter, r *http.Request) {
}
}
if isUrlUsed {
log.Warnf("Setting web3signer base url for imported keys is not supported. Prysm only uses the url from --validators-external-signer-url flag for web3signer.")
log.Warnf("Setting web3signer base url for IMPORTED keys is not supported. Prysm only uses the url from --validators-external-signer-url flag for web3signerKeymanagerKind.")
}
http2.WriteJson(w, &RemoteKeysResponse{Data: adder.AddPublicKeys(remoteKeys)})
httputil.WriteJson(w, &RemoteKeysResponse{Data: adder.AddPublicKeys(remoteKeys)})
}
// DeleteRemoteKeys deletes a list of public keys defined for web3signer keymanager type.
@@ -485,25 +485,25 @@ func (s *Server) DeleteRemoteKeys(w http.ResponseWriter, r *http.Request) {
defer span.End()
if s.validatorService == nil {
http2.HandleError(w, "Validator service not ready.", http.StatusServiceUnavailable)
httputil.HandleError(w, "Validator service not ready.", http.StatusServiceUnavailable)
return
}
if !s.walletInitialized {
http2.HandleError(w, "Prysm Wallet not initialized. Please create a new wallet.", http.StatusServiceUnavailable)
httputil.HandleError(w, "Prysm Wallet not initialized. Please create a new wallet.", http.StatusServiceUnavailable)
return
}
km, err := s.validatorService.Keymanager()
if err != nil {
http2.HandleError(w, err.Error(), http.StatusInternalServerError)
httputil.HandleError(w, err.Error(), http.StatusInternalServerError)
return
}
if s.wallet.KeymanagerKind() != keymanager.Web3Signer {
http2.HandleError(w, "Prysm Wallet is not of type Web3Signer. Please execute validator client with web3signer flags.", http.StatusInternalServerError)
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 {
http2.HandleError(w, "Could not decode request body: "+err.Error(), http.StatusBadRequest)
httputil.HandleError(w, "Could not decode request body: "+err.Error(), http.StatusBadRequest)
return
}
@@ -516,11 +516,11 @@ func (s *Server) DeleteRemoteKeys(w http.ResponseWriter, r *http.Request) {
Message: "Keymanager kind cannot delete public keys for web3signer keymanager type.",
}
}
http2.WriteJson(w, &RemoteKeysResponse{Data: statuses})
httputil.WriteJson(w, &RemoteKeysResponse{Data: statuses})
return
}
http2.WriteJson(w, RemoteKeysResponse{Data: deleter.DeletePublicKeys(req.Pubkeys)})
httputil.WriteJson(w, RemoteKeysResponse{Data: deleter.DeletePublicKeys(req.Pubkeys)})
}
// ListFeeRecipientByPubkey returns the public key to eth address mapping object to the end user.
@@ -529,13 +529,13 @@ func (s *Server) ListFeeRecipientByPubkey(w http.ResponseWriter, r *http.Request
defer span.End()
if s.validatorService == nil {
http2.HandleError(w, "Validator service not ready.", http.StatusServiceUnavailable)
httputil.HandleError(w, "Validator service not ready.", http.StatusServiceUnavailable)
return
}
rawPubkey := mux.Vars(r)["pubkey"]
if rawPubkey == "" {
http2.HandleError(w, "pubkey is required in URL params", http.StatusBadRequest)
httputil.HandleError(w, "pubkey is required in URL params", http.StatusBadRequest)
return
}
@@ -557,7 +557,7 @@ func (s *Server) ListFeeRecipientByPubkey(w http.ResponseWriter, r *http.Request
if found && proposerOption.FeeRecipientConfig != nil {
finalResp.Data.Ethaddress = proposerOption.FeeRecipientConfig.FeeRecipient.String()
http2.WriteJson(w, finalResp)
httputil.WriteJson(w, finalResp)
return
}
}
@@ -565,11 +565,11 @@ func (s *Server) ListFeeRecipientByPubkey(w http.ResponseWriter, r *http.Request
// If fee recipient is defined in default configuration, use it
if proposerSettings != nil && proposerSettings.DefaultConfig != nil && proposerSettings.DefaultConfig.FeeRecipientConfig != nil {
finalResp.Data.Ethaddress = proposerSettings.DefaultConfig.FeeRecipientConfig.FeeRecipient.String()
http2.WriteJson(w, finalResp)
httputil.WriteJson(w, finalResp)
return
}
http2.HandleError(w, "No fee recipient set", http.StatusBadRequest)
httputil.HandleError(w, "No fee recipient set", http.StatusBadRequest)
}
// SetFeeRecipientByPubkey updates the eth address mapped to the public key.
@@ -578,13 +578,13 @@ func (s *Server) SetFeeRecipientByPubkey(w http.ResponseWriter, r *http.Request)
defer span.End()
if s.validatorService == nil {
http2.HandleError(w, "Validator service not ready.", http.StatusServiceUnavailable)
httputil.HandleError(w, "Validator service not ready.", http.StatusServiceUnavailable)
return
}
rawPubkey := mux.Vars(r)["pubkey"]
if rawPubkey == "" {
http2.HandleError(w, "pubkey is required in URL params", http.StatusBadRequest)
httputil.HandleError(w, "pubkey is required in URL params", http.StatusBadRequest)
return
}
@@ -594,7 +594,7 @@ func (s *Server) SetFeeRecipientByPubkey(w http.ResponseWriter, r *http.Request)
}
var req SetFeeRecipientByPubkeyRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http2.HandleError(w, "Could not decode request body: "+err.Error(), http.StatusBadRequest)
httputil.HandleError(w, "Could not decode request body: "+err.Error(), http.StatusBadRequest)
return
}
ethAddress, valid := shared.ValidateHex(w, "Ethereum Address", req.Ethaddress, fieldparams.FeeRecipientLength)
@@ -650,7 +650,7 @@ func (s *Server) SetFeeRecipientByPubkey(w http.ResponseWriter, r *http.Request)
}
// save the settings
if err := s.validatorService.SetProposerSettings(ctx, settings); err != nil {
http2.HandleError(w, "Could not set proposer settings: "+err.Error(), http.StatusInternalServerError)
httputil.HandleError(w, "Could not set proposer settings: "+err.Error(), http.StatusInternalServerError)
return
}
// override the 200 success with 202 according to the specs
@@ -663,12 +663,12 @@ func (s *Server) DeleteFeeRecipientByPubkey(w http.ResponseWriter, r *http.Reque
defer span.End()
if s.validatorService == nil {
http2.HandleError(w, "Validator service not ready.", http.StatusServiceUnavailable)
httputil.HandleError(w, "Validator service not ready.", http.StatusServiceUnavailable)
return
}
rawPubkey := mux.Vars(r)["pubkey"]
if rawPubkey == "" {
http2.HandleError(w, "pubkey is required in URL params", http.StatusBadRequest)
httputil.HandleError(w, "pubkey is required in URL params", http.StatusBadRequest)
return
}
@@ -687,7 +687,7 @@ func (s *Server) DeleteFeeRecipientByPubkey(w http.ResponseWriter, r *http.Reque
// save the settings
if err := s.validatorService.SetProposerSettings(ctx, settings); err != nil {
http2.HandleError(w, "Could not set proposer settings: "+err.Error(), http.StatusInternalServerError)
httputil.HandleError(w, "Could not set proposer settings: "+err.Error(), http.StatusInternalServerError)
return
}
@@ -701,13 +701,13 @@ func (s *Server) GetGasLimit(w http.ResponseWriter, r *http.Request) {
defer span.End()
if s.validatorService == nil {
http2.HandleError(w, "Validator service not ready", http.StatusServiceUnavailable)
httputil.HandleError(w, "Validator service not ready", http.StatusServiceUnavailable)
return
}
rawPubkey := mux.Vars(r)["pubkey"]
if rawPubkey == "" {
http2.HandleError(w, "pubkey is required in URL params", http.StatusBadRequest)
httputil.HandleError(w, "pubkey is required in URL params", http.StatusBadRequest)
return
}
@@ -733,7 +733,7 @@ func (s *Server) GetGasLimit(w http.ResponseWriter, r *http.Request) {
resp.Data.GasLimit = fmt.Sprintf("%d", s.validatorService.ProposerSettings().DefaultConfig.BuilderConfig.GasLimit)
}
}
http2.WriteJson(w, resp)
httputil.WriteJson(w, resp)
}
// SetGasLimit updates the gas limit by public key
@@ -742,12 +742,12 @@ func (s *Server) SetGasLimit(w http.ResponseWriter, r *http.Request) {
defer span.End()
if s.validatorService == nil {
http2.HandleError(w, "Validator service not ready", http.StatusServiceUnavailable)
httputil.HandleError(w, "Validator service not ready", http.StatusServiceUnavailable)
return
}
rawPubkey := mux.Vars(r)["pubkey"]
if rawPubkey == "" {
http2.HandleError(w, "pubkey is required in URL params", http.StatusBadRequest)
httputil.HandleError(w, "pubkey is required in URL params", http.StatusBadRequest)
return
}
@@ -758,7 +758,7 @@ func (s *Server) SetGasLimit(w http.ResponseWriter, r *http.Request) {
var req SetGasLimitRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http2.HandleError(w, "Could not decode request body: "+err.Error(), http.StatusBadRequest)
httputil.HandleError(w, "Could not decode request body: "+err.Error(), http.StatusBadRequest)
return
}
@@ -769,11 +769,11 @@ func (s *Server) SetGasLimit(w http.ResponseWriter, r *http.Request) {
settings := s.validatorService.ProposerSettings()
if settings == nil {
http2.HandleError(w, "No proposer settings were found to update", http.StatusInternalServerError)
httputil.HandleError(w, "No proposer settings were found to update", http.StatusInternalServerError)
return
} else if settings.ProposeConfig == nil {
if settings.DefaultConfig == nil || settings.DefaultConfig.BuilderConfig == nil || !settings.DefaultConfig.BuilderConfig.Enabled {
http2.HandleError(w, "Gas limit changes only apply when builder is enabled", http.StatusInternalServerError)
httputil.HandleError(w, "Gas limit changes only apply when builder is enabled", http.StatusInternalServerError)
return
}
settings.ProposeConfig = make(map[[fieldparams.BLSPubkeyLength]byte]*validatorServiceConfig.ProposerOption)
@@ -784,14 +784,14 @@ func (s *Server) SetGasLimit(w http.ResponseWriter, r *http.Request) {
proposerOption, found := settings.ProposeConfig[bytesutil.ToBytes48(pubkey)]
if found {
if proposerOption.BuilderConfig == nil || !proposerOption.BuilderConfig.Enabled {
http2.HandleError(w, "Gas limit changes only apply when builder is enabled", http.StatusInternalServerError)
httputil.HandleError(w, "Gas limit changes only apply when builder is enabled", http.StatusInternalServerError)
return
} else {
proposerOption.BuilderConfig.GasLimit = validator.Uint64(gasLimit)
}
} else {
if settings.DefaultConfig == nil {
http2.HandleError(w, "Gas limit changes only apply when builder is enabled", http.StatusInternalServerError)
httputil.HandleError(w, "Gas limit changes only apply when builder is enabled", http.StatusInternalServerError)
return
}
option := settings.DefaultConfig.Clone()
@@ -801,7 +801,7 @@ func (s *Server) SetGasLimit(w http.ResponseWriter, r *http.Request) {
}
// save the settings
if err := s.validatorService.SetProposerSettings(ctx, settings); err != nil {
http2.HandleError(w, "Could not set proposer settings: "+err.Error(), http.StatusInternalServerError)
httputil.HandleError(w, "Could not set proposer settings: "+err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusAccepted)
@@ -813,12 +813,12 @@ func (s *Server) DeleteGasLimit(w http.ResponseWriter, r *http.Request) {
defer span.End()
if s.validatorService == nil {
http2.HandleError(w, "Validator service not ready", http.StatusServiceUnavailable)
httputil.HandleError(w, "Validator service not ready", http.StatusServiceUnavailable)
return
}
rawPubkey := mux.Vars(r)["pubkey"]
if rawPubkey == "" {
http2.HandleError(w, "pubkey is required in URL params", http.StatusBadRequest)
httputil.HandleError(w, "pubkey is required in URL params", http.StatusBadRequest)
return
}
@@ -840,7 +840,7 @@ func (s *Server) DeleteGasLimit(w http.ResponseWriter, r *http.Request) {
}
// save the settings
if err := s.validatorService.SetProposerSettings(ctx, proposerSettings); err != nil {
http2.HandleError(w, "Could not set proposer settings: "+err.Error(), http.StatusBadRequest)
httputil.HandleError(w, "Could not set proposer settings: "+err.Error(), http.StatusBadRequest)
return
}
// Successfully deleted gas limit (reset to proposer config default or global default).
@@ -851,5 +851,5 @@ func (s *Server) DeleteGasLimit(w http.ResponseWriter, r *http.Request) {
}
// Otherwise, either no proposerOption is found for the pubkey or proposerOption.BuilderConfig is not enabled at all,
// we respond "not found".
http2.HandleError(w, fmt.Sprintf("No gas limit found for pubkey: %q", rawPubkey), http.StatusNotFound)
httputil.HandleError(w, fmt.Sprintf("No gas limit found for pubkey: %q", rawPubkey), http.StatusNotFound)
}

View File

@@ -184,7 +184,6 @@ func (s *Server) Start() {
// Register services available for the gRPC server.
reflection.Register(s.grpcServer)
validatorpb.RegisterAuthServer(s.grpcServer, s)
validatorpb.RegisterWalletServer(s.grpcServer, s)
validatorpb.RegisterBeaconServer(s.grpcServer, s)
validatorpb.RegisterAccountsServer(s.grpcServer, s)
@@ -236,6 +235,11 @@ func (s *Server) InitializeRoutes() error {
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)
// 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)
// 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)

View File

@@ -27,6 +27,10 @@ func TestServer_InitializeRoutes(t *testing.T) {
"/v2/validator/health/version": {http.MethodGet},
"/v2/validator/health/logs/validator/stream": {http.MethodGet},
"/v2/validator/health/logs/beacon/stream": {http.MethodGet},
"/v2/validator/wallet": {http.MethodGet},
"/v2/validator/wallet/create": {http.MethodPost},
"/v2/validator/wallet/keystores/validate": {http.MethodPost},
"/v2/validator/wallet/recover": {http.MethodPost},
"/v2/validator/slashing-protection/export": {http.MethodGet},
"/v2/validator/slashing-protection/import": {http.MethodPost},
}

View File

@@ -90,9 +90,53 @@ type SetFeeRecipientByPubkeyRequest struct {
Ethaddress string `json:"ethaddress"`
}
// KeymanagerKind is a type of key manager for the wallet
type KeymanagerKind string
const (
derivedKeymanagerKind KeymanagerKind = "DERIVED"
importedKeymanagerKind KeymanagerKind = "IMPORTED"
web3signerKeymanagerKind KeymanagerKind = "WEB3SIGNER"
)
type CreateWalletRequest struct {
Keymanager KeymanagerKind `json:"keymanager"`
WalletPassword string `json:"wallet_password"`
Mnemonic string `json:"mnemonic"`
NumAccounts uint64 `json:"num_accounts"`
MnemonicLanguage string `json:"mnemonic_language"`
}
type CreateWalletResponse struct {
Wallet *WalletResponse `json:"wallet"`
}
type GenerateMnemonicResponse struct {
Mnemonic string `json:"mnemonic"`
}
type WalletResponse struct {
WalletPath string `json:"wallet_path"`
KeymanagerKind KeymanagerKind `json:"keymanager_kind"`
}
type ValidateKeystoresRequest struct {
Keystores []string `json:"keystores"`
KeystoresPassword string `json:"keystores_password"`
}
type RecoverWalletRequest struct {
Mnemonic string `json:"mnemonic"`
NumAccounts uint64 `json:"num_accounts"`
WalletPassword string `json:"wallet_password"`
Language string `json:"language"`
Mnemonic25ThWord string `json:"mnemonic25th_word"`
}
type ImportSlashingProtectionRequest struct {
SlashingProtectionJson string `json:"slashing_protection_json"`
}
type ExportSlashingProtectionResponse struct {
File string `json:"file"`
}